@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 +21 -0
- package/README.md +227 -0
- package/dist/auth/index.cjs +313 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +124 -0
- package/dist/auth/index.d.ts +124 -0
- package/dist/auth/index.js +293 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/client-DIpYSHtx.d.ts +162 -0
- package/dist/client-ohjqX4t5.d.cts +162 -0
- package/dist/index-D6H-lvis.d.cts +484 -0
- package/dist/index-D6H-lvis.d.ts +484 -0
- package/dist/index.cjs +1689 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1653 -0
- package/dist/index.js.map +1 -0
- package/dist/places/index.cjs +94 -0
- package/dist/places/index.cjs.map +1 -0
- package/dist/places/index.d.cts +69 -0
- package/dist/places/index.d.ts +69 -0
- package/dist/places/index.js +89 -0
- package/dist/places/index.js.map +1 -0
- package/dist/tree/index.cjs +191 -0
- package/dist/tree/index.cjs.map +1 -0
- package/dist/tree/index.d.cts +47 -0
- package/dist/tree/index.d.ts +47 -0
- package/dist/tree/index.js +186 -0
- package/dist/tree/index.js.map +1 -0
- package/dist/utils/index.cjs +663 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +46 -0
- package/dist/utils/index.d.ts +46 -0
- package/dist/utils/index.js +660 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +78 -0
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"]}
|