@feelflow/ffid-sdk 5.10.0 → 5.13.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.
@@ -118,6 +118,9 @@ function normalizeUserinfo(raw) {
118
118
  return {
119
119
  sub: raw.sub,
120
120
  email: raw.email,
121
+ // #3791: undefined → undefined を保つ(false に丸めない)。古い FFID は wire に
122
+ // 含めないため、consumer は undefined を「未確認」ではなく「不明」として扱える。
123
+ emailVerified: raw.email_verified,
121
124
  name: raw.name,
122
125
  picture: raw.picture,
123
126
  companyName: raw.company_name ?? null,
@@ -373,6 +376,9 @@ function createVerifyAccessToken(deps) {
373
376
  const base = {
374
377
  sub: introspectResponse.sub,
375
378
  email: introspectResponse.email ?? null,
379
+ // #3791: preserve undefined (do NOT coerce to false) so introspect-strategy
380
+ // callers can distinguish "verified" / "not verified" / "older server".
381
+ email_verified: introspectResponse.email_verified,
376
382
  name: introspectResponse.name ?? null,
377
383
  picture: introspectResponse.picture ?? null,
378
384
  company_name: introspectResponse.company_name ?? null,
@@ -430,6 +436,8 @@ function createVerifyAccessToken(deps) {
430
436
  // src/client/billing-methods.ts
431
437
  var BILLING_CHECKOUT_ENDPOINT = "/api/v1/billing/checkout";
432
438
  var BILLING_PORTAL_ENDPOINT = "/api/v1/billing/portal";
439
+ var EXT_INVOICES_ENDPOINT = "/api/v1/billing/ext/invoices";
440
+ var EXT_RETRY_PAYMENT_ENDPOINT = "/api/v1/billing/ext/retry-payment";
433
441
  function createBillingMethods(deps) {
434
442
  const { fetchWithAuth, createError } = deps;
435
443
  async function createCheckoutSession(params) {
@@ -471,7 +479,31 @@ function createBillingMethods(deps) {
471
479
  `${BILLING_PORTAL_ENDPOINT}?${query.toString()}`
472
480
  );
473
481
  }
474
- return { createCheckoutSession, createPortalSession };
482
+ async function listInvoices(params) {
483
+ if (!params.organizationId) {
484
+ return { error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059") };
485
+ }
486
+ const query = new URLSearchParams({ organizationId: params.organizationId });
487
+ return fetchWithAuth(`${EXT_INVOICES_ENDPOINT}?${query.toString()}`);
488
+ }
489
+ async function getInvoice(params) {
490
+ if (!params.invoiceId) {
491
+ return { error: createError("VALIDATION_ERROR", "invoiceId \u306F\u5FC5\u9808\u3067\u3059") };
492
+ }
493
+ return fetchWithAuth(
494
+ `${EXT_INVOICES_ENDPOINT}/${encodeURIComponent(params.invoiceId)}`
495
+ );
496
+ }
497
+ async function retryPayment(params) {
498
+ if (!params.organizationId) {
499
+ return { error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059") };
500
+ }
501
+ return fetchWithAuth(EXT_RETRY_PAYMENT_ENDPOINT, {
502
+ method: "POST",
503
+ body: JSON.stringify({ organizationId: params.organizationId })
504
+ });
505
+ }
506
+ return { createCheckoutSession, createPortalSession, listInvoices, getInvoice, retryPayment };
475
507
  }
476
508
 
477
509
  // src/client/subscription-methods.ts
@@ -813,6 +845,45 @@ function createMembersMethods(deps) {
813
845
  return { listMembers, addMember, updateMemberRole, removeMember };
814
846
  }
815
847
 
848
+ // src/client/seat-methods.ts
849
+ function seatsEndpoint(subscriptionId) {
850
+ return `/api/v1/subscriptions/ext/${encodeURIComponent(subscriptionId)}/seats/assignments`;
851
+ }
852
+ function createSeatMethods(deps) {
853
+ const { fetchWithAuth, createError } = deps;
854
+ async function listSeatAssignments(params) {
855
+ if (!params.subscriptionId) {
856
+ return { error: createError("VALIDATION_ERROR", "subscriptionId \u306F\u5FC5\u9808\u3067\u3059") };
857
+ }
858
+ return fetchWithAuth(seatsEndpoint(params.subscriptionId));
859
+ }
860
+ async function assignSeats(params) {
861
+ if (!params.subscriptionId) {
862
+ return { error: createError("VALIDATION_ERROR", "subscriptionId \u306F\u5FC5\u9808\u3067\u3059") };
863
+ }
864
+ if (!Array.isArray(params.userIds) || params.userIds.length === 0) {
865
+ return { error: createError("VALIDATION_ERROR", "userIds \u306F 1 \u4EF6\u4EE5\u4E0A\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044") };
866
+ }
867
+ return fetchWithAuth(seatsEndpoint(params.subscriptionId), {
868
+ method: "POST",
869
+ body: JSON.stringify({ userIds: params.userIds })
870
+ });
871
+ }
872
+ async function unassignSeat(params) {
873
+ if (!params.subscriptionId) {
874
+ return { error: createError("VALIDATION_ERROR", "subscriptionId \u306F\u5FC5\u9808\u3067\u3059") };
875
+ }
876
+ if (!params.userId) {
877
+ return { error: createError("VALIDATION_ERROR", "userId \u306F\u5FC5\u9808\u3067\u3059") };
878
+ }
879
+ return fetchWithAuth(
880
+ `${seatsEndpoint(params.subscriptionId)}/${encodeURIComponent(params.userId)}`,
881
+ { method: "DELETE" }
882
+ );
883
+ }
884
+ return { listSeatAssignments, assignSeats, unassignSeat };
885
+ }
886
+
816
887
  // src/client/profile-methods.ts
817
888
  var EXT_PROFILE_ENDPOINT = "/api/v1/users/ext/me";
818
889
  function resolveAuthOverride(options, createError) {
@@ -863,7 +934,7 @@ function createProfileMethods(deps) {
863
934
  }
864
935
 
865
936
  // src/client/version-check.ts
866
- var SDK_VERSION = "5.10.0";
937
+ var SDK_VERSION = "5.13.0";
867
938
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
868
939
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
869
940
  function sdkHeaders() {
@@ -935,9 +1006,20 @@ function createOAuthTokenMethods(deps) {
935
1006
  logger,
936
1007
  errorCodes
937
1008
  } = deps;
938
- async function exchangeCodeForTokens(code, codeVerifier) {
1009
+ async function exchangeCodeForTokens(code, codeVerifier, opts) {
939
1010
  const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
940
1011
  logger.debug("Exchanging code for tokens:", url);
1012
+ if (opts?.expectedState !== void 0 || opts?.actualState !== void 0) {
1013
+ if (opts?.expectedState !== opts?.actualState) {
1014
+ logger.error("Token exchange aborted: OAuth state mismatch or omitted (possible CSRF)");
1015
+ return {
1016
+ error: {
1017
+ code: errorCodes.STATE_MISMATCH_ERROR,
1018
+ message: "OAuth state \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002CSRF \u306E\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
1019
+ }
1020
+ };
1021
+ }
1022
+ }
941
1023
  const effectiveRedirectUri = resolvedRedirectUri ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : null);
942
1024
  if (!effectiveRedirectUri) {
943
1025
  logger.error("redirectUri is required for token exchange in SSR environments. Set config.redirectUri explicitly.");
@@ -1382,6 +1464,46 @@ function base64UrlEncode(buffer) {
1382
1464
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1383
1465
  }
1384
1466
 
1467
+ // src/auth/state.ts
1468
+ var STATE_STORAGE_KEY = "ffid_oauth_state";
1469
+ var STATE_FALLBACK_STORAGE_KEY = "ffid_oauth_state_fb";
1470
+ var STATE_FALLBACK_TIMESTAMP_KEY = "ffid_oauth_state_fb_ts";
1471
+ function storeState(state, logger) {
1472
+ if (typeof window === "undefined") {
1473
+ logger?.warn("storeState: storage is not available in SSR context");
1474
+ return false;
1475
+ }
1476
+ let sessionStored = false;
1477
+ try {
1478
+ window.sessionStorage.setItem(STATE_STORAGE_KEY, state);
1479
+ sessionStored = true;
1480
+ } catch (error) {
1481
+ logger?.warn("storeState: sessionStorage \u3078\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1482
+ }
1483
+ let localStored = false;
1484
+ try {
1485
+ window.localStorage.setItem(STATE_FALLBACK_STORAGE_KEY, state);
1486
+ window.localStorage.setItem(STATE_FALLBACK_TIMESTAMP_KEY, String(Date.now()));
1487
+ localStored = true;
1488
+ } catch (error) {
1489
+ logger?.warn("storeState: localStorage fallback \u3078\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1490
+ }
1491
+ return sessionStored || localStored;
1492
+ }
1493
+ function cleanupStateStorage(logger) {
1494
+ try {
1495
+ window.sessionStorage.removeItem(STATE_STORAGE_KEY);
1496
+ } catch (error) {
1497
+ logger?.warn("retrieveState: sessionStorage \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1498
+ }
1499
+ try {
1500
+ window.localStorage.removeItem(STATE_FALLBACK_STORAGE_KEY);
1501
+ window.localStorage.removeItem(STATE_FALLBACK_TIMESTAMP_KEY);
1502
+ } catch (error) {
1503
+ logger?.warn("retrieveState: localStorage \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1504
+ }
1505
+ }
1506
+
1385
1507
  // src/client/redirect.ts
1386
1508
  var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
1387
1509
  var AUTH_LOGOUT_ENDPOINT = "/api/v1/auth/logout";
@@ -1513,6 +1635,7 @@ function createRedirectMethods(deps) {
1513
1635
  const recentCount = getRecentRedirectCount(authorizeKey, now, logger);
1514
1636
  if (recentCount >= REDIRECT_LOOP_THRESHOLD) {
1515
1637
  cleanupVerifierStorage(logger);
1638
+ cleanupStateStorage(logger);
1516
1639
  logger.warn("[FFID SDK] redirect loop detected \u2014 \u81EA\u52D5\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u505C\u6B62\u3057\u307E\u3057\u305F", {
1517
1640
  authorizeKey,
1518
1641
  recentCount,
@@ -1536,6 +1659,11 @@ function createRedirectMethods(deps) {
1536
1659
  return { success: false, error: errorMessage };
1537
1660
  }
1538
1661
  const state = generateRandomState();
1662
+ if (!storeState(state, logger)) {
1663
+ logger.error(
1664
+ "redirectToAuthorize: OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08storage \u5229\u7528\u4E0D\u53EF\uFF09\u3002\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u3067\u306E CSRF \u691C\u8A3C\u304C\u5931\u6557\u3057\u307E\u3059"
1665
+ );
1666
+ }
1539
1667
  const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
1540
1668
  const params = new URLSearchParams({
1541
1669
  response_type: "code",
@@ -2250,7 +2378,8 @@ var FFID_ERROR_CODES = {
2250
2378
  TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
2251
2379
  TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
2252
2380
  NO_TOKENS: "NO_TOKENS",
2253
- TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR"
2381
+ TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR",
2382
+ STATE_MISMATCH_ERROR: "STATE_MISMATCH_ERROR"
2254
2383
  };
2255
2384
  var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2256
2385
  var DEFAULT_ALLOW_GRACE = true;
@@ -2608,7 +2737,11 @@ function createFFIDClient(config) {
2608
2737
  }
2609
2738
  return result;
2610
2739
  }
2611
- const { createCheckoutSession, createPortalSession } = createBillingMethods({
2740
+ const { createCheckoutSession, createPortalSession, listInvoices, getInvoice, retryPayment } = createBillingMethods({
2741
+ fetchWithAuth,
2742
+ createError
2743
+ });
2744
+ const { listSeatAssignments, assignSeats, unassignSeat } = createSeatMethods({
2612
2745
  fetchWithAuth,
2613
2746
  createError
2614
2747
  });
@@ -2717,6 +2850,12 @@ function createFFIDClient(config) {
2717
2850
  getAnalyticsConfig,
2718
2851
  createCheckoutSession,
2719
2852
  createPortalSession,
2853
+ listInvoices,
2854
+ getInvoice,
2855
+ retryPayment,
2856
+ listSeatAssignments,
2857
+ assignSeats,
2858
+ unassignSeat,
2720
2859
  listPlans,
2721
2860
  getSubscription,
2722
2861
  subscribe,
@@ -1,5 +1,5 @@
1
- import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-DRjzfAaS.cjs';
2
- import '../../types-BYdtyO_2.cjs';
1
+ import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-C3K3sAqq.cjs';
2
+ import '../../types-BeVl-z12.cjs';
3
3
 
4
4
  /**
5
5
  * FFID SDK - Test mode client (E2E / integration test bypass)
@@ -1,5 +1,5 @@
1
- import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-gboJROw0.js';
2
- import '../../types-BYdtyO_2.js';
1
+ import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-BG0Qr86U.js';
2
+ import '../../types-BeVl-z12.js';
3
3
 
4
4
  /**
5
5
  * FFID SDK - Test mode client (E2E / integration test bypass)
@@ -49,6 +49,15 @@
49
49
  * @see /api/v1/subscriptions/ext/check
50
50
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
51
51
  */
52
- type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
52
+ declare const EFFECTIVE_SUBSCRIPTION_STATUSES: readonly ["active", "active_canceling", "past_due_grace", "blocked", "canceled", "trial_expired", "expired"];
53
+ /**
54
+ * Derived from {@link EFFECTIVE_SUBSCRIPTION_STATUSES} so the literal set has
55
+ * a runtime representation. The server mirrors the same const in
56
+ * `src/lib/common/subscription-helpers.ts`, and a server-side unit test pins
57
+ * element-equality between the two arrays (#3474) — convention-only sync
58
+ * could drift silently in the server-extending direction (a new server
59
+ * literal would fall through SDK consumers' `switch` defaults).
60
+ */
61
+ type EffectiveSubscriptionStatus = (typeof EFFECTIVE_SUBSCRIPTION_STATUSES)[number];
53
62
 
54
63
  export type { EffectiveSubscriptionStatus as E };
@@ -49,6 +49,15 @@
49
49
  * @see /api/v1/subscriptions/ext/check
50
50
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
51
51
  */
52
- type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
52
+ declare const EFFECTIVE_SUBSCRIPTION_STATUSES: readonly ["active", "active_canceling", "past_due_grace", "blocked", "canceled", "trial_expired", "expired"];
53
+ /**
54
+ * Derived from {@link EFFECTIVE_SUBSCRIPTION_STATUSES} so the literal set has
55
+ * a runtime representation. The server mirrors the same const in
56
+ * `src/lib/common/subscription-helpers.ts`, and a server-side unit test pins
57
+ * element-equality between the two arrays (#3474) — convention-only sync
58
+ * could drift silently in the server-extending direction (a new server
59
+ * literal would fall through SDK consumers' `switch` defaults).
60
+ */
61
+ type EffectiveSubscriptionStatus = (typeof EFFECTIVE_SUBSCRIPTION_STATUSES)[number];
53
62
 
54
63
  export type { EffectiveSubscriptionStatus as E };
@@ -92,6 +92,7 @@ function verifyWebhookSignature(rawBody, signatureHeader, secret, toleranceSecon
92
92
  var SDK_LOG_PREFIX = "[FFID Webhook SDK]";
93
93
  var HTTP_OK = 200;
94
94
  var HTTP_BAD_REQUEST = 400;
95
+ var HTTP_INTERNAL_SERVER_ERROR = 500;
95
96
  var noopLogger = {
96
97
  debug: () => {
97
98
  },
@@ -184,7 +185,8 @@ function createFFIDWebhookHandler(config) {
184
185
  }).catch((error) => {
185
186
  logger.error("Webhook processing failed:", error);
186
187
  const message = error instanceof Error ? error.message : "Webhook processing failed";
187
- res.status(HTTP_BAD_REQUEST).json({ error: message });
188
+ const status = error instanceof FFIDWebhookError ? HTTP_BAD_REQUEST : HTTP_INTERNAL_SERVER_ERROR;
189
+ res.status(status).json({ error: message });
188
190
  });
189
191
  };
190
192
  }
@@ -211,9 +213,10 @@ function createFFIDWebhookHandler(config) {
211
213
  } catch (error) {
212
214
  logger.error("Webhook processing failed:", error);
213
215
  const message = error instanceof Error ? error.message : "Webhook processing failed";
216
+ const status = error instanceof FFIDWebhookError ? HTTP_BAD_REQUEST : HTTP_INTERNAL_SERVER_ERROR;
214
217
  return new Response(
215
218
  JSON.stringify({ error: message }),
216
- { status: HTTP_BAD_REQUEST, headers: { "Content-Type": "application/json" } }
219
+ { status, headers: { "Content-Type": "application/json" } }
217
220
  );
218
221
  }
219
222
  };
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from '../types-BYdtyO_2.cjs';
1
+ import { E as EffectiveSubscriptionStatus } from '../types-BeVl-z12.cjs';
2
2
 
3
3
  /**
4
4
  * FFID Webhook SDK Constants
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from '../types-BYdtyO_2.js';
1
+ import { E as EffectiveSubscriptionStatus } from '../types-BeVl-z12.js';
2
2
 
3
3
  /**
4
4
  * FFID Webhook SDK Constants
@@ -90,6 +90,7 @@ function verifyWebhookSignature(rawBody, signatureHeader, secret, toleranceSecon
90
90
  var SDK_LOG_PREFIX = "[FFID Webhook SDK]";
91
91
  var HTTP_OK = 200;
92
92
  var HTTP_BAD_REQUEST = 400;
93
+ var HTTP_INTERNAL_SERVER_ERROR = 500;
93
94
  var noopLogger = {
94
95
  debug: () => {
95
96
  },
@@ -182,7 +183,8 @@ function createFFIDWebhookHandler(config) {
182
183
  }).catch((error) => {
183
184
  logger.error("Webhook processing failed:", error);
184
185
  const message = error instanceof Error ? error.message : "Webhook processing failed";
185
- res.status(HTTP_BAD_REQUEST).json({ error: message });
186
+ const status = error instanceof FFIDWebhookError ? HTTP_BAD_REQUEST : HTTP_INTERNAL_SERVER_ERROR;
187
+ res.status(status).json({ error: message });
186
188
  });
187
189
  };
188
190
  }
@@ -209,9 +211,10 @@ function createFFIDWebhookHandler(config) {
209
211
  } catch (error) {
210
212
  logger.error("Webhook processing failed:", error);
211
213
  const message = error instanceof Error ? error.message : "Webhook processing failed";
214
+ const status = error instanceof FFIDWebhookError ? HTTP_BAD_REQUEST : HTTP_INTERNAL_SERVER_ERROR;
212
215
  return new Response(
213
216
  JSON.stringify({ error: message }),
214
- { status: HTTP_BAD_REQUEST, headers: { "Content-Type": "application/json" } }
217
+ { status, headers: { "Content-Type": "application/json" } }
215
218
  );
216
219
  }
217
220
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelflow/ffid-sdk",
3
- "version": "5.10.0",
3
+ "version": "5.13.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
5
  "keywords": [
6
6
  "feelflow",