@feelflow/ffid-sdk 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,11 +4,192 @@ 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
+
138
+ // src/client/oauth-userinfo.ts
139
+ var VALID_SUBSCRIPTION_STATUSES = ["trialing", "active", "past_due", "canceled", "paused"];
140
+ function isValidSubscriptionStatus(value) {
141
+ return VALID_SUBSCRIPTION_STATUSES.includes(value);
142
+ }
143
+ function normalizeUserinfo(raw) {
144
+ return {
145
+ sub: raw.sub,
146
+ email: raw.email,
147
+ name: raw.name,
148
+ picture: raw.picture,
149
+ organizationId: raw.organization_id ?? null,
150
+ subscription: raw.subscription ? {
151
+ status: raw.subscription.status ?? null,
152
+ planCode: raw.subscription.plan_code ?? null,
153
+ seatModel: raw.subscription.seat_model ?? null,
154
+ memberRole: raw.subscription.member_role ?? null,
155
+ organizationId: raw.subscription.organization_id ?? null
156
+ } : void 0
157
+ };
158
+ }
159
+ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
160
+ const subscription = userinfo.subscription;
161
+ if (!subscription || !subscription.planCode || !isValidSubscriptionStatus(subscription.status)) {
162
+ return [];
163
+ }
164
+ return [
165
+ {
166
+ id: `userinfo:${serviceCode}`,
167
+ serviceCode,
168
+ serviceName: serviceCode,
169
+ planCode: subscription.planCode,
170
+ planName: subscription.planCode,
171
+ status: subscription.status,
172
+ currentPeriodEnd: null,
173
+ seatModel: subscription.seatModel ?? void 0,
174
+ memberRole: subscription.memberRole ?? void 0,
175
+ organizationId: subscription.organizationId
176
+ }
177
+ ];
178
+ }
179
+
7
180
  // src/client/ffid-client.ts
8
181
  var NO_CONTENT_STATUS = 204;
9
182
  var SESSION_ENDPOINT = "/api/v1/auth/session";
10
183
  var LOGOUT_ENDPOINT = "/api/v1/auth/signout";
184
+ var OAUTH_TOKEN_ENDPOINT = "/api/v1/oauth/token";
185
+ var OAUTH_USERINFO_ENDPOINT = "/api/v1/oauth/userinfo";
186
+ var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
187
+ var OAUTH_REVOKE_ENDPOINT = "/api/v1/oauth/revoke";
11
188
  var SDK_LOG_PREFIX = "[FFID SDK]";
189
+ var MS_PER_SECOND = 1e3;
190
+ var UNAUTHORIZED_STATUS = 401;
191
+ var STATE_RANDOM_BYTES = 16;
192
+ var HEX_BASE = 16;
12
193
  var noopLogger = {
13
194
  debug: () => {
14
195
  },
@@ -30,28 +211,30 @@ var FFID_ERROR_CODES = {
30
211
  /** Server returned non-JSON response (e.g., HTML error page) */
31
212
  PARSE_ERROR: "PARSE_ERROR",
32
213
  /** Server returned error without structured error body */
33
- UNKNOWN_ERROR: "UNKNOWN_ERROR"
214
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
215
+ /** Token exchange failed */
216
+ TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
217
+ /** Token refresh failed */
218
+ TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
219
+ /** No tokens available */
220
+ NO_TOKENS: "NO_TOKENS"
34
221
  };
35
222
  function createFFIDClient(config) {
36
223
  if (!config.serviceCode || !config.serviceCode.trim()) {
37
224
  throw new Error("FFID Client: serviceCode \u304C\u672A\u8A2D\u5B9A\u3067\u3059");
38
225
  }
39
226
  const baseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
227
+ const authMode = config.authMode ?? "cookie";
228
+ const clientId = config.clientId ?? config.serviceCode;
40
229
  const logger = config.logger ?? (config.debug ? consoleLogger : noopLogger);
230
+ const tokenStore = authMode === "token" ? createTokenStore() : createTokenStore("memory");
41
231
  async function fetchWithAuth(endpoint, options = {}) {
42
232
  const url = `${baseUrl}${endpoint}`;
43
233
  logger.debug("Fetching:", url);
234
+ const fetchOptions = buildFetchOptions(options);
44
235
  let response;
45
236
  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
- });
237
+ response = await fetch(url, fetchOptions);
55
238
  } catch (error) {
56
239
  logger.error("Network error:", error);
57
240
  return {
@@ -61,6 +244,24 @@ function createFFIDClient(config) {
61
244
  }
62
245
  };
63
246
  }
