@feelflow/ffid-sdk 4.1.0 → 4.3.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.
@@ -704,6 +704,7 @@ function createSubscriptionMethods(deps) {
704
704
  var EXT_MEMBERS_ENDPOINT = "/api/v1/organizations/ext/members";
705
705
  function createMembersMethods(deps) {
706
706
  const { fetchWithAuth, createError, serviceCode } = deps;
707
+ const assignableRoles = /* @__PURE__ */ new Set(["admin", "member", "viewer"]);
707
708
  function buildQuery(organizationId) {
708
709
  return new URLSearchParams({ organizationId, serviceCode }).toString();
709
710
  }
@@ -717,6 +718,37 @@ function createMembersMethods(deps) {
717
718
  `${EXT_MEMBERS_ENDPOINT}?${buildQuery(params.organizationId)}`
718
719
  );
719
720
  }
721
+ function normalizeAddMemberArgs(paramsOrOrganizationId, data) {
722
+ if (typeof paramsOrOrganizationId === "string") {
723
+ const params = {
724
+ organizationId: paramsOrOrganizationId,
725
+ email: data?.email ?? ""
726
+ };
727
+ if (data?.role !== void 0) params.role = data.role;
728
+ return params;
729
+ }
730
+ return paramsOrOrganizationId;
731
+ }
732
+ async function addMember(paramsOrOrganizationId, data) {
733
+ const params = normalizeAddMemberArgs(paramsOrOrganizationId, data);
734
+ if (!params.organizationId || !params.email) {
735
+ return {
736
+ error: createError("VALIDATION_ERROR", "organizationId \u3068 email \u306F\u5FC5\u9808\u3067\u3059")
737
+ };
738
+ }
739
+ if (params.role !== void 0 && !assignableRoles.has(params.role)) {
740
+ return {
741
+ error: createError("VALIDATION_ERROR", "role \u306F admin\u3001member\u3001viewer \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044")
742
+ };
743
+ }
744
+ return fetchWithAuth(
745
+ `${EXT_MEMBERS_ENDPOINT}?${buildQuery(params.organizationId)}`,
746
+ {
747
+ method: "POST",
748
+ body: JSON.stringify({ email: params.email, role: params.role })
749
+ }
750
+ );
751
+ }
720
752
  async function updateMemberRole(params) {
721
753
  if (!params.organizationId || !params.userId) {
722
754
  return {
@@ -749,7 +781,7 @@ function createMembersMethods(deps) {
749
781
  }
750
782
  );
751
783
  }
752
- return { listMembers, updateMemberRole, removeMember };
784
+ return { listMembers, addMember, updateMemberRole, removeMember };
753
785
  }
754
786
 
755
787
  // src/client/profile-methods.ts
@@ -802,7 +834,7 @@ function createProfileMethods(deps) {
802
834
  }
803
835
 
804
836
  // src/client/version-check.ts
805
- var SDK_VERSION = "4.1.0";
837
+ var SDK_VERSION = "4.3.0";
806
838
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
807
839
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
808
840
  function sdkHeaders() {
@@ -2179,6 +2211,73 @@ var FFID_ERROR_CODES = {
2179
2211
  TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR"
2180
2212
  };
2181
2213
  var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2214
+ var DEFAULT_ALLOW_GRACE = true;
2215
+ var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2216
+ var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2217
+ var ACCESS_GRANTING_EFFECTIVE_STATUSES = ["active", "past_due_grace"];
2218
+ var BLOCKING_EFFECTIVE_STATUSES = [
2219
+ "blocked",
2220
+ "canceled",
2221
+ "expired",
2222
+ "trial_expired"
2223
+ ];
2224
+ function resolveServiceAccessDenialReason(params, allowGrace) {
2225
+ if (params.hasAccess) {
2226
+ return null;
2227
+ }
2228
+ if (params.effectiveStatus === null) {
2229
+ return "no_subscription";
2230
+ }
2231
+ if (params.isGrace && !allowGrace) {
2232
+ return "grace_disallowed";
2233
+ }
2234
+ if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2235
+ return "blocked";
2236
+ }
2237
+ return params.effectiveStatus;
2238
+ }
2239
+ function toServiceAccessDecision(response, allowGrace) {
2240
+ const effectiveStatus = response.effectiveStatus ?? null;
2241
+ const serverHasAccess = response.hasAccess ?? (effectiveStatus !== null && ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(effectiveStatus));
2242
+ const isGrace = response.isGrace ?? effectiveStatus === "past_due_grace";
2243
+ const hasAccess = serverHasAccess && (allowGrace || !isGrace);
2244
+ const isBlocked = response.isBlocked ?? (effectiveStatus === null || effectiveStatus !== null && BLOCKING_EFFECTIVE_STATUSES.includes(effectiveStatus));
2245
+ return {
2246
+ hasAccess,
2247
+ effectiveStatus,
2248
+ isGrace,
2249
+ isBlocked: isBlocked || !hasAccess,
2250
+ allowGrace,
2251
+ failPolicy: DEFAULT_SERVICE_ACCESS_FAIL_POLICY,
2252
+ denialReason: resolveServiceAccessDenialReason({ effectiveStatus, hasAccess, isGrace }, allowGrace),
2253
+ organizationId: response.organizationId,
2254
+ subscriptionId: response.subscriptionId,
2255
+ status: response.status,
2256
+ planCode: response.planCode,
2257
+ currentPeriodEnd: response.currentPeriodEnd,
2258
+ gracePeriodEndsAt: response.gracePeriodEndsAt ?? null,
2259
+ reactivatable: response.reactivatable ?? false
2260
+ };
2261
+ }
2262
+ function failClosedServiceAccessDecision(params, error) {
2263
+ return {
2264
+ hasAccess: false,
2265
+ effectiveStatus: null,
2266
+ isGrace: false,
2267
+ isBlocked: true,
2268
+ allowGrace: params.allowGrace ?? DEFAULT_ALLOW_GRACE,
2269
+ failPolicy: DEFAULT_SERVICE_ACCESS_FAIL_POLICY,
2270
+ denialReason: "ffid_unreachable",
2271
+ organizationId: params.organizationId || null,
2272
+ subscriptionId: null,
2273
+ status: null,
2274
+ planCode: null,
2275
+ currentPeriodEnd: null,
2276
+ gracePeriodEndsAt: null,
2277
+ reactivatable: false,
2278
+ error
2279
+ };
2280
+ }
2182
2281
  function resolveRedirectUri(raw, logger) {
2183
2282
  if (raw === null) return null;
2184
2283
  try {
@@ -2417,6 +2516,57 @@ function createFFIDClient(config) {
2417
2516
  `${EXT_CHECK_ENDPOINT}?${query.toString()}`
2418
2517
  );
2419
2518
  }
2519
+ async function checkServiceAccess(params) {
2520
+ const failPolicy = params.failPolicy ?? DEFAULT_SERVICE_ACCESS_FAIL_POLICY;
2521
+ if (failPolicy !== DEFAULT_SERVICE_ACCESS_FAIL_POLICY) {
2522
+ return {
2523
+ error: createError(
2524
+ "VALIDATION_ERROR",
2525
+ `failPolicy \u306F ${DEFAULT_SERVICE_ACCESS_FAIL_POLICY} \u306E\u307F\u6307\u5B9A\u3067\u304D\u307E\u3059`
2526
+ )
2527
+ };
2528
+ }
2529
+ const subscriptionParams = {
2530
+ organizationId: params.organizationId
2531
+ };
2532
+ if (params.userId !== void 0) {
2533
+ subscriptionParams.userId = params.userId;
2534
+ }
2535
+ const subscriptionResult = await checkSubscription(subscriptionParams);
2536
+ if (subscriptionResult.error) {
2537
+ if (subscriptionResult.error.code === "VALIDATION_ERROR") {
2538
+ return { error: subscriptionResult.error };
2539
+ }
2540
+ return {
2541
+ data: failClosedServiceAccessDecision(params, subscriptionResult.error)
2542
+ };
2543
+ }
2544
+ return {
2545
+ data: toServiceAccessDecision(
2546
+ subscriptionResult.data,
2547
+ params.allowGrace ?? DEFAULT_ALLOW_GRACE
2548
+ )
2549
+ };
2550
+ }
2551
+ async function requireServiceAccess(params) {
2552
+ const result = await checkServiceAccess(params);
2553
+ if (result.error) {
2554
+ return result;
2555
+ }
2556
+ if (!result.data.hasAccess) {
2557
+ const error = createError(
2558
+ SERVICE_ACCESS_DENIED_CODE,
2559
+ `FFID service access denied: ${result.data.denialReason ?? "unknown"}`
2560
+ );
2561
+ if (result.data.error) {
2562
+ error.details = { cause: result.data.error };
2563
+ }
2564
+ return {
2565
+ error
2566
+ };
2567
+ }
2568
+ return result;
2569
+ }
2420
2570
  const { createCheckoutSession, createPortalSession } = createBillingMethods({
2421
2571
  fetchWithAuth,
2422
2572
  createError
@@ -2434,7 +2584,7 @@ function createFFIDClient(config) {
2434
2584
  fetchWithAuth,
2435
2585
  createError
2436
2586
  });
2437
- const { listMembers, updateMemberRole, removeMember } = createMembersMethods({
2587
+ const { listMembers, addMember, updateMemberRole, removeMember } = createMembersMethods({
2438
2588
  fetchWithAuth,
2439
2589
  createError,
2440
2590
  serviceCode: config.serviceCode
@@ -2515,7 +2665,10 @@ function createFFIDClient(config) {
2515
2665
  exchangeCodeForTokens,
2516
2666
  refreshAccessToken,
2517
2667
  checkSubscription,
2668
+ checkServiceAccess,
2669
+ requireServiceAccess,
2518
2670
  listMembers,
2671
+ addMember,
2519
2672
  updateMemberRole,
2520
2673
  removeMember,
2521
2674
  getProfile,
@@ -1,4 +1,5 @@
1
- import { g as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-ZcJhRbD6.cjs';
1
+ import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-DgprK2ec.cjs';
2
+ import '../../types-5g_Bg6Ey.cjs';
2
3
 
3
4
  /**
4
5
  * FFID SDK - Test mode client (E2E / integration test bypass)
@@ -1,4 +1,5 @@
1
- import { g as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-ZcJhRbD6.js';
1
+ import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-BaStAONh.js';
2
+ import '../../types-5g_Bg6Ey.js';
2
3
 
3
4
  /**
4
5
  * FFID SDK - Test mode client (E2E / integration test bypass)
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Subscription access-control types (Chain of Pacts / Issue #2443).
3
+ *
4
+ * Defines `EffectiveSubscriptionStatus` — the semantic status surfaced by
5
+ * `/api/v1/subscriptions/ext/check` for external services that need a single
6
+ * value to drive access-control decisions. Mirrors the server-side type in
7
+ * `src/lib/common/subscription-helpers.ts` so the FFID backend, SDK, and
8
+ * consuming services agree on the same vocabulary.
9
+ */
10
+ /**
11
+ * Semantic subscription status used for access-control decisions by external
12
+ * services.
13
+ *
14
+ * Unlike the raw DB `FFIDSubscriptionStatus` (which keeps the Stripe-aligned
15
+ * status machine, e.g. `past_due` / `incomplete` / `incomplete_expired`), this
16
+ * narrowed union flattens dunning-window vs. hard-block cases so callers can
17
+ * branch on a single value:
18
+ *
19
+ * - `active` — normal paying subscription; grant full access.
20
+ * - `past_due_grace` — payment failed but FFID is still retrying. Service
21
+ * SHOULD keep access and surface a recovery banner.
22
+ * - `blocked` — payment dunning exhausted (`past_due` over grace,
23
+ * `unpaid`, `incomplete_expired`, `paused`, `incomplete`). Deny access.
24
+ * - `canceled` — contract ended (voluntary or auto-cancel). Deny access;
25
+ * check the accompanying `reactivatable` flag before showing a restart CTA.
26
+ * - `trial_expired` — `status = 'trialing'` but the trial end is in the past.
27
+ * DB row is still `trialing`; deny access and prompt for a paid plan.
28
+ * - `expired` — `status = 'active'` but `current_period_end` is in the past.
29
+ * This is the FFID-internal runtime-expired case (see
30
+ * `getEffectiveSubscriptionStatus` on the server). Deny access.
31
+ *
32
+ * @see /api/v1/subscriptions/ext/check
33
+ * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
34
+ */
35
+ type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
36
+
37
+ export type { EffectiveSubscriptionStatus as E };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Subscription access-control types (Chain of Pacts / Issue #2443).
3
+ *
4
+ * Defines `EffectiveSubscriptionStatus` — the semantic status surfaced by
5
+ * `/api/v1/subscriptions/ext/check` for external services that need a single
6
+ * value to drive access-control decisions. Mirrors the server-side type in
7
+ * `src/lib/common/subscription-helpers.ts` so the FFID backend, SDK, and
8
+ * consuming services agree on the same vocabulary.
9
+ */
10
+ /**
11
+ * Semantic subscription status used for access-control decisions by external
12
+ * services.
13
+ *
14
+ * Unlike the raw DB `FFIDSubscriptionStatus` (which keeps the Stripe-aligned
15
+ * status machine, e.g. `past_due` / `incomplete` / `incomplete_expired`), this
16
+ * narrowed union flattens dunning-window vs. hard-block cases so callers can
17
+ * branch on a single value:
18
+ *
19
+ * - `active` — normal paying subscription; grant full access.
20
+ * - `past_due_grace` — payment failed but FFID is still retrying. Service
21
+ * SHOULD keep access and surface a recovery banner.
22
+ * - `blocked` — payment dunning exhausted (`past_due` over grace,
23
+ * `unpaid`, `incomplete_expired`, `paused`, `incomplete`). Deny access.
24
+ * - `canceled` — contract ended (voluntary or auto-cancel). Deny access;
25
+ * check the accompanying `reactivatable` flag before showing a restart CTA.
26
+ * - `trial_expired` — `status = 'trialing'` but the trial end is in the past.
27
+ * DB row is still `trialing`; deny access and prompt for a paid plan.
28
+ * - `expired` — `status = 'active'` but `current_period_end` is in the past.
29
+ * This is the FFID-internal runtime-expired case (see
30
+ * `getEffectiveSubscriptionStatus` on the server). Deny access.
31
+ *
32
+ * @see /api/v1/subscriptions/ext/check
33
+ * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
34
+ */
35
+ type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
36
+
37
+ export type { EffectiveSubscriptionStatus as E };
@@ -1,3 +1,5 @@
1
+ import { E as EffectiveSubscriptionStatus } from '../types-5g_Bg6Ey.cjs';
2
+
1
3
  /**
2
4
  * FFID Webhook SDK Constants
3
5
  *
@@ -43,6 +45,7 @@ declare class FFIDWebhookPayloadError extends FFIDWebhookError {
43
45
  * Types for receiving and verifying FeelFlow ID webhook events.
44
46
  * Used by external services to handle real-time event notifications.
45
47
  */
48
+
46
49
  /** All supported webhook event types */
47
50
  type FFIDWebhookEventType = 'subscription.created' | 'subscription.updated' | 'subscription.canceled' | 'subscription.downgrade_scheduled' | 'subscription.downgrade_applied' | 'subscription.downgrade_canceled' | 'subscription.trial_ending' | 'subscription.payment_failed' | 'user.created' | 'user.updated' | 'user.deleted' | 'user.deletion_requested' | 'organization.created' | 'organization.updated' | 'organization.member.added' | 'organization.member.removed' | 'organization.member.role_changed' | 'legal.document.updated' | 'legal.agreement.required' | 'system.maintenance.scheduled' | 'system.maintenance.started' | 'system.maintenance.completed' | 'announcement.published' | 'test.ping';
48
51
  interface FFIDSubscriptionCreatedPayload {
@@ -50,6 +53,8 @@ interface FFIDSubscriptionCreatedPayload {
50
53
  organizationId?: string;
51
54
  plan?: string;
52
55
  status?: string;
56
+ effectiveStatus?: EffectiveSubscriptionStatus;
57
+ gracePeriodEndsAt?: string | null;
53
58
  }
54
59
  interface FFIDSubscriptionUpdatedPayload {
55
60
  subscriptionId: string;
@@ -57,6 +62,8 @@ interface FFIDSubscriptionUpdatedPayload {
57
62
  plan?: string;
58
63
  previousPlan?: string;
59
64
  status?: string;
65
+ effectiveStatus?: EffectiveSubscriptionStatus;
66
+ gracePeriodEndsAt?: string | null;
60
67
  }
61
68
  interface FFIDSubscriptionCanceledPayload {
62
69
  /** FFID subscription UUID. */
@@ -86,6 +93,10 @@ interface FFIDSubscriptionCanceledPayload {
86
93
  * when Stripe runs out the paid period before emitting the deletion event.
87
94
  */
88
95
  expiredAt?: string;
96
+ /** Cancellation always leaves the canonical access-control status as `canceled`. */
97
+ effectiveStatus?: EffectiveSubscriptionStatus;
98
+ /** Always null for cancellation events because no grace window remains. */
99
+ gracePeriodEndsAt?: string | null;
89
100
  }
90
101
  interface FFIDSubscriptionDowngradeScheduledPayload {
91
102
  /** FFID subscription UUID. */
@@ -172,6 +183,13 @@ interface FFIDSubscriptionPaymentFailedPayload {
172
183
  * `null` means already blocking.
173
184
  */
174
185
  graceUntil?: string | null;
186
+ /** Canonical FFID access-control status derived from payment-failure severity. */
187
+ effectiveStatus?: EffectiveSubscriptionStatus;
188
+ /**
189
+ * Alias of `graceUntil` for canonical access payloads; services should prefer
190
+ * this field when sharing code with `/subscriptions/ext/check`.
191
+ */
192
+ gracePeriodEndsAt?: string | null;
175
193
  /**
176
194
  * `true` when the FFID server could not resolve `stripeSubscriptionId` to an
177
195
  * FFID subscription row (e.g. race with a not-yet-synced sub, or invoice
@@ -1,3 +1,5 @@
1
+ import { E as EffectiveSubscriptionStatus } from '../types-5g_Bg6Ey.js';
2
+
1
3
  /**
2
4
  * FFID Webhook SDK Constants
3
5
  *
@@ -43,6 +45,7 @@ declare class FFIDWebhookPayloadError extends FFIDWebhookError {
43
45
  * Types for receiving and verifying FeelFlow ID webhook events.
44
46
  * Used by external services to handle real-time event notifications.
45
47
  */
48
+
46
49
  /** All supported webhook event types */
47
50
  type FFIDWebhookEventType = 'subscription.created' | 'subscription.updated' | 'subscription.canceled' | 'subscription.downgrade_scheduled' | 'subscription.downgrade_applied' | 'subscription.downgrade_canceled' | 'subscription.trial_ending' | 'subscription.payment_failed' | 'user.created' | 'user.updated' | 'user.deleted' | 'user.deletion_requested' | 'organization.created' | 'organization.updated' | 'organization.member.added' | 'organization.member.removed' | 'organization.member.role_changed' | 'legal.document.updated' | 'legal.agreement.required' | 'system.maintenance.scheduled' | 'system.maintenance.started' | 'system.maintenance.completed' | 'announcement.published' | 'test.ping';
48
51
  interface FFIDSubscriptionCreatedPayload {
@@ -50,6 +53,8 @@ interface FFIDSubscriptionCreatedPayload {
50
53
  organizationId?: string;
51
54
  plan?: string;
52
55
  status?: string;
56
+ effectiveStatus?: EffectiveSubscriptionStatus;
57
+ gracePeriodEndsAt?: string | null;
53
58
  }
54
59
  interface FFIDSubscriptionUpdatedPayload {
55
60
  subscriptionId: string;
@@ -57,6 +62,8 @@ interface FFIDSubscriptionUpdatedPayload {
57
62
  plan?: string;
58
63
  previousPlan?: string;
59
64
  status?: string;
65
+ effectiveStatus?: EffectiveSubscriptionStatus;
66
+ gracePeriodEndsAt?: string | null;
60
67
  }
61
68
  interface FFIDSubscriptionCanceledPayload {
62
69
  /** FFID subscription UUID. */
@@ -86,6 +93,10 @@ interface FFIDSubscriptionCanceledPayload {
86
93
  * when Stripe runs out the paid period before emitting the deletion event.
87
94
  */
88
95
  expiredAt?: string;
96
+ /** Cancellation always leaves the canonical access-control status as `canceled`. */
97
+ effectiveStatus?: EffectiveSubscriptionStatus;
98
+ /** Always null for cancellation events because no grace window remains. */
99
+ gracePeriodEndsAt?: string | null;
89
100
  }
90
101
  interface FFIDSubscriptionDowngradeScheduledPayload {
91
102
  /** FFID subscription UUID. */
@@ -172,6 +183,13 @@ interface FFIDSubscriptionPaymentFailedPayload {
172
183
  * `null` means already blocking.
173
184
  */
174
185
  graceUntil?: string | null;
186
+ /** Canonical FFID access-control status derived from payment-failure severity. */
187
+ effectiveStatus?: EffectiveSubscriptionStatus;
188
+ /**
189
+ * Alias of `graceUntil` for canonical access payloads; services should prefer
190
+ * this field when sharing code with `/subscriptions/ext/check`.
191
+ */
192
+ gracePeriodEndsAt?: string | null;
175
193
  /**
176
194
  * `true` when the FFID server could not resolve `stripeSubscriptionId` to an
177
195
  * FFID subscription row (e.g. race with a not-yet-synced sub, or invoice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelflow/ffid-sdk",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
5
  "keywords": [
6
6
  "feelflow",