@treeviz/familysearch-sdk 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 gedcom-visualiser contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # familysearch-sdk
2
+
3
+ A modern, TypeScript-first SDK for the FamilySearch API v3.
4
+
5
+ ## Features
6
+
7
+ - 🔷 **Full TypeScript support** with comprehensive type definitions
8
+ - 🔐 **OAuth v3 compatible** authentication utilities
9
+ - 📊 **Promise-based API** for async operations
10
+ - 🌍 **Environment support** (production, beta, integration)
11
+ - 📝 **GEDCOM export** - Convert FamilySearch data to GEDCOM 5.5 format
12
+ - 📍 **Places API** helpers for location searches
13
+ - 👨‍👩‍👧 **Tree/Pedigree API** for ancestry data
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install familysearch-sdk
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import {
25
+ createFamilySearchSDK,
26
+ fetchPedigree,
27
+ convertToGedcom
28
+ } from 'familysearch-sdk';
29
+
30
+ // Create SDK instance with your OAuth access token
31
+ const sdk = createFamilySearchSDK({
32
+ environment: 'production',
33
+ accessToken: 'your-oauth-token'
34
+ });
35
+
36
+ // Fetch pedigree data
37
+ const pedigree = await fetchPedigree(sdk, undefined, {
38
+ generations: 5,
39
+ onProgress: (progress) => {
40
+ console.log(`${progress.percent}% complete`);
41
+ }
42
+ });
43
+
44
+ // Convert to GEDCOM format
45
+ const gedcom = convertToGedcom(pedigree, {
46
+ treeName: 'My Family Tree'
47
+ });
48
+
49
+ console.log(gedcom);
50
+ ```
51
+
52
+ ## OAuth Authentication
53
+
54
+ The SDK provides utilities for OAuth 2.0 authentication with FamilySearch.
55
+
56
+ ```typescript
57
+ import {
58
+ generateOAuthState,
59
+ buildAuthorizationUrl,
60
+ exchangeCodeForToken,
61
+ validateAccessToken
62
+ } from 'familysearch-sdk/auth';
63
+
64
+ // Generate state for CSRF protection
65
+ const state = generateOAuthState();
66
+
67
+ // Build authorization URL
68
+ const authUrl = buildAuthorizationUrl({
69
+ clientId: 'your-client-id',
70
+ redirectUri: 'https://your-app.com/callback',
71
+ environment: 'production'
72
+ }, state);
73
+
74
+ // Redirect user to authUrl...
75
+
76
+ // After callback, exchange code for token
77
+ const tokens = await exchangeCodeForToken(code, {
78
+ clientId: 'your-client-id',
79
+ redirectUri: 'https://your-app.com/callback',
80
+ environment: 'production'
81
+ });
82
+
83
+ // Validate token
84
+ const isValid = await validateAccessToken(tokens.access_token, 'production');
85
+ ```
86
+
87
+ ## Places API
88
+
89
+ Search and retrieve place information from FamilySearch.
90
+
91
+ ```typescript
92
+ import { createFamilySearchSDK } from 'familysearch-sdk';
93
+ import { searchPlaces, getPlaceDetails } from 'familysearch-sdk/places';
94
+
95
+ const sdk = createFamilySearchSDK({ accessToken: 'token' });
96
+
97
+ // Search for places
98
+ const results = await searchPlaces(sdk, 'London, England', {
99
+ date: '1850',
100
+ count: 10
101
+ });
102
+
103
+ // Get place details
104
+ const details = await getPlaceDetails(sdk, 'place-id');
105
+ console.log(details.name, details.latitude, details.longitude);
106
+ ```
107
+
108
+ ## Tree/Pedigree API
109
+
110
+ Fetch and manage family tree data.
111
+
112
+ ```typescript
113
+ import { createFamilySearchSDK } from 'familysearch-sdk';
114
+ import { fetchPedigree, getCurrentUser } from 'familysearch-sdk/tree';
115
+
116
+ const sdk = createFamilySearchSDK({ accessToken: 'token' });
117
+
118
+ // Get current user
119
+ const user = await getCurrentUser(sdk);
120
+ console.log(user?.displayName);
121
+
122
+ // Fetch pedigree (will use current user's personId)
123
+ const pedigree = await fetchPedigree(sdk, undefined, {
124
+ generations: 4,
125
+ includeDetails: true,
126
+ includeNotes: true
127
+ });
128
+ ```
129
+
130
+ ## GEDCOM Conversion
131
+
132
+ Convert FamilySearch data to GEDCOM 5.5 format.
133
+
134
+ ```typescript
135
+ import { convertToGedcom } from 'familysearch-sdk/utils';
136
+
137
+ const gedcom = convertToGedcom(pedigreeData, {
138
+ treeName: 'Family Tree',
139
+ includeLinks: true,
140
+ includeNotes: true
141
+ });
142
+
143
+ // Save to file
144
+ fs.writeFileSync('family.ged', gedcom);
145
+ ```
146
+
147
+ ## Environment Configuration
148
+
149
+ The SDK supports three FamilySearch environments:
150
+
151
+ | Environment | Description | API Host |
152
+ |-------------|-------------|----------|
153
+ | `production` | Live production API | api.familysearch.org |
154
+ | `beta` | Beta testing environment | apibeta.familysearch.org |
155
+ | `integration` | Sandbox for development | api-integ.familysearch.org |
156
+
157
+ ```typescript
158
+ import { createFamilySearchSDK, ENVIRONMENT_CONFIGS } from 'familysearch-sdk';
159
+
160
+ // Create SDK for production
161
+ const sdk = createFamilySearchSDK({
162
+ environment: 'production',
163
+ accessToken: 'token'
164
+ });
165
+
166
+ // Access environment configuration
167
+ const config = ENVIRONMENT_CONFIGS['production'];
168
+ console.log(config.platformHost); // https://api.familysearch.org
169
+ ```
170
+
171
+ ## Custom Logging
172
+
173
+ Provide a custom logger for debugging.
174
+
175
+ ```typescript
176
+ const sdk = createFamilySearchSDK({
177
+ accessToken: 'token',
178
+ logger: {
179
+ log: (msg, ...args) => console.log(`[FS SDK] ${msg}`, ...args),
180
+ warn: (msg, ...args) => console.warn(`[FS SDK] ${msg}`, ...args),
181
+ error: (msg, ...args) => console.error(`[FS SDK] ${msg}`, ...args),
182
+ }
183
+ });
184
+ ```
185
+
186
+ ## API Reference
187
+
188
+ ### Core SDK
189
+
190
+ - `FamilySearchSDK` - Main SDK class
191
+ - `createFamilySearchSDK(config)` - Create a new SDK instance
192
+ - `initFamilySearchSDK(config)` - Initialize/get singleton instance
193
+ - `getFamilySearchSDK()` - Get singleton instance
194
+
195
+ ### Authentication (`/auth`)
196
+
197
+ - `generateOAuthState()` - Generate CSRF state
198
+ - `buildAuthorizationUrl(config, state)` - Build OAuth URL
199
+ - `exchangeCodeForToken(code, config)` - Exchange code for tokens
200
+ - `refreshAccessToken(refreshToken, config)` - Refresh access token
201
+ - `validateAccessToken(token, environment)` - Validate token
202
+
203
+ ### Places (`/places`)
204
+
205
+ - `searchPlaces(sdk, query, options)` - Search for places
206
+ - `getPlaceById(sdk, id)` - Get place by ID
207
+ - `getPlaceChildren(sdk, id, options)` - Get child places
208
+ - `getPlaceDetails(sdk, id)` - Get detailed place info
209
+
210
+ ### Tree (`/tree`)
211
+
212
+ - `fetchPedigree(sdk, personId, options)` - Fetch ancestry data
213
+ - `getCurrentUser(sdk)` - Get current user info
214
+ - `getPersonWithDetails(sdk, personId)` - Get person details
215
+ - `fetchMultiplePersons(sdk, personIds)` - Batch fetch persons
216
+
217
+ ### Utils (`/utils`)
218
+
219
+ - `convertToGedcom(pedigreeData, options)` - Convert to GEDCOM
220
+
221
+ ## License
222
+
223
+ MIT License - see [LICENSE](./LICENSE) file for details.
224
+
225
+ ## Contributing
226
+
227
+ Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
@@ -0,0 +1,313 @@
1
+ 'use strict';
2
+
3
+ // src/auth/oauth.ts
4
+ var OAUTH_ENDPOINTS = {
5
+ production: {
6
+ authorization: "https://ident.familysearch.org/cis-web/oauth2/v3/authorization",
7
+ token: "https://ident.familysearch.org/cis-web/oauth2/v3/token",
8
+ currentUser: "https://api.familysearch.org/platform/users/current"
9
+ },
10
+ beta: {
11
+ authorization: "https://identbeta.familysearch.org/cis-web/oauth2/v3/authorization",
12
+ token: "https://identbeta.familysearch.org/cis-web/oauth2/v3/token",
13
+ currentUser: "https://apibeta.familysearch.org/platform/users/current"
14
+ },
15
+ integration: {
16
+ authorization: "https://identint.familysearch.org/cis-web/oauth2/v3/authorization",
17
+ token: "https://identint.familysearch.org/cis-web/oauth2/v3/token",
18
+ currentUser: "https://api-integ.familysearch.org/platform/users/current"
19
+ }
20
+ };
21
+ function getOAuthEndpoints(environment = "integration") {
22
+ return OAUTH_ENDPOINTS[environment];
23
+ }
24
+ function generateOAuthState() {
25
+ const array = new Uint8Array(32);
26
+ crypto.getRandomValues(array);
27
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
28
+ ""
29
+ );
30
+ }
31
+ function buildAuthorizationUrl(config, state, options = {}) {
32
+ const endpoints = getOAuthEndpoints(config.environment);
33
+ const url = new URL(endpoints.authorization);
34
+ url.searchParams.set("response_type", "code");
35
+ url.searchParams.set("client_id", config.clientId);
36
+ url.searchParams.set("redirect_uri", config.redirectUri);
37
+ url.searchParams.set("state", state);
38
+ if (options.scopes && options.scopes.length > 0) {
39
+ url.searchParams.set("scope", options.scopes.join(" "));
40
+ }
41
+ if (options.prompt) {
42
+ url.searchParams.set("prompt", options.prompt);
43
+ }
44
+ return url.toString();
45
+ }
46
+ async function exchangeCodeForToken(code, config) {
47
+ const endpoints = getOAuthEndpoints(config.environment);
48
+ const response = await fetch(endpoints.token, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/x-www-form-urlencoded",
52
+ Accept: "application/json"
53
+ },
54
+ body: new URLSearchParams({
55
+ grant_type: "authorization_code",
56
+ code,
57
+ client_id: config.clientId,
58
+ redirect_uri: config.redirectUri
59
+ })
60
+ });
61
+ if (!response.ok) {
62
+ const error = await response.text();
63
+ throw new Error(`Failed to exchange code for token: ${error}`);
64
+ }
65
+ return response.json();
66
+ }
67
+ async function refreshAccessToken(refreshToken, config) {
68
+ const endpoints = getOAuthEndpoints(config.environment);
69
+ const response = await fetch(endpoints.token, {
70
+ method: "POST",
71
+ headers: {
72
+ "Content-Type": "application/x-www-form-urlencoded",
73
+ Accept: "application/json"
74
+ },
75
+ body: new URLSearchParams({
76
+ grant_type: "refresh_token",
77
+ refresh_token: refreshToken,
78
+ client_id: config.clientId
79
+ })
80
+ });
81
+ if (!response.ok) {
82
+ const error = await response.text();
83
+ throw new Error(`Failed to refresh token: ${error}`);
84
+ }
85
+ return response.json();
86
+ }
87
+ async function validateAccessToken(accessToken, environment = "integration") {
88
+ const endpoints = getOAuthEndpoints(environment);
89
+ try {
90
+ const response = await fetch(endpoints.currentUser, {
91
+ headers: {
92
+ Authorization: `Bearer ${accessToken}`,
93
+ Accept: "application/json"
94
+ }
95
+ });
96
+ return response.ok;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ async function getUserInfo(accessToken, environment = "integration") {
102
+ const endpoints = getOAuthEndpoints(environment);
103
+ try {
104
+ const response = await fetch(endpoints.currentUser, {
105
+ headers: {
106
+ Authorization: `Bearer ${accessToken}`,
107
+ Accept: "application/json"
108
+ }
109
+ });
110
+ if (!response.ok) {
111
+ return null;
112
+ }
113
+ const data = await response.json();
114
+ const fsUser = data.users?.[0];
115
+ if (!fsUser || !fsUser.id) {
116
+ return null;
117
+ }
118
+ return {
119
+ sub: fsUser.id,
120
+ name: fsUser.contactName || fsUser.displayName,
121
+ given_name: fsUser.givenName,
122
+ family_name: fsUser.familyName,
123
+ email: fsUser.email,
124
+ email_verified: fsUser.email ? true : false
125
+ };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ var OAUTH_STORAGE_KEYS = {
131
+ state: "fs_oauth_state",
132
+ linkMode: "fs_oauth_link_mode",
133
+ lang: "fs_oauth_lang",
134
+ parentUid: "fs_oauth_parent_uid"
135
+ };
136
+ function storeOAuthState(state, options = {}) {
137
+ if (typeof localStorage === "undefined") {
138
+ throw new Error(
139
+ "localStorage is not available. For server-side usage, implement custom state storage."
140
+ );
141
+ }
142
+ localStorage.setItem(OAUTH_STORAGE_KEYS.state, state);
143
+ if (options.isLinkMode) {
144
+ localStorage.setItem(OAUTH_STORAGE_KEYS.linkMode, "true");
145
+ } else {
146
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.linkMode);
147
+ }
148
+ if (options.lang) {
149
+ localStorage.setItem(OAUTH_STORAGE_KEYS.lang, options.lang);
150
+ } else {
151
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.lang);
152
+ }
153
+ if (options.parentUid) {
154
+ localStorage.setItem(OAUTH_STORAGE_KEYS.parentUid, options.parentUid);
155
+ } else {
156
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.parentUid);
157
+ }
158
+ }
159
+ function validateOAuthState(state) {
160
+ if (typeof localStorage === "undefined") {
161
+ return { valid: false, isLinkMode: false };
162
+ }
163
+ const storedState = localStorage.getItem(OAUTH_STORAGE_KEYS.state);
164
+ const isLinkMode = localStorage.getItem(OAUTH_STORAGE_KEYS.linkMode) === "true";
165
+ const lang = localStorage.getItem(OAUTH_STORAGE_KEYS.lang) || void 0;
166
+ const parentUid = localStorage.getItem(OAUTH_STORAGE_KEYS.parentUid) || void 0;
167
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.state);
168
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.linkMode);
169
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.lang);
170
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.parentUid);
171
+ return {
172
+ valid: storedState === state,
173
+ isLinkMode,
174
+ lang,
175
+ parentUid
176
+ };
177
+ }
178
+ function openOAuthPopup(authUrl, options = {}) {
179
+ if (typeof window === "undefined") {
180
+ throw new Error("window is not available");
181
+ }
182
+ const width = options.width || 500;
183
+ const height = options.height || 600;
184
+ const windowName = options.windowName || "FamilySearch Login";
185
+ const left = window.screenX + (window.outerWidth - width) / 2;
186
+ const top = window.screenY + (window.outerHeight - height) / 2;
187
+ const popup = window.open(
188
+ authUrl,
189
+ windowName,
190
+ `width=${width},height=${height},left=${left},top=${top},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes`
191
+ );
192
+ if (popup) {
193
+ popup.focus();
194
+ }
195
+ return popup;
196
+ }
197
+ function parseCallbackParams(url = typeof window !== "undefined" ? window.location.href : "") {
198
+ const urlObj = new URL(url);
199
+ const params = urlObj.searchParams;
200
+ return {
201
+ code: params.get("code") || void 0,
202
+ state: params.get("state") || void 0,
203
+ error: params.get("error") || void 0,
204
+ error_description: params.get("error_description") || void 0
205
+ };
206
+ }
207
+ function getTokenStorageKey(userId, type) {
208
+ return `fs_token_${userId}_${type}`;
209
+ }
210
+ function storeTokens(userId, tokens) {
211
+ if (typeof sessionStorage === "undefined" || typeof localStorage === "undefined") {
212
+ throw new Error("Storage APIs are not available");
213
+ }
214
+ sessionStorage.setItem(
215
+ getTokenStorageKey(userId, "access"),
216
+ tokens.accessToken
217
+ );
218
+ if (tokens.expiresAt) {
219
+ sessionStorage.setItem(
220
+ getTokenStorageKey(userId, "expires"),
221
+ tokens.expiresAt.toString()
222
+ );
223
+ }
224
+ if (tokens.refreshToken) {
225
+ localStorage.setItem(
226
+ getTokenStorageKey(userId, "refresh"),
227
+ tokens.refreshToken
228
+ );
229
+ }
230
+ if (tokens.environment) {
231
+ localStorage.setItem(
232
+ getTokenStorageKey(userId, "environment"),
233
+ tokens.environment
234
+ );
235
+ }
236
+ }
237
+ function getStoredAccessToken(userId) {
238
+ if (typeof sessionStorage === "undefined") {
239
+ return null;
240
+ }
241
+ const token = sessionStorage.getItem(getTokenStorageKey(userId, "access"));
242
+ const expiresAt = sessionStorage.getItem(
243
+ getTokenStorageKey(userId, "expires")
244
+ );
245
+ if (!token) {
246
+ return null;
247
+ }
248
+ const EXPIRATION_BUFFER = 5 * 60 * 1e3;
249
+ if (expiresAt && Date.now() > parseInt(expiresAt) - EXPIRATION_BUFFER) {
250
+ return null;
251
+ }
252
+ return token;
253
+ }
254
+ function getStoredRefreshToken(userId) {
255
+ if (typeof localStorage === "undefined") {
256
+ return null;
257
+ }
258
+ return localStorage.getItem(getTokenStorageKey(userId, "refresh"));
259
+ }
260
+ function clearStoredTokens(userId) {
261
+ if (typeof sessionStorage !== "undefined") {
262
+ sessionStorage.removeItem(getTokenStorageKey(userId, "access"));
263
+ sessionStorage.removeItem(getTokenStorageKey(userId, "expires"));
264
+ }
265
+ if (typeof localStorage !== "undefined") {
266
+ localStorage.removeItem(getTokenStorageKey(userId, "refresh"));
267
+ localStorage.removeItem(getTokenStorageKey(userId, "environment"));
268
+ }
269
+ }
270
+ function clearAllTokens() {
271
+ if (typeof sessionStorage === "undefined" || typeof localStorage === "undefined") {
272
+ return;
273
+ }
274
+ const keysToRemove = [];
275
+ for (let i = 0; i < sessionStorage.length; i++) {
276
+ const key = sessionStorage.key(i);
277
+ if (key && key.startsWith("fs_token_")) {
278
+ keysToRemove.push(key);
279
+ }
280
+ }
281
+ for (let i = 0; i < localStorage.length; i++) {
282
+ const key = localStorage.key(i);
283
+ if (key && key.startsWith("fs_token_")) {
284
+ keysToRemove.push(key);
285
+ }
286
+ }
287
+ keysToRemove.forEach((key) => {
288
+ sessionStorage.removeItem(key);
289
+ localStorage.removeItem(key);
290
+ });
291
+ }
292
+
293
+ exports.OAUTH_ENDPOINTS = OAUTH_ENDPOINTS;
294
+ exports.OAUTH_STORAGE_KEYS = OAUTH_STORAGE_KEYS;
295
+ exports.buildAuthorizationUrl = buildAuthorizationUrl;
296
+ exports.clearAllTokens = clearAllTokens;
297
+ exports.clearStoredTokens = clearStoredTokens;
298
+ exports.exchangeCodeForToken = exchangeCodeForToken;
299
+ exports.generateOAuthState = generateOAuthState;
300
+ exports.getOAuthEndpoints = getOAuthEndpoints;
301
+ exports.getStoredAccessToken = getStoredAccessToken;
302
+ exports.getStoredRefreshToken = getStoredRefreshToken;
303
+ exports.getTokenStorageKey = getTokenStorageKey;
304
+ exports.getUserInfo = getUserInfo;
305
+ exports.openOAuthPopup = openOAuthPopup;
306
+ exports.parseCallbackParams = parseCallbackParams;
307
+ exports.refreshAccessToken = refreshAccessToken;
308
+ exports.storeOAuthState = storeOAuthState;
309
+ exports.storeTokens = storeTokens;
310
+ exports.validateAccessToken = validateAccessToken;
311
+ exports.validateOAuthState = validateOAuthState;
312
+ //# sourceMappingURL=index.cjs.map
313
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * FamilySearch OAuth Authentication Module\n *\n * Provides OAuth 2.0 authentication utilities for FamilySearch API v3.\n * This module is designed to be framework-agnostic and can be used\n * in any JavaScript/TypeScript environment.\n */\n\nimport type {\n\tFamilySearchEnvironment,\n\tOAuthConfig,\n\tOAuthEndpoints,\n\tOAuthStateValidation,\n\tOAuthTokenResponse,\n} from \"../types\";\n\n// OAuth endpoints by environment\nconst OAUTH_ENDPOINTS: Record<FamilySearchEnvironment, OAuthEndpoints> = {\n\tproduction: {\n\t\tauthorization:\n\t\t\t\"https://ident.familysearch.org/cis-web/oauth2/v3/authorization\",\n\t\ttoken: \"https://ident.familysearch.org/cis-web/oauth2/v3/token\",\n\t\tcurrentUser: \"https://api.familysearch.org/platform/users/current\",\n\t},\n\tbeta: {\n\t\tauthorization:\n\t\t\t\"https://identbeta.familysearch.org/cis-web/oauth2/v3/authorization\",\n\t\ttoken: \"https://identbeta.familysearch.org/cis-web/oauth2/v3/token\",\n\t\tcurrentUser: \"https://apibeta.familysearch.org/platform/users/current\",\n\t},\n\tintegration: {\n\t\tauthorization:\n\t\t\t\"https://identint.familysearch.org/cis-web/oauth2/v3/authorization\",\n\t\ttoken: \"https://identint.familysearch.org/cis-web/oauth2/v3/token\",\n\t\tcurrentUser:\n\t\t\t\"https://api-integ.familysearch.org/platform/users/current\",\n\t},\n};\n\n/**\n * Get OAuth endpoints for a specific environment\n */\nexport function getOAuthEndpoints(\n\tenvironment: FamilySearchEnvironment = \"integration\"\n): OAuthEndpoints {\n\treturn OAUTH_ENDPOINTS[environment];\n}\n\n/**\n * Generate a cryptographically secure random state for CSRF protection\n */\nexport function generateOAuthState(): string {\n\tconst array = new Uint8Array(32);\n\tcrypto.getRandomValues(array);\n\treturn Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n\t\t\"\"\n\t);\n}\n\n/**\n * Build the authorization URL for OAuth flow\n */\nexport function buildAuthorizationUrl(\n\tconfig: OAuthConfig,\n\tstate: string,\n\toptions: {\n\t\tscopes?: string[];\n\t\tprompt?: string;\n\t} = {}\n): string {\n\tconst endpoints = getOAuthEndpoints(config.environment);\n\tconst url = new URL(endpoints.authorization);\n\n\turl.searchParams.set(\"response_type\", \"code\");\n\turl.searchParams.set(\"client_id\", config.clientId);\n\turl.searchParams.set(\"redirect_uri\", config.redirectUri);\n\turl.searchParams.set(\"state\", state);\n\n\tif (options.scopes && options.scopes.length > 0) {\n\t\turl.searchParams.set(\"scope\", options.scopes.join(\" \"));\n\t}\n\n\tif (options.prompt) {\n\t\turl.searchParams.set(\"prompt\", options.prompt);\n\t}\n\n\treturn url.toString();\n}\n\n/**\n * Exchange authorization code for access token\n */\nexport async function exchangeCodeForToken(\n\tcode: string,\n\tconfig: OAuthConfig\n): Promise<OAuthTokenResponse> {\n\tconst endpoints = getOAuthEndpoints(config.environment);\n\n\tconst response = await fetch(endpoints.token, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tcode: code,\n\t\t\tclient_id: config.clientId,\n\t\t\tredirect_uri: config.redirectUri,\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Failed to exchange code for token: ${error}`);\n\t}\n\n\treturn response.json();\n}\n\n/**\n * Refresh an access token using a refresh token\n */\nexport async function refreshAccessToken(\n\trefreshToken: string,\n\tconfig: OAuthConfig\n): Promise<OAuthTokenResponse> {\n\tconst endpoints = getOAuthEndpoints(config.environment);\n\n\tconst response = await fetch(endpoints.token, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tgrant_type: \"refresh_token\",\n\t\t\trefresh_token: refreshToken,\n\t\t\tclient_id: config.clientId,\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Failed to refresh token: ${error}`);\n\t}\n\n\treturn response.json();\n}\n\n/**\n * Validate an access token by making a test API call\n */\nexport async function validateAccessToken(\n\taccessToken: string,\n\tenvironment: FamilySearchEnvironment = \"integration\"\n): Promise<boolean> {\n\tconst endpoints = getOAuthEndpoints(environment);\n\n\ttry {\n\t\tconst response = await fetch(endpoints.currentUser, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\t\tAccept: \"application/json\",\n\t\t\t},\n\t\t});\n\n\t\treturn response.ok;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Get user info from access token\n */\nexport async function getUserInfo(\n\taccessToken: string,\n\tenvironment: FamilySearchEnvironment = \"integration\"\n): Promise<{\n\tsub: string;\n\tname?: string;\n\tgiven_name?: string;\n\tfamily_name?: string;\n\temail?: string;\n\temail_verified?: boolean;\n} | null> {\n\tconst endpoints = getOAuthEndpoints(environment);\n\n\ttry {\n\t\tconst response = await fetch(endpoints.currentUser, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\t\tAccept: \"application/json\",\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst data = await response.json();\n\t\tconst fsUser = data.users?.[0];\n\n\t\tif (!fsUser || !fsUser.id) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn {\n\t\t\tsub: fsUser.id,\n\t\t\tname: fsUser.contactName || fsUser.displayName,\n\t\t\tgiven_name: fsUser.givenName,\n\t\t\tfamily_name: fsUser.familyName,\n\t\t\temail: fsUser.email,\n\t\t\temail_verified: fsUser.email ? true : false,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n// ====================================\n// Browser-specific OAuth Helpers\n// ====================================\n\n/**\n * Storage keys for OAuth state management\n */\nexport const OAUTH_STORAGE_KEYS = {\n\tstate: \"fs_oauth_state\",\n\tlinkMode: \"fs_oauth_link_mode\",\n\tlang: \"fs_oauth_lang\",\n\tparentUid: \"fs_oauth_parent_uid\",\n} as const;\n\n/**\n * Store OAuth state in localStorage for popup flow\n * Uses localStorage instead of sessionStorage because popup windows\n * don't share sessionStorage with the parent window\n */\nexport function storeOAuthState(\n\tstate: string,\n\toptions: {\n\t\tisLinkMode?: boolean;\n\t\tlang?: string;\n\t\tparentUid?: string;\n\t} = {}\n): void {\n\tif (typeof localStorage === \"undefined\") {\n\t\t// In server-side or non-browser environments, state storage is not available\n\t\t// Callers should handle this by implementing their own state storage mechanism\n\t\tthrow new Error(\n\t\t\t\"localStorage is not available. For server-side usage, implement custom state storage.\"\n\t\t);\n\t}\n\n\tlocalStorage.setItem(OAUTH_STORAGE_KEYS.state, state);\n\n\tif (options.isLinkMode) {\n\t\tlocalStorage.setItem(OAUTH_STORAGE_KEYS.linkMode, \"true\");\n\t} else {\n\t\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.linkMode);\n\t}\n\n\tif (options.lang) {\n\t\tlocalStorage.setItem(OAUTH_STORAGE_KEYS.lang, options.lang);\n\t} else {\n\t\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.lang);\n\t}\n\n\tif (options.parentUid) {\n\t\tlocalStorage.setItem(OAUTH_STORAGE_KEYS.parentUid, options.parentUid);\n\t} else {\n\t\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.parentUid);\n\t}\n}\n\n/**\n * Validate OAuth state from callback and extract metadata\n * Returns invalid state if localStorage is not available (SSR/Node.js environments)\n */\nexport function validateOAuthState(state: string): OAuthStateValidation {\n\tif (typeof localStorage === \"undefined\") {\n\t\t// In server-side environments, return invalid state\n\t\t// Callers should implement their own state validation for SSR\n\t\treturn { valid: false, isLinkMode: false };\n\t}\n\n\tconst storedState = localStorage.getItem(OAUTH_STORAGE_KEYS.state);\n\tconst isLinkMode =\n\t\tlocalStorage.getItem(OAUTH_STORAGE_KEYS.linkMode) === \"true\";\n\tconst lang = localStorage.getItem(OAUTH_STORAGE_KEYS.lang) || undefined;\n\tconst parentUid =\n\t\tlocalStorage.getItem(OAUTH_STORAGE_KEYS.parentUid) || undefined;\n\n\t// Clean up stored values\n\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.state);\n\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.linkMode);\n\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.lang);\n\tlocalStorage.removeItem(OAUTH_STORAGE_KEYS.parentUid);\n\n\treturn {\n\t\tvalid: storedState === state,\n\t\tisLinkMode,\n\t\tlang,\n\t\tparentUid,\n\t};\n}\n\n/**\n * Open OAuth authorization in a popup window\n */\nexport function openOAuthPopup(\n\tauthUrl: string,\n\toptions: {\n\t\twidth?: number;\n\t\theight?: number;\n\t\twindowName?: string;\n\t} = {}\n): Window | null {\n\tif (typeof window === \"undefined\") {\n\t\tthrow new Error(\"window is not available\");\n\t}\n\n\tconst width = options.width || 500;\n\tconst height = options.height || 600;\n\tconst windowName = options.windowName || \"FamilySearch Login\";\n\n\tconst left = window.screenX + (window.outerWidth - width) / 2;\n\tconst top = window.screenY + (window.outerHeight - height) / 2;\n\n\tconst popup = window.open(\n\t\tauthUrl,\n\t\twindowName,\n\t\t`width=${width},height=${height},left=${left},top=${top},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes`\n\t);\n\n\tif (popup) {\n\t\tpopup.focus();\n\t}\n\n\treturn popup;\n}\n\n/**\n * Parse OAuth callback parameters from URL\n */\nexport function parseCallbackParams(\n\turl: string = typeof window !== \"undefined\" ? window.location.href : \"\"\n): {\n\tcode?: string;\n\tstate?: string;\n\terror?: string;\n\terror_description?: string;\n} {\n\tconst urlObj = new URL(url);\n\tconst params = urlObj.searchParams;\n\n\treturn {\n\t\tcode: params.get(\"code\") || undefined,\n\t\tstate: params.get(\"state\") || undefined,\n\t\terror: params.get(\"error\") || undefined,\n\t\terror_description: params.get(\"error_description\") || undefined,\n\t};\n}\n\n// ====================================\n// Token Storage Helpers\n// ====================================\n\n/**\n * Generate a storage key scoped to a user ID\n */\nexport function getTokenStorageKey(\n\tuserId: string,\n\ttype: \"access\" | \"expires\" | \"refresh\" | \"environment\"\n): string {\n\treturn `fs_token_${userId}_${type}`;\n}\n\n/**\n * Store access token with expiration\n * Per FamilySearch compatibility requirements:\n * - Access tokens stored in sessionStorage (cleared on browser close)\n * - Refresh tokens stored in localStorage (for re-authentication)\n */\nexport function storeTokens(\n\tuserId: string,\n\ttokens: {\n\t\taccessToken: string;\n\t\texpiresAt?: number;\n\t\trefreshToken?: string;\n\t\tenvironment?: string;\n\t}\n): void {\n\tif (typeof sessionStorage === \"undefined\" || typeof localStorage === \"undefined\") {\n\t\tthrow new Error(\"Storage APIs are not available\");\n\t}\n\n\t// Access tokens in sessionStorage (temporary)\n\tsessionStorage.setItem(\n\t\tgetTokenStorageKey(userId, \"access\"),\n\t\ttokens.accessToken\n\t);\n\n\tif (tokens.expiresAt) {\n\t\tsessionStorage.setItem(\n\t\t\tgetTokenStorageKey(userId, \"expires\"),\n\t\t\ttokens.expiresAt.toString()\n\t\t);\n\t}\n\n\t// Refresh tokens in localStorage (persistent)\n\tif (tokens.refreshToken) {\n\t\tlocalStorage.setItem(\n\t\t\tgetTokenStorageKey(userId, \"refresh\"),\n\t\t\ttokens.refreshToken\n\t\t);\n\t}\n\n\tif (tokens.environment) {\n\t\tlocalStorage.setItem(\n\t\t\tgetTokenStorageKey(userId, \"environment\"),\n\t\t\ttokens.environment\n\t\t);\n\t}\n}\n\n/**\n * Get stored access token\n */\nexport function getStoredAccessToken(userId: string): string | null {\n\tif (typeof sessionStorage === \"undefined\") {\n\t\treturn null;\n\t}\n\n\tconst token = sessionStorage.getItem(getTokenStorageKey(userId, \"access\"));\n\tconst expiresAt = sessionStorage.getItem(\n\t\tgetTokenStorageKey(userId, \"expires\")\n\t);\n\n\tif (!token) {\n\t\treturn null;\n\t}\n\n\t// Check expiration with 5-minute buffer\n\tconst EXPIRATION_BUFFER = 5 * 60 * 1000;\n\tif (expiresAt && Date.now() > parseInt(expiresAt) - EXPIRATION_BUFFER) {\n\t\treturn null;\n\t}\n\n\treturn token;\n}\n\n/**\n * Get stored refresh token\n */\nexport function getStoredRefreshToken(userId: string): string | null {\n\tif (typeof localStorage === \"undefined\") {\n\t\treturn null;\n\t}\n\n\treturn localStorage.getItem(getTokenStorageKey(userId, \"refresh\"));\n}\n\n/**\n * Clear all stored tokens for a user\n */\nexport function clearStoredTokens(userId: string): void {\n\tif (typeof sessionStorage !== \"undefined\") {\n\t\tsessionStorage.removeItem(getTokenStorageKey(userId, \"access\"));\n\t\tsessionStorage.removeItem(getTokenStorageKey(userId, \"expires\"));\n\t}\n\n\tif (typeof localStorage !== \"undefined\") {\n\t\tlocalStorage.removeItem(getTokenStorageKey(userId, \"refresh\"));\n\t\tlocalStorage.removeItem(getTokenStorageKey(userId, \"environment\"));\n\t}\n}\n\n/**\n * Clear all FamilySearch tokens from storage\n */\nexport function clearAllTokens(): void {\n\tif (typeof sessionStorage === \"undefined\" || typeof localStorage === \"undefined\") {\n\t\treturn;\n\t}\n\n\tconst keysToRemove: string[] = [];\n\n\t// Find all fs_token_* keys in sessionStorage\n\tfor (let i = 0; i < sessionStorage.length; i++) {\n\t\tconst key = sessionStorage.key(i);\n\t\tif (key && key.startsWith(\"fs_token_\")) {\n\t\t\tkeysToRemove.push(key);\n\t\t}\n\t}\n\n\t// Find all fs_token_* keys in localStorage\n\tfor (let i = 0; i < localStorage.length; i++) {\n\t\tconst key = localStorage.key(i);\n\t\tif (key && key.startsWith(\"fs_token_\")) {\n\t\t\tkeysToRemove.push(key);\n\t\t}\n\t}\n\n\t// Remove from both storages\n\tkeysToRemove.forEach((key) => {\n\t\tsessionStorage.removeItem(key);\n\t\tlocalStorage.removeItem(key);\n\t});\n}\n\nexport { OAUTH_ENDPOINTS };\n"]}