@feelflow/ffid-sdk 1.9.0 → 1.10.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 +1 -1
- package/dist/agency/index.cjs +3 -3
- package/dist/agency/index.d.cts +1 -1
- package/dist/agency/index.d.ts +1 -1
- package/dist/agency/index.js +2 -2
- package/dist/announcements/index.cjs +2 -2
- package/dist/announcements/index.d.cts +2 -2
- package/dist/announcements/index.d.ts +2 -2
- package/dist/announcements/index.js +1 -1
- package/dist/{chunk-CBYBTBKA.cjs → chunk-OAPA5VKR.cjs} +3 -3
- package/dist/{chunk-H5JRKOD7.js → chunk-PSSNMEJB.js} +3 -3
- package/dist/chunk-QBRM2RRC.js +4 -0
- package/dist/{chunk-P5PPUZGX.cjs → chunk-YUIITYBE.cjs} +1 -1
- 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/{constants-BeWMWOOd.d.cts → constants-DvTGHPZn.d.cts} +1 -1
- package/dist/{constants-BeWMWOOd.d.ts → constants-DvTGHPZn.d.ts} +1 -1
- package/dist/{index-CTvhqdrH.d.cts → index-DsXjcF0i.d.cts} +1 -1
- package/dist/{index-CTvhqdrH.d.ts → index-DsXjcF0i.d.ts} +1 -1
- package/dist/index.cjs +22 -22
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/legal/index.cjs +3 -3
- package/dist/legal/index.d.cts +3 -3
- package/dist/legal/index.d.ts +3 -3
- package/dist/legal/index.js +2 -2
- package/dist/server/index.cjs +1138 -0
- package/dist/server/index.d.cts +416 -0
- package/dist/server/index.d.ts +416 -0
- package/dist/server/index.js +1129 -0
- package/package.json +9 -3
- package/dist/chunk-P4MLCG4T.js +0 -4
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../chunk-QBRM2RRC.js';
|
|
2
|
+
export { DEFAULT_API_BASE_URL } from '../chunk-QBRM2RRC.js';
|
|
3
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
4
|
+
|
|
5
|
+
// src/auth/token-store.ts
|
|
6
|
+
var STORAGE_KEY = "ffid_tokens";
|
|
7
|
+
var EXPIRY_BUFFER_SECONDS = 30;
|
|
8
|
+
var EXPIRY_BUFFER_MS = EXPIRY_BUFFER_SECONDS * 1e3;
|
|
9
|
+
function isLocalStorageAvailable() {
|
|
10
|
+
try {
|
|
11
|
+
if (typeof window === "undefined") return false;
|
|
12
|
+
const storage = window.localStorage;
|
|
13
|
+
if (!storage || typeof storage.setItem !== "function") return false;
|
|
14
|
+
const testKey = "__ffid_storage_test__";
|
|
15
|
+
storage.setItem(testKey, "1");
|
|
16
|
+
storage.removeItem(testKey);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function createLocalStorageStore() {
|
|
23
|
+
const storage = window.localStorage;
|
|
24
|
+
return {
|
|
25
|
+
getTokens() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = storage.getItem(STORAGE_KEY);
|
|
28
|
+
if (!raw) return null;
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!isTokenData(parsed)) return null;
|
|
31
|
+
return parsed;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
setTokens(tokens) {
|
|
37
|
+
try {
|
|
38
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(tokens));
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
clearTokens() {
|
|
43
|
+
try {
|
|
44
|
+
storage.removeItem(STORAGE_KEY);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
isAccessTokenExpired() {
|
|
49
|
+
const tokens = this.getTokens();
|
|
50
|
+
if (!tokens) return true;
|
|
51
|
+
return Date.now() >= tokens.expiresAt - EXPIRY_BUFFER_MS;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createMemoryStore() {
|
|
56
|
+
let stored = null;
|
|
57
|
+
return {
|
|
58
|
+
getTokens() {
|
|
59
|
+
return stored;
|
|
60
|
+
},
|
|
61
|
+
setTokens(tokens) {
|
|
62
|
+
stored = { ...tokens };
|
|
63
|
+
},
|
|
64
|
+
clearTokens() {
|
|
65
|
+
stored = null;
|
|
66
|
+
},
|
|
67
|
+
isAccessTokenExpired() {
|
|
68
|
+
if (!stored) return true;
|
|
69
|
+
return Date.now() >= stored.expiresAt - EXPIRY_BUFFER_MS;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isTokenData(value) {
|
|
74
|
+
if (typeof value !== "object" || value === null) return false;
|
|
75
|
+
const obj = value;
|
|
76
|
+
return typeof obj.accessToken === "string" && typeof obj.refreshToken === "string" && typeof obj.expiresAt === "number";
|
|
77
|
+
}
|
|
78
|
+
function createTokenStore(storageType) {
|
|
79
|
+
if (storageType === "memory") {
|
|
80
|
+
return createMemoryStore();
|
|
81
|
+
}
|
|
82
|
+
if (typeof window !== "undefined" && isLocalStorageAvailable()) {
|
|
83
|
+
return createLocalStorageStore();
|
|
84
|
+
}
|
|
85
|
+
return createMemoryStore();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/auth/pkce.ts
|
|
89
|
+
var VERIFIER_STORAGE_KEY = "ffid_code_verifier";
|
|
90
|
+
var CODE_VERIFIER_MIN_LENGTH = 43;
|
|
91
|
+
var UNRESERVED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
92
|
+
function generateCodeVerifier() {
|
|
93
|
+
const length = CODE_VERIFIER_MIN_LENGTH;
|
|
94
|
+
const randomValues = new Uint8Array(length);
|
|
95
|
+
crypto.getRandomValues(randomValues);
|
|
96
|
+
let verifier = "";
|
|
97
|
+
for (let i = 0; i < length; i++) {
|
|
98
|
+
verifier += UNRESERVED_CHARS[randomValues[i] % UNRESERVED_CHARS.length];
|
|
99
|
+
}
|
|
100
|
+
return verifier;
|
|
101
|
+
}
|
|
102
|
+
async function generateCodeChallenge(verifier) {
|
|
103
|
+
const encoder = new TextEncoder();
|
|
104
|
+
const data = encoder.encode(verifier);
|
|
105
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
106
|
+
return base64UrlEncode(digest);
|
|
107
|
+
}
|
|
108
|
+
function storeCodeVerifier(verifier) {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof window === "undefined") return;
|
|
111
|
+
window.sessionStorage.setItem(VERIFIER_STORAGE_KEY, verifier);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function base64UrlEncode(buffer) {
|
|
116
|
+
const bytes = new Uint8Array(buffer);
|
|
117
|
+
let binary = "";
|
|
118
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
119
|
+
binary += String.fromCharCode(bytes[i]);
|
|
120
|
+
}
|
|
121
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/client/oauth-userinfo.ts
|
|
125
|
+
var VALID_SUBSCRIPTION_STATUSES = ["trialing", "active", "past_due", "canceled", "paused"];
|
|
126
|
+
function isValidSubscriptionStatus(value) {
|
|
127
|
+
return VALID_SUBSCRIPTION_STATUSES.includes(value);
|
|
128
|
+
}
|
|
129
|
+
function normalizeUserinfo(raw) {
|
|
130
|
+
return {
|
|
131
|
+
sub: raw.sub,
|
|
132
|
+
email: raw.email,
|
|
133
|
+
name: raw.name,
|
|
134
|
+
picture: raw.picture,
|
|
135
|
+
organizationId: raw.organization_id ?? null,
|
|
136
|
+
subscription: raw.subscription ? {
|
|
137
|
+
subscriptionId: raw.subscription.subscription_id ?? null,
|
|
138
|
+
status: raw.subscription.status ?? null,
|
|
139
|
+
planCode: raw.subscription.plan_code ?? null,
|
|
140
|
+
seatModel: raw.subscription.seat_model ?? null,
|
|
141
|
+
memberRole: raw.subscription.member_role ?? null,
|
|
142
|
+
organizationId: raw.subscription.organization_id ?? null
|
|
143
|
+
} : void 0
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
|
|
147
|
+
const subscription = userinfo.subscription;
|
|
148
|
+
if (!subscription || !subscription.planCode || !isValidSubscriptionStatus(subscription.status)) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
id: subscription.subscriptionId ?? `userinfo:${serviceCode}`,
|
|
154
|
+
serviceCode,
|
|
155
|
+
serviceName: serviceCode,
|
|
156
|
+
planCode: subscription.planCode,
|
|
157
|
+
planName: subscription.planCode,
|
|
158
|
+
status: subscription.status,
|
|
159
|
+
currentPeriodEnd: null,
|
|
160
|
+
seatModel: subscription.seatModel ?? void 0,
|
|
161
|
+
memberRole: subscription.memberRole ?? void 0,
|
|
162
|
+
organizationId: subscription.organizationId
|
|
163
|
+
}
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
var JWKS_ENDPOINT = "/.well-known/jwks.json";
|
|
167
|
+
var JWT_ISSUER = "https://id.feelflow.net";
|
|
168
|
+
var JWT_ALGORITHM = "ES256";
|
|
169
|
+
var JWT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
170
|
+
function createJwtVerifier(deps) {
|
|
171
|
+
const { baseUrl, serviceCode, logger, createError, errorCodes } = deps;
|
|
172
|
+
const jwksUrl = new URL(JWKS_ENDPOINT, baseUrl);
|
|
173
|
+
const jwks = createRemoteJWKSet(jwksUrl);
|
|
174
|
+
async function verifyJwt(accessToken) {
|
|
175
|
+
if (!accessToken || !accessToken.trim()) {
|
|
176
|
+
return {
|
|
177
|
+
error: createError(
|
|
178
|
+
errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
179
|
+
"\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
|
|
180
|
+
)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const { payload } = await jwtVerify(accessToken, jwks, {
|
|
185
|
+
algorithms: [JWT_ALGORITHM],
|
|
186
|
+
issuer: JWT_ISSUER,
|
|
187
|
+
audience: serviceCode,
|
|
188
|
+
clockTolerance: JWT_CLOCK_TOLERANCE_SECONDS
|
|
189
|
+
});
|
|
190
|
+
if (!payload.sub) {
|
|
191
|
+
logger.error("JWT payload missing sub claim");
|
|
192
|
+
return {
|
|
193
|
+
error: createError(
|
|
194
|
+
errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
195
|
+
"JWT\u30DA\u30A4\u30ED\u30FC\u30C9\u306B\u30E6\u30FC\u30B6\u30FCID\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093"
|
|
196
|
+
)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const userInfo = {
|
|
200
|
+
sub: payload.sub,
|
|
201
|
+
email: null,
|
|
202
|
+
name: null,
|
|
203
|
+
picture: null,
|
|
204
|
+
organizationId: payload.org_id ?? null
|
|
205
|
+
// subscription is not available from JWT
|
|
206
|
+
};
|
|
207
|
+
return { data: userInfo };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : "JWT\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F";
|
|
210
|
+
logger.error("JWT verification failed:", message);
|
|
211
|
+
return {
|
|
212
|
+
error: createError(
|
|
213
|
+
errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
214
|
+
`JWT\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${message}`
|
|
215
|
+
)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return verifyJwt;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/client/verify-access-token.ts
|
|
223
|
+
var OAUTH_INTROSPECT_ENDPOINT = "/api/v1/oauth/introspect";
|
|
224
|
+
var CACHE_KEY_PREFIX = "ffid:introspect:";
|
|
225
|
+
var HEX_BASE = 16;
|
|
226
|
+
var HEX_BYTE_WIDTH = 2;
|
|
227
|
+
function createVerifyAccessToken(deps) {
|
|
228
|
+
const { authMode, baseUrl, serviceCode, serviceApiKey, verifyStrategy, logger, createError, errorCodes, cache, timeout } = deps;
|
|
229
|
+
let jwtVerify2 = null;
|
|
230
|
+
function getJwtVerifier() {
|
|
231
|
+
if (!jwtVerify2) {
|
|
232
|
+
jwtVerify2 = createJwtVerifier({
|
|
233
|
+
baseUrl,
|
|
234
|
+
serviceCode,
|
|
235
|
+
logger,
|
|
236
|
+
createError,
|
|
237
|
+
errorCodes: { TOKEN_VERIFICATION_ERROR: errorCodes.TOKEN_VERIFICATION_ERROR }
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return jwtVerify2;
|
|
241
|
+
}
|
|
242
|
+
async function verifyAccessToken(accessToken) {
|
|
243
|
+
if (authMode !== "service-key") {
|
|
244
|
+
return {
|
|
245
|
+
error: createError(
|
|
246
|
+
errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
247
|
+
"verifyAccessToken \u306F service-key \u30E2\u30FC\u30C9\u3067\u306E\u307F\u5229\u7528\u53EF\u80FD\u3067\u3059"
|
|
248
|
+
)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (!accessToken || !accessToken.trim()) {
|
|
252
|
+
return {
|
|
253
|
+
error: createError(
|
|
254
|
+
errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
255
|
+
"\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
|
|
256
|
+
)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (verifyStrategy === "jwt") {
|
|
260
|
+
return getJwtVerifier()(accessToken);
|
|
261
|
+
}
|
|
262
|
+
return verifyViaIntrospect(accessToken);
|
|
263
|
+
}
|
|
264
|
+
async function verifyViaIntrospect(accessToken) {
|
|
265
|
+
if (!serviceApiKey) {
|
|
266
|
+
return {
|
|
267
|
+
error: createError(
|
|
268
|
+
errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
269
|
+
"serviceApiKey \u304C\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
|
|
270
|
+
)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const url = `${baseUrl}${OAUTH_INTROSPECT_ENDPOINT}`;
|
|
274
|
+
logger.debug("Verifying access token:", url);
|
|
275
|
+
const cacheKey = await buildCacheKey(accessToken);
|
|
276
|
+
if (cache) {
|
|
277
|
+
try {
|
|
278
|
+
const cached = await cache.adapter.get(cacheKey);
|
|
279
|
+
if (cached && typeof cached === "object" && "sub" in cached) {
|
|
280
|
+
logger.debug("Cache hit for introspect result");
|
|
281
|
+
return { data: cached };
|
|
282
|
+
}
|
|
283
|
+
} catch (cacheError) {
|
|
284
|
+
logger.warn("Cache read failed, falling back to API call:", cacheError);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const fetchOptions = {
|
|
288
|
+
method: "POST",
|
|
289
|
+
credentials: "omit",
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
292
|
+
"X-Service-Api-Key": serviceApiKey
|
|
293
|
+
},
|
|
294
|
+
body: new URLSearchParams({ token: accessToken }).toString()
|
|
295
|
+
};
|
|
296
|
+
if (timeout !== void 0) {
|
|
297
|
+
fetchOptions.signal = AbortSignal.timeout(timeout);
|
|
298
|
+
}
|
|
299
|
+
let response;
|
|
300
|
+
try {
|
|
301
|
+
response = await fetch(url, fetchOptions);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
logger.error("Network error during token verification:", error);
|
|
304
|
+
return {
|
|
305
|
+
error: {
|
|
306
|
+
code: errorCodes.NETWORK_ERROR,
|
|
307
|
+
message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
let introspectResponse;
|
|
312
|
+
try {
|
|
313
|
+
introspectResponse = await response.json();
|
|
314
|
+
} catch (parseError) {
|
|
315
|
+
logger.error("Parse error during token verification:", parseError);
|
|
316
|
+
return {
|
|
317
|
+
error: {
|
|
318
|
+
code: errorCodes.PARSE_ERROR,
|
|
319
|
+
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})`
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
const errorBody = introspectResponse;
|
|
325
|
+
return {
|
|
326
|
+
error: {
|
|
327
|
+
code: errorBody.error?.code ?? errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
328
|
+
message: errorBody.error?.message ?? "\u30C8\u30FC\u30AF\u30F3\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (!introspectResponse.active) {
|
|
333
|
+
return {
|
|
334
|
+
error: {
|
|
335
|
+
code: errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
336
|
+
message: "\u30C8\u30FC\u30AF\u30F3\u304C\u7121\u52B9\u307E\u305F\u306F\u671F\u9650\u5207\u308C\u3067\u3059"
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (!introspectResponse.sub) {
|
|
341
|
+
logger.error("Active token introspection returned no sub claim");
|
|
342
|
+
return {
|
|
343
|
+
error: {
|
|
344
|
+
code: errorCodes.TOKEN_VERIFICATION_ERROR,
|
|
345
|
+
message: "\u30C8\u30FC\u30AF\u30F3\u691C\u8A3C\u30EC\u30B9\u30DD\u30F3\u30B9\u306B\u30E6\u30FC\u30B6\u30FCID\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093"
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const base = {
|
|
350
|
+
sub: introspectResponse.sub,
|
|
351
|
+
email: introspectResponse.email ?? null,
|
|
352
|
+
name: introspectResponse.name ?? null,
|
|
353
|
+
picture: introspectResponse.picture ?? null,
|
|
354
|
+
organization_id: introspectResponse.organization_id ?? null
|
|
355
|
+
};
|
|
356
|
+
const raw = introspectResponse.subscription ? {
|
|
357
|
+
...base,
|
|
358
|
+
subscription: {
|
|
359
|
+
subscription_id: introspectResponse.subscription.subscription_id ?? null,
|
|
360
|
+
status: introspectResponse.subscription.status,
|
|
361
|
+
plan_code: introspectResponse.subscription.plan_code,
|
|
362
|
+
seat_model: introspectResponse.subscription.seat_model,
|
|
363
|
+
member_role: introspectResponse.subscription.member_role,
|
|
364
|
+
organization_id: introspectResponse.subscription.organization_id
|
|
365
|
+
}
|
|
366
|
+
} : base;
|
|
367
|
+
const userinfo = normalizeUserinfo(raw);
|
|
368
|
+
if (cache) {
|
|
369
|
+
try {
|
|
370
|
+
await cache.adapter.set(cacheKey, userinfo, cache.ttl);
|
|
371
|
+
} catch (cacheError) {
|
|
372
|
+
logger.warn("Cache write failed (result still returned):", cacheError);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return { data: userinfo };
|
|
376
|
+
}
|
|
377
|
+
async function buildCacheKey(token) {
|
|
378
|
+
if (typeof globalThis.crypto?.subtle?.digest === "function") {
|
|
379
|
+
const encoder = new TextEncoder();
|
|
380
|
+
const data = encoder.encode(token);
|
|
381
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
382
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
383
|
+
const hashHex = hashArray.map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, "0")).join("");
|
|
384
|
+
return CACHE_KEY_PREFIX + hashHex;
|
|
385
|
+
}
|
|
386
|
+
return CACHE_KEY_PREFIX + token;
|
|
387
|
+
}
|
|
388
|
+
return verifyAccessToken;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/client/billing-methods.ts
|
|
392
|
+
var BILLING_CHECKOUT_ENDPOINT = "/api/v1/billing/checkout";
|
|
393
|
+
var BILLING_PORTAL_ENDPOINT = "/api/v1/billing/portal";
|
|
394
|
+
function createBillingMethods(deps) {
|
|
395
|
+
const { fetchWithAuth, createError } = deps;
|
|
396
|
+
async function createCheckoutSession(params) {
|
|
397
|
+
if (!params.organizationId || !params.subscriptionId) {
|
|
398
|
+
return {
|
|
399
|
+
error: createError("VALIDATION_ERROR", "organizationId \u3068 subscriptionId \u306F\u5FC5\u9808\u3067\u3059")
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (!params.successUrl || !params.cancelUrl) {
|
|
403
|
+
return {
|
|
404
|
+
error: createError("VALIDATION_ERROR", "successUrl \u3068 cancelUrl \u306F\u5FC5\u9808\u3067\u3059")
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return fetchWithAuth(
|
|
408
|
+
BILLING_CHECKOUT_ENDPOINT,
|
|
409
|
+
{
|
|
410
|
+
method: "POST",
|
|
411
|
+
body: JSON.stringify({
|
|
412
|
+
organizationId: params.organizationId,
|
|
413
|
+
subscriptionId: params.subscriptionId,
|
|
414
|
+
successUrl: params.successUrl,
|
|
415
|
+
cancelUrl: params.cancelUrl,
|
|
416
|
+
...params.planId ? { planId: params.planId } : {}
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
async function createPortalSession(params) {
|
|
422
|
+
if (!params.organizationId || !params.returnUrl) {
|
|
423
|
+
return {
|
|
424
|
+
error: createError("VALIDATION_ERROR", "organizationId \u3068 returnUrl \u306F\u5FC5\u9808\u3067\u3059")
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const query = new URLSearchParams({
|
|
428
|
+
organizationId: params.organizationId,
|
|
429
|
+
returnUrl: params.returnUrl
|
|
430
|
+
});
|
|
431
|
+
return fetchWithAuth(
|
|
432
|
+
`${BILLING_PORTAL_ENDPOINT}?${query.toString()}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return { createCheckoutSession, createPortalSession };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/client/version-check.ts
|
|
439
|
+
var SDK_VERSION = "1.10.0";
|
|
440
|
+
var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
|
|
441
|
+
var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
|
|
442
|
+
var LATEST_VERSION_HEADER = "X-FFID-SDK-Latest-Version";
|
|
443
|
+
var SEMVER_PATTERN = /^\d+(\.\d+)*$/;
|
|
444
|
+
var _versionWarningShown = false;
|
|
445
|
+
function compareSemver(a, b) {
|
|
446
|
+
const cleanA = a.replace(/^v/, "");
|
|
447
|
+
const cleanB = b.replace(/^v/, "");
|
|
448
|
+
if (!SEMVER_PATTERN.test(cleanA) || !SEMVER_PATTERN.test(cleanB)) return 0;
|
|
449
|
+
const partsA = cleanA.split(".").map(Number);
|
|
450
|
+
const partsB = cleanB.split(".").map(Number);
|
|
451
|
+
const maxLen = Math.max(partsA.length, partsB.length);
|
|
452
|
+
for (let i = 0; i < maxLen; i++) {
|
|
453
|
+
const numA = partsA[i] ?? 0;
|
|
454
|
+
const numB = partsB[i] ?? 0;
|
|
455
|
+
if (numA < numB) return -1;
|
|
456
|
+
if (numA > numB) return 1;
|
|
457
|
+
}
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
function checkVersionHeader(response, logger) {
|
|
461
|
+
if (_versionWarningShown) return;
|
|
462
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
463
|
+
if (!response.headers?.get) return;
|
|
464
|
+
const latestVersion = response.headers.get(LATEST_VERSION_HEADER);
|
|
465
|
+
if (!latestVersion) return;
|
|
466
|
+
if (compareSemver(SDK_VERSION, latestVersion) < 0) {
|
|
467
|
+
_versionWarningShown = true;
|
|
468
|
+
logger.warn(
|
|
469
|
+
`\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3\u304C\u5229\u7528\u53EF\u80FD\u3067\u3059: v${latestVersion} (\u73FE\u5728: v${SDK_VERSION})
|
|
470
|
+
npm install @feelflow/ffid-sdk@latest \u3067\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8\u3067\u304D\u307E\u3059`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/client/ffid-client.ts
|
|
476
|
+
var NO_CONTENT_STATUS = 204;
|
|
477
|
+
var SESSION_ENDPOINT = "/api/v1/auth/session";
|
|
478
|
+
var LOGOUT_ENDPOINT = "/api/v1/auth/signout";
|
|
479
|
+
var OAUTH_TOKEN_ENDPOINT = "/api/v1/oauth/token";
|
|
480
|
+
var OAUTH_USERINFO_ENDPOINT = "/api/v1/oauth/userinfo";
|
|
481
|
+
var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
|
|
482
|
+
var OAUTH_REVOKE_ENDPOINT = "/api/v1/oauth/revoke";
|
|
483
|
+
var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
|
|
484
|
+
var SDK_LOG_PREFIX = "[FFID SDK]";
|
|
485
|
+
var MS_PER_SECOND = 1e3;
|
|
486
|
+
var UNAUTHORIZED_STATUS = 401;
|
|
487
|
+
var STATE_RANDOM_BYTES = 16;
|
|
488
|
+
var HEX_BASE2 = 16;
|
|
489
|
+
var noopLogger = {
|
|
490
|
+
debug: () => {
|
|
491
|
+
},
|
|
492
|
+
info: () => {
|
|
493
|
+
},
|
|
494
|
+
warn: () => {
|
|
495
|
+
},
|
|
496
|
+
error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
|
|
497
|
+
};
|
|
498
|
+
var consoleLogger = {
|
|
499
|
+
debug: (...args) => console.debug(SDK_LOG_PREFIX, ...args),
|
|
500
|
+
info: (...args) => console.info(SDK_LOG_PREFIX, ...args),
|
|
501
|
+
warn: (...args) => console.warn(SDK_LOG_PREFIX, ...args),
|
|
502
|
+
error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
|
|
503
|
+
};
|
|
504
|
+
var FFID_ERROR_CODES = {
|
|
505
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
506
|
+
PARSE_ERROR: "PARSE_ERROR",
|
|
507
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
508
|
+
TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
|
|
509
|
+
TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
|
|
510
|
+
NO_TOKENS: "NO_TOKENS",
|
|
511
|
+
TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR"
|
|
512
|
+
};
|
|
513
|
+
function createFFIDClient(config) {
|
|
514
|
+
if (!config.serviceCode || !config.serviceCode.trim()) {
|
|
515
|
+
throw new Error("FFID Client: serviceCode \u304C\u672A\u8A2D\u5B9A\u3067\u3059");
|
|
516
|
+
}
|
|
517
|
+
const baseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
518
|
+
const authMode = config.authMode ?? "cookie";
|
|
519
|
+
const clientId = config.clientId ?? config.serviceCode;
|
|
520
|
+
const resolvedRedirectUri = config.redirectUri ?? null;
|
|
521
|
+
const serviceApiKey = config.serviceApiKey?.trim();
|
|
522
|
+
const verifyStrategy = config.verifyStrategy ?? "jwt";
|
|
523
|
+
const cache = config.cache;
|
|
524
|
+
const timeout = config.timeout;
|
|
525
|
+
if (authMode === "service-key" && !serviceApiKey) {
|
|
526
|
+
throw new Error("FFID Client: service-key \u30E2\u30FC\u30C9\u3067\u306F serviceApiKey \u304C\u5FC5\u9808\u3067\u3059");
|
|
527
|
+
}
|
|
528
|
+
if (cache && cache.ttl <= 0) {
|
|
529
|
+
throw new Error("FFID Client: cache.ttl \u306F\u6B63\u306E\u6570\u5024\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
530
|
+
}
|
|
531
|
+
if (timeout !== void 0 && timeout <= 0) {
|
|
532
|
+
throw new Error("FFID Client: timeout \u306F\u6B63\u306E\u6570\u5024\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
|
|
533
|
+
}
|
|
534
|
+
const logger = config.logger ?? (config.debug ? consoleLogger : noopLogger);
|
|
535
|
+
const tokenStore = authMode === "token" ? createTokenStore() : createTokenStore("memory");
|
|
536
|
+
async function fetchWithAuth(endpoint, options = {}) {
|
|
537
|
+
const url = `${baseUrl}${endpoint}`;
|
|
538
|
+
logger.debug("Fetching:", url);
|
|
539
|
+
const fetchOptions = buildFetchOptions(options);
|
|
540
|
+
let response;
|
|
541
|
+
try {
|
|
542
|
+
response = await fetch(url, fetchOptions);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
logger.error("Network error:", error);
|
|
545
|
+
return {
|
|
546
|
+
error: {
|
|
547
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
548
|
+
message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (authMode === "token" && response.status === UNAUTHORIZED_STATUS) {
|
|
553
|
+
const refreshResult = await refreshAccessToken();
|
|
554
|
+
if (!refreshResult.error) {
|
|
555
|
+
logger.debug("Token refreshed, retrying request");
|
|
556
|
+
const retryOptions = buildFetchOptions(options);
|
|
557
|
+
try {
|
|
558
|
+
response = await fetch(url, retryOptions);
|
|
559
|
+
} catch (retryError) {
|
|
560
|
+
logger.error("Network error on retry:", retryError);
|
|
561
|
+
return {
|
|
562
|
+
error: {
|
|
563
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
564
|
+
message: retryError instanceof Error ? retryError.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
let raw;
|
|
571
|
+
try {
|
|
572
|
+
raw = await response.json();
|
|
573
|
+
} catch (parseError) {
|
|
574
|
+
logger.error("Parse error:", parseError, "Status:", response.status);
|
|
575
|
+
return {
|
|
576
|
+
error: {
|
|
577
|
+
code: FFID_ERROR_CODES.PARSE_ERROR,
|
|
578
|
+
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})`
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
logger.debug("Response:", response.status, raw);
|
|
583
|
+
checkVersionHeader(response, logger);
|
|
584
|
+
if (!response.ok) {
|
|
585
|
+
return {
|
|
586
|
+
error: raw.error ?? {
|
|
587
|
+
code: FFID_ERROR_CODES.UNKNOWN_ERROR,
|
|
588
|
+
message: "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (raw.data === void 0) {
|
|
593
|
+
return {
|
|
594
|
+
error: {
|
|
595
|
+
code: FFID_ERROR_CODES.UNKNOWN_ERROR,
|
|
596
|
+
message: "\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u30C7\u30FC\u30BF\u304C\u8FD4\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F"
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return { data: raw.data };
|
|
601
|
+
}
|
|
602
|
+
function sdkHeaders() {
|
|
603
|
+
return {
|
|
604
|
+
"User-Agent": SDK_USER_AGENT,
|
|
605
|
+
[SDK_VERSION_HEADER]: SDK_VERSION
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function buildFetchOptions(options) {
|
|
609
|
+
if (authMode === "service-key") {
|
|
610
|
+
return {
|
|
611
|
+
...options,
|
|
612
|
+
credentials: "omit",
|
|
613
|
+
headers: {
|
|
614
|
+
"Content-Type": "application/json",
|
|
615
|
+
...sdkHeaders(),
|
|
616
|
+
"X-Service-Api-Key": serviceApiKey,
|
|
617
|
+
...options.headers
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (authMode === "token") {
|
|
622
|
+
const tokens = tokenStore.getTokens();
|
|
623
|
+
const headers = {
|
|
624
|
+
"Content-Type": "application/json",
|
|
625
|
+
...sdkHeaders(),
|
|
626
|
+
...options.headers
|
|
627
|
+
};
|
|
628
|
+
if (tokens) {
|
|
629
|
+
headers["Authorization"] = `Bearer ${tokens.accessToken}`;
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
...options,
|
|
633
|
+
credentials: "omit",
|
|
634
|
+
headers
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
...options,
|
|
639
|
+
credentials: "include",
|
|
640
|
+
headers: {
|
|
641
|
+
"Content-Type": "application/json",
|
|
642
|
+
...sdkHeaders(),
|
|
643
|
+
...options.headers
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async function getSession() {
|
|
648
|
+
if (authMode === "token") {
|
|
649
|
+
return getSessionFromUserinfo();
|
|
650
|
+
}
|
|
651
|
+
const result = await fetchWithAuth(SESSION_ENDPOINT);
|
|
652
|
+
if (result.data?.user) {
|
|
653
|
+
return {
|
|
654
|
+
...result,
|
|
655
|
+
data: {
|
|
656
|
+
...result.data,
|
|
657
|
+
user: normalizeFFIDUser(result.data.user)
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
function normalizeFFIDUser(user) {
|
|
664
|
+
return {
|
|
665
|
+
...user,
|
|
666
|
+
locale: user.locale ?? null,
|
|
667
|
+
timezone: user.timezone ?? null
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async function getSessionFromUserinfo() {
|
|
671
|
+
const tokens = tokenStore.getTokens();
|
|
672
|
+
if (!tokens) {
|
|
673
|
+
return {
|
|
674
|
+
error: {
|
|
675
|
+
code: FFID_ERROR_CODES.NO_TOKENS,
|
|
676
|
+
message: "\u30C8\u30FC\u30AF\u30F3\u304C\u4FDD\u5B58\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const url = `${baseUrl}${OAUTH_USERINFO_ENDPOINT}`;
|
|
681
|
+
logger.debug("Fetching userinfo:", url);
|
|
682
|
+
let response;
|
|
683
|
+
try {
|
|
684
|
+
response = await fetch(url, {
|
|
685
|
+
credentials: "omit",
|
|
686
|
+
headers: {
|
|
687
|
+
"Authorization": `Bearer ${tokens.accessToken}`,
|
|
688
|
+
"Content-Type": "application/json",
|
|
689
|
+
...sdkHeaders()
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
} catch (error) {
|
|
693
|
+
logger.error("Network error:", error);
|
|
694
|
+
return {
|
|
695
|
+
error: {
|
|
696
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
697
|
+
message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (response.status === UNAUTHORIZED_STATUS) {
|
|
702
|
+
const refreshResult = await refreshAccessToken();
|
|
703
|
+
if (!refreshResult.error) {
|
|
704
|
+
const retryTokens = tokenStore.getTokens();
|
|
705
|
+
if (retryTokens) {
|
|
706
|
+
try {
|
|
707
|
+
response = await fetch(url, {
|
|
708
|
+
credentials: "omit",
|
|
709
|
+
headers: {
|
|
710
|
+
"Authorization": `Bearer ${retryTokens.accessToken}`,
|
|
711
|
+
"Content-Type": "application/json",
|
|
712
|
+
...sdkHeaders()
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
} catch (retryError) {
|
|
716
|
+
logger.error("Network error on retry:", retryError);
|
|
717
|
+
return {
|
|
718
|
+
error: {
|
|
719
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
720
|
+
message: retryError instanceof Error ? retryError.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
return refreshResult;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
let rawUserinfo;
|
|
730
|
+
try {
|
|
731
|
+
rawUserinfo = await response.json();
|
|
732
|
+
} catch (parseError) {
|
|
733
|
+
logger.error("Parse error:", parseError, "Status:", response.status);
|
|
734
|
+
return {
|
|
735
|
+
error: {
|
|
736
|
+
code: FFID_ERROR_CODES.PARSE_ERROR,
|
|
737
|
+
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})`
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (!response.ok) {
|
|
742
|
+
const errorBody = rawUserinfo;
|
|
743
|
+
return {
|
|
744
|
+
error: {
|
|
745
|
+
code: errorBody.code ?? FFID_ERROR_CODES.UNKNOWN_ERROR,
|
|
746
|
+
message: errorBody.message ?? "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const userinfo = normalizeUserinfo(rawUserinfo);
|
|
751
|
+
const user = {
|
|
752
|
+
id: userinfo.sub,
|
|
753
|
+
email: userinfo.email ?? "",
|
|
754
|
+
displayName: userinfo.name ?? null,
|
|
755
|
+
avatarUrl: userinfo.picture ?? null,
|
|
756
|
+
locale: null,
|
|
757
|
+
timezone: null,
|
|
758
|
+
createdAt: ""
|
|
759
|
+
};
|
|
760
|
+
return {
|
|
761
|
+
data: {
|
|
762
|
+
user,
|
|
763
|
+
organizations: [],
|
|
764
|
+
subscriptions: mapUserinfoSubscriptionToSession(userinfo, config.serviceCode)
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function signOut() {
|
|
769
|
+
if (authMode === "token") {
|
|
770
|
+
return signOutToken();
|
|
771
|
+
}
|
|
772
|
+
return signOutCookie();
|
|
773
|
+
}
|
|
774
|
+
async function signOutCookie() {
|
|
775
|
+
const url = `${baseUrl}${LOGOUT_ENDPOINT}`;
|
|
776
|
+
logger.debug("Fetching:", url);
|
|
777
|
+
let response;
|
|
778
|
+
try {
|
|
779
|
+
response = await fetch(url, {
|
|
780
|
+
method: "POST",
|
|
781
|
+
credentials: "include",
|
|
782
|
+
headers: { "Content-Type": "application/json", ...sdkHeaders() }
|
|
783
|
+
});
|
|
784
|
+
} catch (error) {
|
|
785
|
+
logger.error("Network error:", error);
|
|
786
|
+
return {
|
|
787
|
+
error: {
|
|
788
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
789
|
+
message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (response.status === NO_CONTENT_STATUS) {
|
|
794
|
+
logger.debug("Response: 204 No Content");
|
|
795
|
+
return { data: void 0 };
|
|
796
|
+
}
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
try {
|
|
799
|
+
const raw = await response.json();
|
|
800
|
+
return {
|
|
801
|
+
error: raw.error ?? {
|
|
802
|
+
code: FFID_ERROR_CODES.UNKNOWN_ERROR,
|
|
803
|
+
message: "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
} catch {
|
|
807
|
+
return {
|
|
808
|
+
error: {
|
|
809
|
+
code: FFID_ERROR_CODES.PARSE_ERROR,
|
|
810
|
+
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})`
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
logger.debug("Response:", response.status);
|
|
816
|
+
return { data: void 0 };
|
|
817
|
+
}
|
|
818
|
+
async function signOutToken() {
|
|
819
|
+
const tokens = tokenStore.getTokens();
|
|
820
|
+
tokenStore.clearTokens();
|
|
821
|
+
if (!tokens) {
|
|
822
|
+
logger.debug("No tokens to revoke");
|
|
823
|
+
return { data: void 0 };
|
|
824
|
+
}
|
|
825
|
+
const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
|
|
826
|
+
logger.debug("Revoking token:", url);
|
|
827
|
+
try {
|
|
828
|
+
await fetch(url, {
|
|
829
|
+
method: "POST",
|
|
830
|
+
credentials: "omit",
|
|
831
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
|
|
832
|
+
body: new URLSearchParams({
|
|
833
|
+
token: tokens.accessToken,
|
|
834
|
+
client_id: clientId
|
|
835
|
+
}).toString()
|
|
836
|
+
});
|
|
837
|
+
} catch (error) {
|
|
838
|
+
logger.warn("Token revocation failed:", error);
|
|
839
|
+
}
|
|
840
|
+
logger.debug("Token sign-out completed");
|
|
841
|
+
return { data: void 0 };
|
|
842
|
+
}
|
|
843
|
+
async function exchangeCodeForTokens(code, codeVerifier) {
|
|
844
|
+
const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
|
|
845
|
+
logger.debug("Exchanging code for tokens:", url);
|
|
846
|
+
const effectiveRedirectUri = resolvedRedirectUri ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : null);
|
|
847
|
+
if (!effectiveRedirectUri) {
|
|
848
|
+
logger.error("redirectUri is required for token exchange in SSR environments. Set config.redirectUri explicitly.");
|
|
849
|
+
return {
|
|
850
|
+
error: {
|
|
851
|
+
code: FFID_ERROR_CODES.TOKEN_EXCHANGE_ERROR,
|
|
852
|
+
message: "redirectUri \u304C\u672A\u8A2D\u5B9A\u3067\u3059\u3002SSR\u74B0\u5883\u3067\u306F config.redirectUri \u3092\u660E\u793A\u7684\u306B\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
const body = {
|
|
857
|
+
grant_type: "authorization_code",
|
|
858
|
+
code,
|
|
859
|
+
client_id: clientId,
|
|
860
|
+
redirect_uri: effectiveRedirectUri
|
|
861
|
+
};
|
|
862
|
+
if (codeVerifier) {
|
|
863
|
+
body.code_verifier = codeVerifier;
|
|
864
|
+
}
|
|
865
|
+
let response;
|
|
866
|
+
try {
|
|
867
|
+
response = await fetch(url, {
|
|
868
|
+
method: "POST",
|
|
869
|
+
credentials: "omit",
|
|
870
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
|
|
871
|
+
body: new URLSearchParams(body).toString()
|
|
872
|
+
});
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logger.error("Network error during token exchange:", error);
|
|
875
|
+
return {
|
|
876
|
+
error: {
|
|
877
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
878
|
+
message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
let tokenResponse;
|
|
883
|
+
try {
|
|
884
|
+
tokenResponse = await response.json();
|
|
885
|
+
} catch (parseError) {
|
|
886
|
+
logger.error("Parse error during token exchange:", parseError);
|
|
887
|
+
return {
|
|
888
|
+
error: {
|
|
889
|
+
code: FFID_ERROR_CODES.PARSE_ERROR,
|
|
890
|
+
message: `\u30C8\u30FC\u30AF\u30F3\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F (status: ${response.status})`
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
if (!response.ok) {
|
|
895
|
+
const errorBody = tokenResponse;
|
|
896
|
+
return {
|
|
897
|
+
error: {
|
|
898
|
+
code: errorBody.error ?? FFID_ERROR_CODES.TOKEN_EXCHANGE_ERROR,
|
|
899
|
+
message: errorBody.error_description ?? "\u30C8\u30FC\u30AF\u30F3\u4EA4\u63DB\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
tokenStore.setTokens({
|
|
904
|
+
accessToken: tokenResponse.access_token,
|
|
905
|
+
refreshToken: tokenResponse.refresh_token,
|
|
906
|
+
expiresAt: Date.now() + tokenResponse.expires_in * MS_PER_SECOND
|
|
907
|
+
});
|
|
908
|
+
logger.debug("Token exchange successful");
|
|
909
|
+
return { data: void 0 };
|
|
910
|
+
}
|
|
911
|
+
async function refreshAccessToken() {
|
|
912
|
+
const tokens = tokenStore.getTokens();
|
|
913
|
+
if (!tokens) {
|
|
914
|
+
return {
|
|
915
|
+
error: {
|
|
916
|
+
code: FFID_ERROR_CODES.NO_TOKENS,
|
|
917
|
+
message: "\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u30C8\u30FC\u30AF\u30F3\u304C\u3042\u308A\u307E\u305B\u3093"
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
|
|
922
|
+
logger.debug("Refreshing access token:", url);
|
|
923
|
+
let response;
|
|
924
|
+
try {
|
|
925
|
+
response = await fetch(url, {
|
|
926
|
+
method: "POST",
|
|
927
|
+
credentials: "omit",
|
|
928
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
|
|
929
|
+
body: new URLSearchParams({
|
|
930
|
+
grant_type: "refresh_token",
|
|
931
|
+
refresh_token: tokens.refreshToken,
|
|
932
|
+
client_id: clientId
|
|
933
|
+
}).toString()
|
|
934
|
+
});
|
|
935
|
+
} catch (error) {
|
|
936
|
+
logger.error("Network error during token refresh:", error);
|
|
937
|
+
return {
|
|
938
|
+
error: {
|
|
939
|
+
code: FFID_ERROR_CODES.NETWORK_ERROR,
|
|
940
|
+
message: error instanceof Error ? error.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
let tokenResponse;
|
|
945
|
+
try {
|
|
946
|
+
tokenResponse = await response.json();
|
|
947
|
+
} catch (parseError) {
|
|
948
|
+
logger.error("Parse error during token refresh:", parseError);
|
|
949
|
+
return {
|
|
950
|
+
error: {
|
|
951
|
+
code: FFID_ERROR_CODES.PARSE_ERROR,
|
|
952
|
+
message: `\u30C8\u30FC\u30AF\u30F3\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F (status: ${response.status})`
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
if (!response.ok) {
|
|
957
|
+
const errorBody = tokenResponse;
|
|
958
|
+
logger.error("Token refresh failed:", errorBody);
|
|
959
|
+
const irrecoverableErrors = ["token_revoked", "invalid_grant"];
|
|
960
|
+
if (errorBody.error && irrecoverableErrors.includes(errorBody.error)) {
|
|
961
|
+
tokenStore.clearTokens();
|
|
962
|
+
logger.debug("Cleared tokens due to irrecoverable refresh error:", errorBody.error);
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
error: {
|
|
966
|
+
code: errorBody.error ?? FFID_ERROR_CODES.TOKEN_REFRESH_ERROR,
|
|
967
|
+
message: errorBody.error_description ?? "\u30C8\u30FC\u30AF\u30F3\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
tokenStore.setTokens({
|
|
972
|
+
accessToken: tokenResponse.access_token,
|
|
973
|
+
refreshToken: tokenResponse.refresh_token,
|
|
974
|
+
expiresAt: Date.now() + tokenResponse.expires_in * MS_PER_SECOND
|
|
975
|
+
});
|
|
976
|
+
logger.debug("Token refresh successful");
|
|
977
|
+
return { data: void 0 };
|
|
978
|
+
}
|
|
979
|
+
function redirectToLogin() {
|
|
980
|
+
if (typeof window === "undefined") {
|
|
981
|
+
logger.debug("Cannot redirect in SSR context");
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
if (authMode === "token") {
|
|
985
|
+
return redirectToAuthorize();
|
|
986
|
+
}
|
|
987
|
+
const currentUrl = window.location.href;
|
|
988
|
+
const loginUrl = `${baseUrl}/login?redirect=${encodeURIComponent(currentUrl)}&service=${encodeURIComponent(config.serviceCode)}`;
|
|
989
|
+
logger.debug("Redirecting to login:", loginUrl);
|
|
990
|
+
window.location.href = loginUrl;
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
function redirectToAuthorize() {
|
|
994
|
+
const verifier = generateCodeVerifier();
|
|
995
|
+
storeCodeVerifier(verifier);
|
|
996
|
+
generateCodeChallenge(verifier).then((challenge) => {
|
|
997
|
+
const state = generateRandomState();
|
|
998
|
+
const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
|
|
999
|
+
const params = new URLSearchParams({
|
|
1000
|
+
response_type: "code",
|
|
1001
|
+
client_id: clientId,
|
|
1002
|
+
redirect_uri: redirectUri,
|
|
1003
|
+
state,
|
|
1004
|
+
code_challenge: challenge,
|
|
1005
|
+
code_challenge_method: "S256"
|
|
1006
|
+
});
|
|
1007
|
+
const authorizeUrl = `${baseUrl}${OAUTH_AUTHORIZE_ENDPOINT}?${params.toString()}`;
|
|
1008
|
+
logger.debug("Redirecting to authorize:", authorizeUrl);
|
|
1009
|
+
window.location.href = authorizeUrl;
|
|
1010
|
+
}).catch((error) => {
|
|
1011
|
+
logger.error("Failed to generate code challenge:", error);
|
|
1012
|
+
});
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
function getLoginUrl(redirectUrl) {
|
|
1016
|
+
const redirect = redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "");
|
|
1017
|
+
return `${baseUrl}/login?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(config.serviceCode)}`;
|
|
1018
|
+
}
|
|
1019
|
+
function getSignupUrl(redirectUrl) {
|
|
1020
|
+
const redirect = redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "");
|
|
1021
|
+
return `${baseUrl}/signup?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(config.serviceCode)}`;
|
|
1022
|
+
}
|
|
1023
|
+
function createError(code, message) {
|
|
1024
|
+
return { code, message };
|
|
1025
|
+
}
|
|
1026
|
+
async function checkSubscription(params) {
|
|
1027
|
+
if (!params.userId || !params.organizationId) {
|
|
1028
|
+
return {
|
|
1029
|
+
error: createError("VALIDATION_ERROR", "userId \u3068 organizationId \u306F\u5FC5\u9808\u3067\u3059")
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const query = new URLSearchParams({
|
|
1033
|
+
userId: params.userId,
|
|
1034
|
+
organizationId: params.organizationId,
|
|
1035
|
+
serviceCode: config.serviceCode
|
|
1036
|
+
});
|
|
1037
|
+
return fetchWithAuth(
|
|
1038
|
+
`${EXT_CHECK_ENDPOINT}?${query.toString()}`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
const { createCheckoutSession, createPortalSession } = createBillingMethods({
|
|
1042
|
+
fetchWithAuth,
|
|
1043
|
+
createError
|
|
1044
|
+
});
|
|
1045
|
+
const verifyAccessToken = createVerifyAccessToken({
|
|
1046
|
+
authMode,
|
|
1047
|
+
baseUrl,
|
|
1048
|
+
serviceCode: config.serviceCode,
|
|
1049
|
+
serviceApiKey,
|
|
1050
|
+
verifyStrategy,
|
|
1051
|
+
logger,
|
|
1052
|
+
createError,
|
|
1053
|
+
errorCodes: FFID_ERROR_CODES,
|
|
1054
|
+
cache,
|
|
1055
|
+
timeout
|
|
1056
|
+
});
|
|
1057
|
+
return {
|
|
1058
|
+
getSession,
|
|
1059
|
+
signOut,
|
|
1060
|
+
redirectToLogin,
|
|
1061
|
+
getLoginUrl,
|
|
1062
|
+
getSignupUrl,
|
|
1063
|
+
createError,
|
|
1064
|
+
exchangeCodeForTokens,
|
|
1065
|
+
refreshAccessToken,
|
|
1066
|
+
checkSubscription,
|
|
1067
|
+
createCheckoutSession,
|
|
1068
|
+
createPortalSession,
|
|
1069
|
+
verifyAccessToken,
|
|
1070
|
+
/** Token store (token mode only) */
|
|
1071
|
+
tokenStore,
|
|
1072
|
+
/** Resolved auth mode */
|
|
1073
|
+
authMode,
|
|
1074
|
+
/** Resolved logger instance */
|
|
1075
|
+
logger,
|
|
1076
|
+
baseUrl,
|
|
1077
|
+
serviceCode: config.serviceCode,
|
|
1078
|
+
clientId,
|
|
1079
|
+
redirectUri: resolvedRedirectUri
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function generateRandomState() {
|
|
1083
|
+
const array = new Uint8Array(STATE_RANDOM_BYTES);
|
|
1084
|
+
crypto.getRandomValues(array);
|
|
1085
|
+
return Array.from(array, (byte) => byte.toString(HEX_BASE2).padStart(2, "0")).join("");
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/client/cache/memory-cache-adapter.ts
|
|
1089
|
+
var MS_PER_SECOND2 = 1e3;
|
|
1090
|
+
function createMemoryCacheAdapter() {
|
|
1091
|
+
const store = /* @__PURE__ */ new Map();
|
|
1092
|
+
return {
|
|
1093
|
+
async get(key) {
|
|
1094
|
+
const entry = store.get(key);
|
|
1095
|
+
if (!entry) return null;
|
|
1096
|
+
if (Date.now() >= entry.expiresAt) {
|
|
1097
|
+
store.delete(key);
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
return entry.value;
|
|
1101
|
+
},
|
|
1102
|
+
async set(key, value, ttlSeconds) {
|
|
1103
|
+
store.set(key, {
|
|
1104
|
+
value,
|
|
1105
|
+
expiresAt: Date.now() + ttlSeconds * MS_PER_SECOND2
|
|
1106
|
+
});
|
|
1107
|
+
},
|
|
1108
|
+
async delete(key) {
|
|
1109
|
+
store.delete(key);
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/client/cache/kv-cache-adapter.ts
|
|
1115
|
+
function createKVCacheAdapter(kv) {
|
|
1116
|
+
return {
|
|
1117
|
+
async get(key) {
|
|
1118
|
+
return kv.get(key, "json");
|
|
1119
|
+
},
|
|
1120
|
+
async set(key, value, ttlSeconds) {
|
|
1121
|
+
await kv.put(key, JSON.stringify(value), { expirationTtl: ttlSeconds });
|
|
1122
|
+
},
|
|
1123
|
+
async delete(key) {
|
|
1124
|
+
await kv.delete(key);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
export { createFFIDClient, createKVCacheAdapter, createMemoryCacheAdapter, createTokenStore, createVerifyAccessToken };
|