@feelflow/ffid-sdk 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,11 +4,150 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  // src/constants.ts
5
5
  var DEFAULT_API_BASE_URL = "https://id.feelflow.co.jp";
6
6
 
7
+ // src/auth/token-store.ts
8
+ var STORAGE_KEY = "ffid_tokens";
9
+ var EXPIRY_BUFFER_SECONDS = 30;
10
+ var EXPIRY_BUFFER_MS = EXPIRY_BUFFER_SECONDS * 1e3;
11
+ function isLocalStorageAvailable() {
12
+ try {
13
+ if (typeof window === "undefined") return false;
14
+ const storage = window.localStorage;
15
+ if (!storage || typeof storage.setItem !== "function") return false;
16
+ const testKey = "__ffid_storage_test__";
17
+ storage.setItem(testKey, "1");
18
+ storage.removeItem(testKey);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ function createLocalStorageStore() {
25
+ const storage = window.localStorage;
26
+ return {
27
+ getTokens() {
28
+ try {
29
+ const raw = storage.getItem(STORAGE_KEY);
30
+ if (!raw) return null;
31
+ const parsed = JSON.parse(raw);
32
+ if (!isTokenData(parsed)) return null;
33
+ return parsed;
34
+ } catch {
35
+ return null;
36
+ }
37
+ },
38
+ setTokens(tokens) {
39
+ try {
40
+ storage.setItem(STORAGE_KEY, JSON.stringify(tokens));
41
+ } catch {
42
+ }
43
+ },
44
+ clearTokens() {
45
+ try {
46
+ storage.removeItem(STORAGE_KEY);
47
+ } catch {
48
+ }
49
+ },
50
+ isAccessTokenExpired() {
51
+ const tokens = this.getTokens();
52
+ if (!tokens) return true;
53
+ return Date.now() >= tokens.expiresAt - EXPIRY_BUFFER_MS;
54
+ }
55
+ };
56
+ }
57
+ function createMemoryStore() {
58
+ let stored = null;
59
+ return {
60
+ getTokens() {
61
+ return stored;
62
+ },
63
+ setTokens(tokens) {
64
+ stored = { ...tokens };
65
+ },
66
+ clearTokens() {
67
+ stored = null;
68
+ },
69
+ isAccessTokenExpired() {
70
+ if (!stored) return true;
71
+ return Date.now() >= stored.expiresAt - EXPIRY_BUFFER_MS;
72
+ }
73
+ };
74
+ }
75
+ function isTokenData(value) {
76
+ if (typeof value !== "object" || value === null) return false;
77
+ const obj = value;
78
+ return typeof obj.accessToken === "string" && typeof obj.refreshToken === "string" && typeof obj.expiresAt === "number";
79
+ }
80
+ function createTokenStore(storageType) {
81
+ if (storageType === "memory") {
82
+ return createMemoryStore();
83
+ }
84
+ if (typeof window !== "undefined" && isLocalStorageAvailable()) {
85
+ return createLocalStorageStore();
86
+ }
87
+ return createMemoryStore();
88
+ }
89
+
90
+ // src/auth/pkce.ts
91
+ var VERIFIER_STORAGE_KEY = "ffid_code_verifier";
92
+ var CODE_VERIFIER_MIN_LENGTH = 43;
93
+ var UNRESERVED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
94
+ function generateCodeVerifier() {
95
+ const length = CODE_VERIFIER_MIN_LENGTH;
96
+ const randomValues = new Uint8Array(length);
97
+ crypto.getRandomValues(randomValues);
98
+ let verifier = "";
99
+ for (let i = 0; i < length; i++) {
100
+ verifier += UNRESERVED_CHARS[randomValues[i] % UNRESERVED_CHARS.length];
101
+ }
102
+ return verifier;
103
+ }
104
+ async function generateCodeChallenge(verifier) {
105
+ const encoder = new TextEncoder();
106
+ const data = encoder.encode(verifier);
107
+ const digest = await crypto.subtle.digest("SHA-256", data);
108
+ return base64UrlEncode(digest);
109
+ }
110
+ function storeCodeVerifier(verifier) {
111
+ try {
112
+ if (typeof window === "undefined") return;
113
+ window.sessionStorage.setItem(VERIFIER_STORAGE_KEY, verifier);
114
+ } catch {
115
+ }
116
+ }
117
+ function retrieveCodeVerifier() {
118
+ try {
119
+ if (typeof window === "undefined") return null;
120
+ const verifier = window.sessionStorage.getItem(VERIFIER_STORAGE_KEY);
121
+ if (verifier) {
122
+ window.sessionStorage.removeItem(VERIFIER_STORAGE_KEY);
123
+ }
124
+ return verifier;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ function base64UrlEncode(buffer) {
130
+ const bytes = new Uint8Array(buffer);
131
+ let binary = "";
132
+ for (let i = 0; i < bytes.length; i++) {
133
+ binary += String.fromCharCode(bytes[i]);
134
+ }
135
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
136
+ }
137
+
7
138
  // src/client/ffid-client.ts
8
139
  var NO_CONTENT_STATUS = 204;
9
140
  var SESSION_ENDPOINT = "/api/v1/auth/session";
10
141
  var LOGOUT_ENDPOINT = "/api/v1/auth/signout";
142
+ var OAUTH_TOKEN_ENDPOINT = "/api/v1/oauth/token";
143
+ var OAUTH_USERINFO_ENDPOINT = "/api/v1/oauth/userinfo";
144
+ var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
145
+ var OAUTH_REVOKE_ENDPOINT = "/api/v1/oauth/revoke";
11
146
  var SDK_LOG_PREFIX = "[FFID SDK]";
147
+ var MS_PER_SECOND = 1e3;
148
+ var UNAUTHORIZED_STATUS = 401;
149
+ var STATE_RANDOM_BYTES = 16;
150
+ var HEX_BASE = 16;
12
151
  var noopLogger = {
13
152
  debug: () => {
14
153
  },
@@ -30,28 +169,30 @@ var FFID_ERROR_CODES = {
30
169
  /** Server returned non-JSON response (e.g., HTML error page) */
31
170
  PARSE_ERROR: "PARSE_ERROR",
32
171
  /** Server returned error without structured error body */
33
- UNKNOWN_ERROR: "UNKNOWN_ERROR"
172
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
173
+ /** Token exchange failed */
174
+ TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
175
+ /** Token refresh failed */
176
+ TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
177
+ /** No tokens available */
178
+ NO_TOKENS: "NO_TOKENS"
34
179
  };
35
180
  function createFFIDClient(config) {
36
181
  if (!config.serviceCode || !config.serviceCode.trim()) {
37
182
  throw new Error("FFID Client: serviceCode \u304C\u672A\u8A2D\u5B9A\u3067\u3059");
38
183
  }
39
184
  const baseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
185
+ const authMode = config.authMode ?? "cookie";
186
+ const clientId = config.clientId ?? config.serviceCode;
40
187
  const logger = config.logger ?? (config.debug ? consoleLogger : noopLogger);
188
+ const tokenStore = authMode === "token" ? createTokenStore() : createTokenStore("memory");
41
189
  async function fetchWithAuth(endpoint, options = {}) {
42
190
  const url = `${baseUrl}${endpoint}`;
43
191
  logger.debug("Fetching:", url);
192
+ const fetchOptions = buildFetchOptions(options);
44
193
  let response;
45
194
  try {
46
- response = await fetch(url, {
47
- ...options,
48
- credentials: "include",
49
- // Include cookies for authentication
50
- headers: {
51
- "Content-Type": "application/json",
52
- ...options.headers
53
- }
54
- });
195
+ response = await fetch(url, fetchOptions);
55
196
  } catch (error) {
56
197
  logger.error("Network error:", error);
57
198
  return {
@@ -61,6 +202,24 @@ function createFFIDClient(config) {
61
202
  }
62
203
  };
63
204
  }
205
+ if (authMode === "token" && response.status === UNAUTHORIZED_STATUS) {
206
+ const refreshResult = await refreshAccessToken();
207
+ if (!refreshResult.error) {
208
+ logger.debug("Token refreshed, retrying request");
209
+ const retryOptions = buildFetchOptions(options);
210
+ try {
211
+ response = await fetch(url, retryOptions);
212
+ } catch (retryError) {
213
+ logger.error("Network error on retry:", retryError);
214
+ return {
215
+ error: {
216
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
217
+ message: retryError instanceof Error ? retryError.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
218
+ }
219
+ };
220
+ }
221
+ }
222
+ }
64
223
  let raw;
65
224
  try {
66
225
  raw = await response.json();
@@ -92,10 +251,139 @@ function createFFIDClient(config) {
92
251
  }
93
252
  return { data: raw.data };
94
253
  }
254
+ function buildFetchOptions(options) {
255
+ if (authMode === "token") {
256
+ const tokens = tokenStore.getTokens();
257
+ const headers = {
258
+ "Content-Type": "application/json",
259
+ ...options.headers
260
+ };
261
+ if (tokens) {
262
+ headers["Authorization"] = `Bearer ${tokens.accessToken}`;
263
+ }
264
+ return {
265
+ ...options,
266
+ credentials: "omit",
267
+ headers
268
+ };
269
+ }
270
+ return {
271
+ ...options,
272
+ credentials: "include",
273
+ headers: {
274
+ "Content-Type": "application/json",
275
+ ...options.headers
276
+ }
277
+ };
278
+ }
95
279
  async function getSession() {
280
+ if (authMode === "token") {
281
+ return getSessionFromUserinfo();
282
+ }
96
283
  return fetchWithAuth(SESSION_ENDPOINT);
97
284
  }
285
+ async function getSessionFromUserinfo() {
286
+ const tokens = tokenStore.getTokens();
287
+ if (!tokens) {
288
+ return {
289
+ error: {
290
+ code: FFID_ERROR_CODES.NO_TOKENS,
291
+ message: "\u30C8\u30FC\u30AF\u30F3\u304C\u4FDD\u5B58\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
292
+ }
293
+ };
294
+ }
295
+ const url = `${baseUrl}${OAUTH_USERINFO_ENDPOINT}`;
296
+ logger.debug("Fetching userinfo:", url);
297
+ let response;
298
+ try {
299
+ response = await fetch(url, {
300
+ credentials: "omit",
301
+ headers: {
302
+ "Authorization": `Bearer ${tokens.accessToken}`,
303
+ "Content-Type": "application/json"
304
+ }
305
+ });
306
+ } catch (error) {
307
+ logger.error("Network error:", error);
308
+ return {
309
+ error: {
310
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
311
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
312
+ }
313
+ };
314
+ }
315
+ if (response.status === UNAUTHORIZED_STATUS) {
316
+ const refreshResult = await refreshAccessToken();
317
+ if (!refreshResult.error) {
318
+ const retryTokens = tokenStore.getTokens();
319
+ if (retryTokens) {
320
+ try {
321
+ response = await fetch(url, {
322
+ credentials: "omit",
323
+ headers: {
324
+ "Authorization": `Bearer ${retryTokens.accessToken}`,
325
+ "Content-Type": "application/json"
326
+ }
327
+ });
328
+ } catch (retryError) {
329
+ logger.error("Network error on retry:", retryError);
330
+ return {
331
+ error: {
332
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
333
+ message: retryError instanceof Error ? retryError.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
334
+ }
335
+ };
336
+ }
337
+ }
338
+ } else {
339
+ return refreshResult;
340
+ }
341
+ }
342
+ let userinfo;
343
+ try {
344
+ userinfo = await response.json();
345
+ } catch (parseError) {
346
+ logger.error("Parse error:", parseError, "Status:", response.status);
347
+ return {
348
+ error: {
349
+ code: FFID_ERROR_CODES.PARSE_ERROR,
350
+ message: `\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u4E0D\u6B63\u306A\u30EC\u30B9\u30DD\u30F3\u30B9\u3092\u53D7\u4FE1\u3057\u307E\u3057\u305F (status: ${response.status})`
351
+ }
352
+ };
353
+ }
354
+ if (!response.ok) {
355
+ const errorBody = userinfo;
356
+ return {
357
+ error: {
358
+ code: errorBody.code ?? FFID_ERROR_CODES.UNKNOWN_ERROR,
359
+ message: errorBody.message ?? "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
360
+ }
361
+ };
362
+ }
363
+ const user = {
364
+ id: userinfo.sub,
365
+ email: userinfo.email ?? "",
366
+ displayName: userinfo.name ?? null,
367
+ avatarUrl: userinfo.picture ?? null,
368
+ locale: null,
369
+ timezone: null,
370
+ createdAt: ""
371
+ };
372
+ return {
373
+ data: {
374
+ user,
375
+ organizations: [],
376
+ subscriptions: []
377
+ }
378
+ };
379
+ }
98
380
  async function signOut() {
381
+ if (authMode === "token") {
382
+ return signOutToken();
383
+ }
384
+ return signOutCookie();
385
+ }
386
+ async function signOutCookie() {
99
387
  const url = `${baseUrl}${LOGOUT_ENDPOINT}`;
100
388
  logger.debug("Fetching:", url);
101
389
  let response;
@@ -139,17 +427,188 @@ function createFFIDClient(config) {
139
427
  logger.debug("Response:", response.status);
140
428
  return { data: void 0 };
141
429
  }
430
+ async function signOutToken() {
431
+ const tokens = tokenStore.getTokens();
432
+ tokenStore.clearTokens();
433
+ if (!tokens) {
434
+ logger.debug("No tokens to revoke");
435
+ return { data: void 0 };
436
+ }
437
+ const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
438
+ logger.debug("Revoking token:", url);
439
+ try {
440
+ await fetch(url, {
441
+ method: "POST",
442
+ credentials: "omit",
443
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
444
+ body: new URLSearchParams({
445
+ token: tokens.accessToken,
446
+ client_id: clientId
447
+ }).toString()
448
+ });
449
+ } catch (error) {
450
+ logger.warn("Token revocation failed:", error);
451
+ }
452
+ logger.debug("Token sign-out completed");
453
+ return { data: void 0 };
454
+ }
455
+ async function exchangeCodeForTokens(code, codeVerifier) {
456
+ const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
457
+ logger.debug("Exchanging code for tokens:", url);
458
+ const body = {
459
+ grant_type: "authorization_code",
460
+ code,
461
+ client_id: clientId,
462
+ redirect_uri: typeof window !== "undefined" ? window.location.origin + window.location.pathname : ""
463
+ };
464
+ if (codeVerifier) {
465
+ body.code_verifier = codeVerifier;
466
+ }
467
+ let response;
468
+ try {
469
+ response = await fetch(url, {
470
+ method: "POST",
471
+ credentials: "omit",
472
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
473
+ body: new URLSearchParams(body).toString()
474
+ });
475
+ } catch (error) {
476
+ logger.error("Network error during token exchange:", error);
477
+ return {
478
+ error: {
479
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
480
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
481
+ }
482
+ };
483
+ }
484
+ let tokenResponse;
485
+ try {
486
+ tokenResponse = await response.json();
487
+ } catch (parseError) {
488
+ logger.error("Parse error during token exchange:", parseError);
489
+ return {
490
+ error: {
491
+ code: FFID_ERROR_CODES.PARSE_ERROR,
492
+ message: `\u30C8\u30FC\u30AF\u30F3\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F (status: ${response.status})`
493
+ }
494
+ };
495
+ }
496
+ if (!response.ok) {
497
+ const errorBody = tokenResponse;
498
+ return {
499
+ error: {
500
+ code: errorBody.error ?? FFID_ERROR_CODES.TOKEN_EXCHANGE_ERROR,
501
+ message: errorBody.error_description ?? "\u30C8\u30FC\u30AF\u30F3\u4EA4\u63DB\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
502
+ }
503
+ };
504
+ }
505
+ tokenStore.setTokens({
506
+ accessToken: tokenResponse.access_token,
507
+ refreshToken: tokenResponse.refresh_token,
508
+ expiresAt: Date.now() + tokenResponse.expires_in * MS_PER_SECOND
509
+ });
510
+ logger.debug("Token exchange successful");
511
+ return { data: void 0 };
512
+ }
513
+ async function refreshAccessToken() {
514
+ const tokens = tokenStore.getTokens();
515
+ if (!tokens) {
516
+ return {
517
+ error: {
518
+ code: FFID_ERROR_CODES.NO_TOKENS,
519
+ message: "\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u30C8\u30FC\u30AF\u30F3\u304C\u3042\u308A\u307E\u305B\u3093"
520
+ }
521
+ };
522
+ }
523
+ const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
524
+ logger.debug("Refreshing access token:", url);
525
+ let response;
526
+ try {
527
+ response = await fetch(url, {
528
+ method: "POST",
529
+ credentials: "omit",
530
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
531
+ body: new URLSearchParams({
532
+ grant_type: "refresh_token",
533
+ refresh_token: tokens.refreshToken,
534
+ client_id: clientId
535
+ }).toString()
536
+ });
537
+ } catch (error) {
538
+ logger.error("Network error during token refresh:", error);
539
+ return {
540
+ error: {
541
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
542
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
543
+ }
544
+ };
545
+ }
546
+ let tokenResponse;
547
+ try {
548
+ tokenResponse = await response.json();
549
+ } catch (parseError) {
550
+ logger.error("Parse error during token refresh:", parseError);
551
+ return {
552
+ error: {
553
+ code: FFID_ERROR_CODES.PARSE_ERROR,
554
+ message: `\u30C8\u30FC\u30AF\u30F3\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F (status: ${response.status})`
555
+ }
556
+ };
557
+ }
558
+ if (!response.ok) {
559
+ const errorBody = tokenResponse;
560
+ logger.error("Token refresh failed:", errorBody);
561
+ return {
562
+ error: {
563
+ code: errorBody.error ?? FFID_ERROR_CODES.TOKEN_REFRESH_ERROR,
564
+ message: errorBody.error_description ?? "\u30C8\u30FC\u30AF\u30F3\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
565
+ }
566
+ };
567
+ }
568
+ tokenStore.setTokens({
569
+ accessToken: tokenResponse.access_token,
570
+ refreshToken: tokenResponse.refresh_token,
571
+ expiresAt: Date.now() + tokenResponse.expires_in * MS_PER_SECOND
572
+ });
573
+ logger.debug("Token refresh successful");
574
+ return { data: void 0 };
575
+ }
142
576
  function redirectToLogin() {
143
577
  if (typeof window === "undefined") {
144
578
  logger.debug("Cannot redirect in SSR context");
145
579
  return false;
146
580
  }
581
+ if (authMode === "token") {
582
+ return redirectToAuthorize();
583
+ }
147
584
  const currentUrl = window.location.href;
148
585
  const loginUrl = `${baseUrl}/login?redirect=${encodeURIComponent(currentUrl)}&service=${encodeURIComponent(config.serviceCode)}`;
149
586
  logger.debug("Redirecting to login:", loginUrl);
150
587
  window.location.href = loginUrl;
151
588
  return true;
152
589
  }
590
+ function redirectToAuthorize() {
591
+ const verifier = generateCodeVerifier();
592
+ storeCodeVerifier(verifier);
593
+ generateCodeChallenge(verifier).then((challenge) => {
594
+ const state = generateRandomState();
595
+ const currentUrl = window.location.origin + window.location.pathname;
596
+ const params = new URLSearchParams({
597
+ response_type: "code",
598
+ client_id: clientId,
599
+ redirect_uri: currentUrl,
600
+ state,
601
+ code_challenge: challenge,
602
+ code_challenge_method: "S256"
603
+ });
604
+ const authorizeUrl = `${baseUrl}${OAUTH_AUTHORIZE_ENDPOINT}?${params.toString()}`;
605
+ logger.debug("Redirecting to authorize:", authorizeUrl);
606
+ window.location.href = authorizeUrl;
607
+ }).catch((error) => {
608
+ logger.error("Failed to generate code challenge:", error);
609
+ });
610
+ return true;
611
+ }
153
612
  function getLoginUrl(redirectUrl) {
154
613
  const redirect = redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "");
155
614
  return `${baseUrl}/login?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(config.serviceCode)}`;
@@ -163,13 +622,26 @@ function createFFIDClient(config) {
163
622
  redirectToLogin,
164
623
  getLoginUrl,
165
624
  createError,
625
+ exchangeCodeForTokens,
626
+ refreshAccessToken,
627
+ /** Token store (token mode only) */
628
+ tokenStore,
629
+ /** Resolved auth mode */
630
+ authMode,
166
631
  /** Resolved logger instance */
167
632
  logger,
168
633
  baseUrl,
169
- serviceCode: config.serviceCode
634
+ serviceCode: config.serviceCode,
635
+ clientId
170
636
  };
171
637
  }
172
- var DEFAULT_REFRESH_INTERVAL = 5 * 60 * 1e3;
638
+ function generateRandomState() {
639
+ const array = new Uint8Array(STATE_RANDOM_BYTES);
640
+ crypto.getRandomValues(array);
641
+ return Array.from(array, (byte) => byte.toString(HEX_BASE).padStart(2, "0")).join("");
642
+ }
643
+ var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
644
+ var TOKEN_REFRESH_RATIO = 0.8;
173
645
  var FFIDContext = createContext(null);
174
646
  var FFIDClientContext = createContext(null);
175
647
  function FFIDProvider({
@@ -178,9 +650,11 @@ function FFIDProvider({
178
650
  apiBaseUrl,
179
651
  debug = false,
180
652
  logger,
181
- refreshInterval = DEFAULT_REFRESH_INTERVAL,
653
+ refreshInterval = DEFAULT_REFRESH_INTERVAL_MS,
182
654
  onAuthStateChange,
183
- onError
655
+ onError,
656
+ authMode,
657
+ clientId
184
658
  }) {
185
659
  const [user, setUser] = useState(null);
186
660
  const [organizations, setOrganizations] = useState([]);
@@ -201,9 +675,11 @@ function FFIDProvider({
201
675
  serviceCode,
202
676
  apiBaseUrl,
203
677
  debug,
204
- logger
678
+ logger,
679
+ authMode,
680
+ clientId
205
681
  }),
206
- [serviceCode, apiBaseUrl, debug, logger]
682
+ [serviceCode, apiBaseUrl, debug, logger, authMode, clientId]
207
683
  );
208
684
  const refresh = useCallback(async () => {
209
685
  client.logger.debug("Refreshing session...");
@@ -216,7 +692,7 @@ function FFIDProvider({
216
692
  setSubscriptions([]);
217
693
  setError(response.error);
218
694
  onAuthStateChangeRef.current?.(null);
219
- if (response.error.code !== "SESSION_NOT_FOUND") {
695
+ if (response.error.code !== "SESSION_NOT_FOUND" && response.error.code !== "NO_TOKENS") {
220
696
  onErrorRef.current?.(response.error);
221
697
  }
222
698
  return;
@@ -305,15 +781,72 @@ function FFIDProvider({
305
781
  [organizations, client]
306
782
  );
307
783
  useEffect(() => {
784
+ if (client.authMode !== "token") return;
785
+ if (typeof window === "undefined") return;
786
+ const urlParams = new URLSearchParams(window.location.search);
787
+ const code = urlParams.get("code");
788
+ if (!code) return;
789
+ client.logger.debug("Authorization code detected, exchanging for tokens");
790
+ const codeVerifier = retrieveCodeVerifier();
791
+ client.exchangeCodeForTokens(code, codeVerifier ?? void 0).then((result) => {
792
+ if (result.error) {
793
+ client.logger.error("Token exchange failed:", result.error);
794
+ setError(result.error);
795
+ onErrorRef.current?.(result.error);
796
+ setIsLoading(false);
797
+ return;
798
+ }
799
+ const cleanUrl = new URL(window.location.href);
800
+ cleanUrl.searchParams.delete("code");
801
+ cleanUrl.searchParams.delete("state");
802
+ window.history.replaceState({}, "", cleanUrl.toString());
803
+ client.logger.debug("Token exchange successful, refreshing session");
804
+ return refresh();
805
+ }).catch((err) => {
806
+ client.logger.error("Token exchange error:", err);
807
+ setIsLoading(false);
808
+ });
809
+ }, [client]);
810
+ useEffect(() => {
811
+ if (client.authMode === "token") {
812
+ if (typeof window !== "undefined") {
813
+ const urlParams = new URLSearchParams(window.location.search);
814
+ if (urlParams.has("code")) {
815
+ return;
816
+ }
817
+ }
818
+ const tokens = client.tokenStore.getTokens();
819
+ if (tokens) {
820
+ refresh();
821
+ } else {
822
+ setIsLoading(false);
823
+ }
824
+ return;
825
+ }
308
826
  refresh();
309
- }, [refresh]);
827
+ }, [refresh, client]);
310
828
  useEffect(() => {
311
- if (!user || refreshInterval <= 0) return;
829
+ if (!user) return;
830
+ let intervalMs = refreshInterval;
831
+ if (client.authMode === "token") {
832
+ const tokens = client.tokenStore.getTokens();
833
+ if (tokens) {
834
+ const remainingMs = tokens.expiresAt - Date.now();
835
+ if (remainingMs > 0) {
836
+ intervalMs = Math.floor(remainingMs * TOKEN_REFRESH_RATIO);
837
+ }
838
+ }
839
+ }
840
+ if (intervalMs <= 0) return;
312
841
  const intervalId = setInterval(() => {
313
- refresh();
314
- }, refreshInterval);
842
+ if (client.authMode === "token") {
843
+ client.refreshAccessToken().then(() => refresh());
844
+ } else {
845
+ refresh();
846
+ }
847
+ }, intervalMs);
315
848
  return () => clearInterval(intervalId);
316
- }, [user, refreshInterval, refresh]);
849
+ }, [user, refreshInterval, refresh, client]);
317
850
  const currentOrganization = useMemo(
318
851
  () => organizations.find((o) => o.id === currentOrganizationId) ?? null,
319
852
  [organizations, currentOrganizationId]
@@ -1379,4 +1912,4 @@ function FFIDAnnouncementList({
1379
1912
  );
1380
1913
  }
1381
1914
 
1382
- export { DEFAULT_API_BASE_URL, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, createFFIDAnnouncementsClient, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, withSubscription };
1915
+ export { DEFAULT_API_BASE_URL, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, createFFIDAnnouncementsClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, retrieveCodeVerifier, storeCodeVerifier, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, withSubscription };