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