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