@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.
- package/README.md +17 -0
- package/dist/{chunk-VXBUXOLF.js → chunk-K525KYQP.js} +608 -24
- package/dist/{chunk-A6YJDYIX.cjs → chunk-TL6U5TJT.cjs} +612 -23
- package/dist/components/index.cjs +7 -7
- package/dist/components/index.d.cts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/{index-DBp3Ulyl.d.cts → index-CyYHo3-R.d.cts} +40 -1
- package/dist/{index-DBp3Ulyl.d.ts → index-CyYHo3-R.d.ts} +40 -1
- package/dist/index.cjs +36 -16
- package/dist/index.d.cts +81 -4
- package/dist/index.d.ts +81 -4
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
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;
|