247
+ if (authMode === "token" && response.status === UNAUTHORIZED_STATUS) {
248
+ const refreshResult = await refreshAccessToken();
249
+ if (!refreshResult.error) {
250
+ logger.debug("Token refreshed, retrying request");
251
+ const retryOptions = buildFetchOptions(options);
252
+ try {
253
+ response = await fetch(url, retryOptions);
254
+ } catch (retryError) {
255
+ logger.error("Network error on retry:", retryError);
256
+ return {
257
+ error: {
258
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
259
+ message: retryError instanceof Error ? retryError.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
260
+ }
261
+ };
262
+ }
263
+ }
264
+ }
64
265
  let raw;
65
266
  try {
66
267
  raw = await response.json();
@@ -92,10 +293,140 @@ function createFFIDClient(config) {
92
293
  }
93
294
  return { data: raw.data };
94
295
  }
296
+ function buildFetchOptions(options) {
297
+ if (authMode === "token") {
298
+ const tokens = tokenStore.getTokens();
299
+ const headers = {
300
+ "Content-Type": "application/json",
301
+ ...options.headers
302
+ };
303
+ if (tokens) {
304
+ headers["Authorization"] = `Bearer ${tokens.accessToken}`;
305
+ }
306
+ return {
307
+ ...options,
308
+ credentials: "omit",
309
+ headers
310
+ };
311
+ }
312
+ return {
313
+ ...options,
314
+ credentials: "include",
315
+ headers: {
316
+ "Content-Type": "application/json",
317
+ ...options.headers
318
+ }
319
+ };
320
+ }
95
321
  async function getSession() {
322
+ if (authMode === "token") {
323
+ return getSessionFromUserinfo();
324
+ }
96
325
  return fetchWithAuth(SESSION_ENDPOINT);
97
326
  }
327
+ async function getSessionFromUserinfo() {
328
+ const tokens = tokenStore.getTokens();
329
+ if (!tokens) {
330
+ return {
331
+ error: {
332
+ code: FFID_ERROR_CODES.NO_TOKENS,
333
+ message: "\u30C8\u30FC\u30AF\u30F3\u304C\u4FDD\u5B58\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
334
+ }
335
+ };
336
+ }
337
+ const url = `${baseUrl}${OAUTH_USERINFO_ENDPOINT}`;
338
+ logger.debug("Fetching userinfo:", url);
339
+ let response;
340
+ try {
341
+ response = await fetch(url, {
342
+ credentials: "omit",
343
+ headers: {
344
+ "Authorization": `Bearer ${tokens.accessToken}`,
345
+ "Content-Type": "application/json"
346
+ }
347
+ });
348
+ } catch (error) {
349
+ logger.error("Network error:", error);
350
+ return {
351
+ error: {
352
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
353
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
354
+ }
355
+ };
356
+ }
357
+ if (response.status === UNAUTHORIZED_STATUS) {
358
+ const refreshResult = await refreshAccessToken();
359
+ if (!refreshResult.error) {
360
+ const retryTokens = tokenStore.getTokens();
361
+ if (retryTokens) {
362
+ try {
363
+ response = await fetch(url, {
364
+ credentials: "omit",
365
+ headers: {
366
+ "Authorization": `Bearer ${retryTokens.accessToken}`,
367
+ "Content-Type": "application/json"
368
+ }
369
+ });
370
+ } catch (retryError) {
371
+ logger.error("Network error on retry:", retryError);
372
+ return {
373
+ error: {
374
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
375
+ message: retryError instanceof Error ? retryError.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
376
+ }
377
+ };
378
+ }
379
+ }
380
+ } else {
381
+ return refreshResult;
382
+ }
383
+ }
384
+ let rawUserinfo;
385
+ try {
386
+ rawUserinfo = await response.json();
387
+ } catch (parseError) {
388
+ logger.error("Parse error:", parseError, "Status:", response.status);
389
+ return {
390
+ error: {
391
+ code: FFID_ERROR_CODES.PARSE_ERROR,
392
+ 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})`
393
+ }
394
+ };
395
+ }
396
+ if (!response.ok) {
397
+ const errorBody = rawUserinfo;
398
+ return {
399
+ error: {
400
+ code: errorBody.code ?? FFID_ERROR_CODES.UNKNOWN_ERROR,
401
+ message: errorBody.message ?? "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
402
+ }
403
+ };
404
+ }
405
+ const userinfo = normalizeUserinfo(rawUserinfo);
406
+ const user = {
407
+ id: userinfo.sub,
408
+ email: userinfo.email ?? "",
409
+ displayName: userinfo.name ?? null,
410
+ avatarUrl: userinfo.picture ?? null,
411
+ locale: null,
412
+ timezone: null,
413
+ createdAt: ""
414
+ };
415
+ return {
416
+ data: {
417
+ user,
418
+ organizations: [],
419
+ subscriptions: mapUserinfoSubscriptionToSession(userinfo, config.serviceCode)
420
+ }
421
+ };
422
+ }
98
423
  async function signOut() {
424
+ if (authMode === "token") {
425
+ return signOutToken();
426
+ }
427
+ return signOutCookie();
428
+ }
429
+ async function signOutCookie() {
99
430
  const url = `${baseUrl}${LOGOUT_ENDPOINT}`;
100
431
  logger.debug("Fetching:", url);
101
432
  let response;
@@ -139,21 +470,196 @@ function createFFIDClient(config) {
139
470
  logger.debug("Response:", response.status);
140
471
  return { data: void 0 };
141
472
  }
473
+ async function signOutToken() {
474
+ const tokens = tokenStore.getTokens();
475
+ tokenStore.clearTokens();
476
+ if (!tokens) {
477
+ logger.debug("No tokens to revoke");
478
+ return { data: void 0 };
479
+ }
480
+ const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
481
+ logger.debug("Revoking token:", url);
482
+ try {
483
+ await fetch(url, {
484
+ method: "POST",
485
+ credentials: "omit",
486
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
487
+ body: new URLSearchParams({
488
+ token: tokens.accessToken,
489
+ client_id: clientId
490
+ }).toString()
491
+ });
492
+ } catch (error) {
493
+ logger.warn("Token revocation failed:", error);
494
+ }
495
+ logger.debug("Token sign-out completed");
496
+ return { data: void 0 };
497
+ }
498
+ async function exchangeCodeForTokens(code, codeVerifier) {
499
+ const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
500
+ logger.debug("Exchanging code for tokens:", url);
501
+ const body = {
502
+ grant_type: "authorization_code",
503
+ code,
504
+ client_id: clientId,
505
+ redirect_uri: typeof window !== "undefined" ? window.location.origin + window.location.pathname : ""
506
+ };
507
+ if (codeVerifier) {
508
+ body.code_verifier = codeVerifier;
509
+ }
510
+ let response;
511
+ try {
512
+ response = await fetch(url, {
513
+ method: "POST",
514
+ credentials: "omit",
515
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
516
+ body: new URLSearchParams(body).toString()
517
+ });
518
+ } catch (error) {
519
+ logger.error("Network error during token exchange:", error);
520
+ return {
521
+ error: {
522
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
523
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
524
+ }
525
+ };
526
+ }
527
+ let tokenResponse;
528
+ try {
529
+ tokenResponse = await response.json();
530
+ } catch (parseError) {
531
+ logger.error("Parse error during token exchange:", parseError);
532
+ return {
533
+ error: {
534
+ code: FFID_ERROR_CODES.PARSE_ERROR,
535
+ message: `\u30C8\u30FC\u30AF\u30F3\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F (status: ${response.status})`
536
+ }
537
+ };
538
+ }
539
+ if (!response.ok) {
540
+ const errorBody = tokenResponse;
541
+ return {
542
+ error: {
543
+ code: errorBody.error ?? FFID_ERROR_CODES.TOKEN_EXCHANGE_ERROR,
544
+ message: errorBody.error_description ?? "\u30C8\u30FC\u30AF\u30F3\u4EA4\u63DB\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
545
+ }
546
+ };
547
+ }
548
+ tokenStore.setTokens({
549
+ accessToken: tokenResponse.access_token,
550
+ refreshToken: tokenResponse.refresh_token,
551
+ expiresAt: Date.now() + tokenResponse.expires_in * MS_PER_SECOND
552
+ });
553
+ logger.debug("Token exchange successful");
554
+ return { data: void 0 };
555
+ }
556
+ async function refreshAccessToken() {
557
+ const tokens = tokenStore.getTokens();
558
+ if (!tokens) {
559
+ return {
560
+ error: {
561
+ code: FFID_ERROR_CODES.NO_TOKENS,
562
+ message: "\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u30C8\u30FC\u30AF\u30F3\u304C\u3042\u308A\u307E\u305B\u3093"
563
+ }
564
+ };
565
+ }
566
+ const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
567
+ logger.debug("Refreshing access token:", url);
568
+ let response;
569
+ try {
570
+ response = await fetch(url, {
571
+ method: "POST",
572
+ credentials: "omit",
573
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
574
+ body: new URLSearchParams({
575
+ grant_type: "refresh_token",
576
+ refresh_token: tokens.refreshToken,
577
+ client_id: clientId
578
+ }).toString()
579
+ });
580
+ } catch (error) {
581
+ logger.error("Network error during token refresh:", error);
582
+ return {
583
+ error: {
584
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
585
+ message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
586
+ }
587
+ };
588
+ }
589
+ let tokenResponse;
590
+ try {
591
+ tokenResponse = await response.json();
592
+ } catch (parseError) {
593
+ logger.error("Parse error during token refresh:", parseError);
594
+ return {
595
+ error: {
596
+ code: FFID_ERROR_CODES.PARSE_ERROR,
597
+ message: `\u30C8\u30FC\u30AF\u30F3\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F (status: ${response.status})`
598
+ }
599
+ };
600
+ }
601
+ if (!response.ok) {
602
+ const errorBody = tokenResponse;
603
+ logger.error("Token refresh failed:", errorBody);
604
+ return {
605
+ error: {
606
+ code: errorBody.error ?? FFID_ERROR_CODES.TOKEN_REFRESH_ERROR,
607
+ message: errorBody.error_description ?? "\u30C8\u30FC\u30AF\u30F3\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
608
+ }
609
+ };
610
+ }
611
+ tokenStore.setTokens({
612
+ accessToken: tokenResponse.access_token,
613
+ refreshToken: tokenResponse.refresh_token,
614
+ expiresAt: Date.now() + tokenResponse.expires_in * MS_PER_SECOND
615
+ });
616
+ logger.debug("Token refresh successful");
617
+ return { data: void 0 };
618
+ }
142
619
  function redirectToLogin() {
143
620
  if (typeof window === "undefined") {
144
621
  logger.debug("Cannot redirect in SSR context");
145
622
  return false;
146
623
  }
624
+ if (authMode === "token") {
625
+ return redirectToAuthorize();
626
+ }
147
627
  const currentUrl = window.location.href;
148
628
  const loginUrl = `${baseUrl}/login?redirect=${encodeURIComponent(currentUrl)}&service=${encodeURIComponent(config.serviceCode)}`;
149
629
  logger.debug("Redirecting to login:", loginUrl);
150
630
  window.location.href = loginUrl;
151
631
  return true;
152
632
  }
633
+ function redirectToAuthorize() {
634
+ const verifier = generateCodeVerifier();
635
+ storeCodeVerifier(verifier);
636
+ generateCodeChallenge(verifier).then((challenge) => {
637
+ const state = generateRandomState();
638
+ const currentUrl = window.location.origin + window.location.pathname;
639
+ const params = new URLSearchParams({
640
+ response_type: "code",
641
+ client_id: clientId,
642
+ redirect_uri: currentUrl,
643
+ state,
644
+ code_challenge: challenge,
645
+ code_challenge_method: "S256"
646
+ });
647
+ const authorizeUrl = `${baseUrl}${OAUTH_AUTHORIZE_ENDPOINT}?${params.toString()}`;
648
+ logger.debug("Redirecting to authorize:", authorizeUrl);
649
+ window.location.href = authorizeUrl;
650
+ }).catch((error) => {
651
+ logger.error("Failed to generate code challenge:", error);
652
+ });
653
+ return true;
654
+ }
153
655
  function getLoginUrl(redirectUrl) {
154
656
  const redirect = redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "");
155
657
  return `${baseUrl}/login?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(config.serviceCode)}`;
156
658
  }
659
+ function getSignupUrl(redirectUrl) {
660
+ const redirect = redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "");
661
+ return `${baseUrl}/signup?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(config.serviceCode)}`;
662
+ }
157
663
  function createError(code, message) {
158
664
  return { code, message };
159
665
  }
@@ -162,14 +668,28 @@ function createFFIDClient(config) {
162
668
  signOut,
163
669
  redirectToLogin,
164
670
  getLoginUrl,
671
+ getSignupUrl,
165
672
  createError,
673
+ exchangeCodeForTokens,
674
+ refreshAccessToken,
675
+ /** Token store (token mode only) */
676
+ tokenStore,
677
+ /** Resolved auth mode */
678
+ authMode,
166
679
  /** Resolved logger instance */
167
680
  logger,
168
681
  baseUrl,
169
- serviceCode: config.serviceCode
682
+ serviceCode: config.serviceCode,
683
+ clientId
170
684
  };
171
685
  }
172
- var DEFAULT_REFRESH_INTERVAL = 5 * 60 * 1e3;
686
+ function generateRandomState() {
687
+ const array = new Uint8Array(STATE_RANDOM_BYTES);
688
+ crypto.getRandomValues(array);
689
+ return Array.from(array, (byte) => byte.toString(HEX_BASE).padStart(2, "0")).join("");
690
+ }
691
+ var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
692
+ var TOKEN_REFRESH_RATIO = 0.8;
173
693
  var FFIDContext = createContext(null);
174
694
  var FFIDClientContext = createContext(null);
175
695
  function FFIDProvider({
@@ -178,9 +698,11 @@ function FFIDProvider({
178
698
  apiBaseUrl,
179
699
  debug = false,
180
700
  logger,
181
- refreshInterval = DEFAULT_REFRESH_INTERVAL,
701
+ refreshInterval = DEFAULT_REFRESH_INTERVAL_MS,
182
702
  onAuthStateChange,
183
- onError
703
+ onError,
704
+ authMode,
705
+ clientId
184
706
  }) {
185
707
  const [user, setUser] = useState(null);
186
708
  const [organizations, setOrganizations] = useState([]);
@@ -201,9 +723,11 @@ function FFIDProvider({
201
723
  serviceCode,
202
724
  apiBaseUrl,
203
725
  debug,
204
- logger
726
+ logger,
727
+ authMode,
728
+ clientId
205
729
  }),
206
- [serviceCode, apiBaseUrl, debug, logger]
730
+ [serviceCode, apiBaseUrl, debug, logger, authMode, clientId]
207
731
  );
208
732
  const refresh = useCallback(async () => {
209
733
  client.logger.debug("Refreshing session...");
@@ -216,7 +740,7 @@ function FFIDProvider({
216
740
  setSubscriptions([]);
217
741
  setError(response.error);
218
742
  onAuthStateChangeRef.current?.(null);
219
- if (response.error.code !== "SESSION_NOT_FOUND") {
743
+ if (response.error.code !== "SESSION_NOT_FOUND" && response.error.code !== "NO_TOKENS") {
220
744
  onErrorRef.current?.(response.error);
221
745
  }
222
746
  return;
@@ -305,15 +829,72 @@ function FFIDProvider({
305
829
  [organizations, client]
306
830
  );
307
831
  useEffect(() => {
832
+ if (client.authMode !== "token") return;
833
+ if (typeof window === "undefined") return;
834
+ const urlParams = new URLSearchParams(window.location.search);
835
+ const code = urlParams.get("code");
836
+ if (!code) return;
837
+ client.logger.debug("Authorization code detected, exchanging for tokens");
838
+ const codeVerifier = retrieveCodeVerifier();
839
+ client.exchangeCodeForTokens(code, codeVerifier ?? void 0).then((result) => {
840
+ if (result.error) {
841
+ client.logger.error("Token exchange failed:", result.error);
842
+ setError(result.error);
843
+ onErrorRef.current?.(result.error);
844
+ setIsLoading(false);
845
+ return;
846
+ }
847
+ const cleanUrl = new URL(window.location.href);
848
+ cleanUrl.searchParams.delete("code");
849
+ cleanUrl.searchParams.delete("state");
850
+ window.history.replaceState({}, "", cleanUrl.toString());
851
+ client.logger.debug("Token exchange successful, refreshing session");
852
+ return refresh();
853
+ }).catch((err) => {
854
+ client.logger.error("Token exchange error:", err);
855
+ setIsLoading(false);
856
+ });
857
+ }, [client]);
858
+ useEffect(() => {
859
+ if (client.authMode === "token") {
860
+ if (typeof window !== "undefined") {
861
+ const urlParams = new URLSearchParams(window.location.search);
862
+ if (urlParams.has("code")) {
863
+ return;
864
+ }
865
+ }
866
+ const tokens = client.tokenStore.getTokens();
867
+ if (tokens) {
868
+ refresh();
869
+ } else {
870
+ setIsLoading(false);
871
+ }
872
+ return;
873
+ }
308
874
  refresh();
309
- }, [refresh]);
875
+ }, [refresh, client]);
310
876
  useEffect(() => {
311
- if (!user || refreshInterval <= 0) return;
877
+ if (!user) return;
878
+ let intervalMs = refreshInterval;
879
+ if (client.authMode === "token") {
880
+ const tokens = client.tokenStore.getTokens();
881
+ if (tokens) {
882
+ const remainingMs = tokens.expiresAt - Date.now();
883
+ if (remainingMs > 0) {
884
+ intervalMs = Math.floor(remainingMs * TOKEN_REFRESH_RATIO);
885
+ }
886
+ }
887
+ }
888
+ if (intervalMs <= 0) return;
312
889
  const intervalId = setInterval(() => {
313
- refresh();
314
- }, refreshInterval);
890
+ if (client.authMode === "token") {
891
+ client.refreshAccessToken().then(() => refresh());
892
+ } else {
893
+ refresh();
894
+ }
895
+ }, intervalMs);
315
896
  return () => clearInterval(intervalId);
316
- }, [user, refreshInterval, refresh]);
897
+ }, [user, refreshInterval, refresh, client]);
317
898
  const currentOrganization = useMemo(
318
899
  () => organizations.find((o) => o.id === currentOrganizationId) ?? null,
319
900
  [organizations, currentOrganizationId]
@@ -365,6 +946,7 @@ function useFFIDClient() {
365
946
  // src/hooks/useFFID.ts
366
947
  function useFFID() {
367
948
  const context = useFFIDContext();
949
+ const client = useFFIDClient();
368
950
  return {
369
951
  user: context.user,
370
952
  organizations: context.organizations,
@@ -375,7 +957,9 @@ function useFFID() {
375
957
  login: context.login,
376
958
  logout: context.logout,
377
959
  switchOrganization: context.switchOrganization,
378
- refresh: context.refresh
960
+ refresh: context.refresh,
961
+ getLoginUrl: client.getLoginUrl,
962
+ getSignupUrl: client.getSignupUrl
379
963
  };
380
964
  }
381
965
  function FFIDLoginButton({
@@ -1379,4 +1963,4 @@ function FFIDAnnouncementList({
1379
1963
  );
1380
1964
  }
1381
1965
 
1382
- export { DEFAULT_API_BASE_URL, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, createFFIDAnnouncementsClient, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, withSubscription };
1966
+ 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 };