@feelflow/ffid-sdk 5.7.0 → 5.9.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.
@@ -111,7 +111,16 @@ var SESSION_ELIGIBLE_SUBSCRIPTION_STATUSES = [
111
111
  function isSessionEligibleSubscriptionStatus(value) {
112
112
  return typeof value === "string" && SESSION_ELIGIBLE_SUBSCRIPTION_STATUSES.includes(value);
113
113
  }
114
+ function hasOwnKey(value, key) {
115
+ return Object.prototype.hasOwnProperty.call(value, key);
116
+ }
114
117
  function normalizeUserinfo(raw) {
118
+ const hasOrganizationId = hasOwnKey(raw, "organization_id");
119
+ const hasOrganizationName = hasOwnKey(raw, "organization_name");
120
+ const organizationFields = hasOrganizationId ? {
121
+ organizationId: raw.organization_id ?? null,
122
+ organizationName: hasOrganizationName ? raw.organization_name ?? null : null
123
+ } : hasOrganizationName ? { organizationName: raw.organization_name ?? null } : {};
115
124
  return {
116
125
  sub: raw.sub,
117
126
  email: raw.email,
@@ -121,7 +130,7 @@ function normalizeUserinfo(raw) {
121
130
  department: raw.department ?? null,
122
131
  position: raw.position ?? null,
123
132
  phoneNumber: raw.phone_number ?? null,
124
- organizationId: raw.organization_id ?? null,
133
+ ...organizationFields,
125
134
  subscription: raw.subscription ? {
126
135
  subscriptionId: raw.subscription.subscription_id ?? null,
127
136
  status: raw.subscription.status ?? null,
@@ -130,7 +139,16 @@ function normalizeUserinfo(raw) {
130
139
  seatModel: raw.subscription.seat_model ?? null,
131
140
  memberRole: raw.subscription.member_role ?? null,
132
141
  organizationId: raw.subscription.organization_id ?? null,
133
- hasSeatAssignment: raw.subscription.has_seat_assignment ?? false
142
+ organizationName: hasOwnKey(raw.subscription, "organization_name") ? raw.subscription.organization_name ?? null : null,
143
+ hasSeatAssignment: raw.subscription.has_seat_assignment ?? false,
144
+ // #3464: legacy FFID (< this issue) omits the field on the wire.
145
+ // The "unknown" axis is unused — no consumer differentiates "server
146
+ // didn't tell us" from "definitely not scheduled" because the
147
+ // companion `effectiveStatus` already returns `'active'` (not
148
+ // `'active_canceling'`) for legacy backends. Defaulting to `false`
149
+ // here keeps the downstream contract a plain boolean.
150
+ cancelAtPeriodEnd: raw.subscription.cancel_at_period_end ?? false,
151
+ currentPeriodEnd: raw.subscription.current_period_end ?? null
134
152
  } : void 0
135
153
  };
136
154
  }
@@ -147,11 +165,18 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
147
165
  planCode: subscription.planCode,
148
166
  planName: subscription.planCode,
149
167
  status: subscription.status,
150
- currentPeriodEnd: null,
168
+ // #3464: userinfo now carries `current_period_end` and
169
+ // `cancel_at_period_end`. Pre-#3464 FFID backends still omit them on
170
+ // the wire (`currentPeriodEnd === null` / `cancelAtPeriodEnd === false`
171
+ // after `normalizeUserinfo` defaults), so the session helper sees
172
+ // plain `'active'` — identical to pre-#3464 behaviour.
173
+ currentPeriodEnd: subscription.currentPeriodEnd,
151
174
  trialEnd: null,
175
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
152
176
  seatModel: subscription.seatModel ?? void 0,
153
177
  memberRole: subscription.memberRole ?? void 0,
154
- organizationId: subscription.organizationId
178
+ organizationId: subscription.organizationId,
179
+ organizationName: subscription.organizationName ?? void 0
155
180
  }
156
181
  ];
157
182
  }
@@ -360,7 +385,8 @@ function createVerifyAccessToken(deps) {
360
385
  department: introspectResponse.department ?? null,
361
386
  position: introspectResponse.position ?? null,
362
387
  phone_number: introspectResponse.phone_number ?? null,
363
- organization_id: introspectResponse.organization_id ?? null
388
+ organization_id: introspectResponse.organization_id ?? null,
389
+ organization_name: introspectResponse.organization_name ?? null
364
390
  };
365
391
  const raw = introspectResponse.subscription ? {
366
392
  ...base,
@@ -372,6 +398,7 @@ function createVerifyAccessToken(deps) {
372
398
  seat_model: introspectResponse.subscription.seat_model,
373
399
  member_role: introspectResponse.subscription.member_role,
374
400
  organization_id: introspectResponse.subscription.organization_id,
401
+ organization_name: introspectResponse.subscription.organization_name ?? null,
375
402
  has_seat_assignment: introspectResponse.subscription.has_seat_assignment ?? false
376
403
  }
377
404
  } : base;
@@ -842,7 +869,7 @@ function createProfileMethods(deps) {
842
869
  }
843
870
 
844
871
  // src/client/version-check.ts
845
- var SDK_VERSION = "5.7.0";
872
+ var SDK_VERSION = "5.9.0";
846
873
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
847
874
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
848
875
  function sdkHeaders() {
@@ -109,7 +109,16 @@ var SESSION_ELIGIBLE_SUBSCRIPTION_STATUSES = [
109
109
  function isSessionEligibleSubscriptionStatus(value) {
110
110
  return typeof value === "string" && SESSION_ELIGIBLE_SUBSCRIPTION_STATUSES.includes(value);
111
111
  }
112
+ function hasOwnKey(value, key) {
113
+ return Object.prototype.hasOwnProperty.call(value, key);
114
+ }
112
115
  function normalizeUserinfo(raw) {
116
+ const hasOrganizationId = hasOwnKey(raw, "organization_id");
117
+ const hasOrganizationName = hasOwnKey(raw, "organization_name");
118
+ const organizationFields = hasOrganizationId ? {
119
+ organizationId: raw.organization_id ?? null,
120
+ organizationName: hasOrganizationName ? raw.organization_name ?? null : null
121
+ } : hasOrganizationName ? { organizationName: raw.organization_name ?? null } : {};
113
122
  return {
114
123
  sub: raw.sub,
115
124
  email: raw.email,
@@ -119,7 +128,7 @@ function normalizeUserinfo(raw) {
119
128
  department: raw.department ?? null,
120
129
  position: raw.position ?? null,
121
130
  phoneNumber: raw.phone_number ?? null,
122
- organizationId: raw.organization_id ?? null,
131
+ ...organizationFields,
123
132
  subscription: raw.subscription ? {
124
133
  subscriptionId: raw.subscription.subscription_id ?? null,
125
134
  status: raw.subscription.status ?? null,
@@ -128,7 +137,16 @@ function normalizeUserinfo(raw) {
128
137
  seatModel: raw.subscription.seat_model ?? null,
129
138
  memberRole: raw.subscription.member_role ?? null,
130
139
  organizationId: raw.subscription.organization_id ?? null,
131
- hasSeatAssignment: raw.subscription.has_seat_assignment ?? false
140
+ organizationName: hasOwnKey(raw.subscription, "organization_name") ? raw.subscription.organization_name ?? null : null,
141
+ hasSeatAssignment: raw.subscription.has_seat_assignment ?? false,
142
+ // #3464: legacy FFID (< this issue) omits the field on the wire.
143
+ // The "unknown" axis is unused — no consumer differentiates "server
144
+ // didn't tell us" from "definitely not scheduled" because the
145
+ // companion `effectiveStatus` already returns `'active'` (not
146
+ // `'active_canceling'`) for legacy backends. Defaulting to `false`
147
+ // here keeps the downstream contract a plain boolean.
148
+ cancelAtPeriodEnd: raw.subscription.cancel_at_period_end ?? false,
149
+ currentPeriodEnd: raw.subscription.current_period_end ?? null
132
150
  } : void 0
133
151
  };
134
152
  }
@@ -145,11 +163,18 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
145
163
  planCode: subscription.planCode,
146
164
  planName: subscription.planCode,
147
165
  status: subscription.status,
148
- currentPeriodEnd: null,
166
+ // #3464: userinfo now carries `current_period_end` and
167
+ // `cancel_at_period_end`. Pre-#3464 FFID backends still omit them on
168
+ // the wire (`currentPeriodEnd === null` / `cancelAtPeriodEnd === false`
169
+ // after `normalizeUserinfo` defaults), so the session helper sees
170
+ // plain `'active'` — identical to pre-#3464 behaviour.
171
+ currentPeriodEnd: subscription.currentPeriodEnd,
149
172
  trialEnd: null,
173
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
150
174
  seatModel: subscription.seatModel ?? void 0,
151
175
  memberRole: subscription.memberRole ?? void 0,
152
- organizationId: subscription.organizationId
176
+ organizationId: subscription.organizationId,
177
+ organizationName: subscription.organizationName ?? void 0
153
178
  }
154
179
  ];
155
180
  }
@@ -358,7 +383,8 @@ function createVerifyAccessToken(deps) {
358
383
  department: introspectResponse.department ?? null,
359
384
  position: introspectResponse.position ?? null,
360
385
  phone_number: introspectResponse.phone_number ?? null,
361
- organization_id: introspectResponse.organization_id ?? null
386
+ organization_id: introspectResponse.organization_id ?? null,
387
+ organization_name: introspectResponse.organization_name ?? null
362
388
  };
363
389
  const raw = introspectResponse.subscription ? {
364
390
  ...base,
@@ -370,6 +396,7 @@ function createVerifyAccessToken(deps) {
370
396
  seat_model: introspectResponse.subscription.seat_model,
371
397
  member_role: introspectResponse.subscription.member_role,
372
398
  organization_id: introspectResponse.subscription.organization_id,
399
+ organization_name: introspectResponse.subscription.organization_name ?? null,
373
400
  has_seat_assignment: introspectResponse.subscription.has_seat_assignment ?? false
374
401
  }
375
402
  } : base;
@@ -840,7 +867,7 @@ function createProfileMethods(deps) {
840
867
  }
841
868
 
842
869
  // src/client/version-check.ts
843
- var SDK_VERSION = "5.7.0";
870
+ var SDK_VERSION = "5.9.0";
844
871
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
845
872
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
846
873
  function sdkHeaders() {
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunkFXIDKFXQ_cjs = require('../chunk-FXIDKFXQ.cjs');
3
+ var chunkA6MOPB7D_cjs = require('../chunk-A6MOPB7D.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
8
8
  enumerable: true,
9
- get: function () { return chunkFXIDKFXQ_cjs.FFIDAnnouncementBadge; }
9
+ get: function () { return chunkA6MOPB7D_cjs.FFIDAnnouncementBadge; }
10
10
  });
11
11
  Object.defineProperty(exports, "FFIDAnnouncementList", {
12
12
  enumerable: true,
13
- get: function () { return chunkFXIDKFXQ_cjs.FFIDAnnouncementList; }
13
+ get: function () { return chunkA6MOPB7D_cjs.FFIDAnnouncementList; }
14
14
  });
15
15
  Object.defineProperty(exports, "FFIDInquiryForm", {
16
16
  enumerable: true,
17
- get: function () { return chunkFXIDKFXQ_cjs.FFIDInquiryForm; }
17
+ get: function () { return chunkA6MOPB7D_cjs.FFIDInquiryForm; }
18
18
  });
19
19
  Object.defineProperty(exports, "FFIDLoginButton", {
20
20
  enumerable: true,
21
- get: function () { return chunkFXIDKFXQ_cjs.FFIDLoginButton; }
21
+ get: function () { return chunkA6MOPB7D_cjs.FFIDLoginButton; }
22
22
  });
23
23
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
24
24
  enumerable: true,
25
- get: function () { return chunkFXIDKFXQ_cjs.FFIDOrganizationSwitcher; }
25
+ get: function () { return chunkA6MOPB7D_cjs.FFIDOrganizationSwitcher; }
26
26
  });
27
27
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
28
28
  enumerable: true,
29
- get: function () { return chunkFXIDKFXQ_cjs.FFIDSubscriptionBadge; }
29
+ get: function () { return chunkA6MOPB7D_cjs.FFIDSubscriptionBadge; }
30
30
  });
31
31
  Object.defineProperty(exports, "FFIDUserMenu", {
32
32
  enumerable: true,
33
- get: function () { return chunkFXIDKFXQ_cjs.FFIDUserMenu; }
33
+ get: function () { return chunkA6MOPB7D_cjs.FFIDUserMenu; }
34
34
  });
@@ -1,3 +1,3 @@
1
- export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-EFp_ZgK9.cjs';
1
+ export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-DSvX4q8E.cjs';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1,3 +1,3 @@
1
- export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-EFp_ZgK9.js';
1
+ export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-DSvX4q8E.js';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1 +1 @@
1
- export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-YAJN36SD.js';
1
+ export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-KENJXMSG.js';
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from './types-s3y11uT6.cjs';
1
+ import { E as EffectiveSubscriptionStatus } from './types-BYdtyO_2.cjs';
2
2
 
3
3
  /**
4
4
  * Inquiry types exposed by the FFID SDK.
@@ -740,6 +740,8 @@ interface FFIDSubscription {
740
740
  memberRole?: FFIDOAuthUserInfoMemberRole | undefined;
741
741
  /** Organization ID used to resolve subscription access */
742
742
  organizationId?: string | null | undefined;
743
+ /** Organization display name for UI rendering; not used for access resolution */
744
+ organizationName?: string | null | undefined;
743
745
  }
744
746
  /** OAuth userinfo subscription summary */
745
747
  interface FFIDOAuthUserInfoSubscription {
@@ -757,13 +759,35 @@ interface FFIDOAuthUserInfoSubscription {
757
759
  seatModel: FFIDSeatModel | null;
758
760
  memberRole: FFIDOAuthUserInfoMemberRole | null;
759
761
  organizationId: string | null;
762
+ organizationName?: string | null | undefined;
760
763
  hasSeatAssignment: boolean | null;
764
+ /**
765
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
766
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
767
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
768
+ * the server omits it (older FFID pre-#3464), so consumers always see
769
+ * a plain boolean and never branch on an "unknown" sentinel — the
770
+ * companion `effectiveStatus` already returns `'active'` (not
771
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
772
+ * is implicitly false there.
773
+ */
774
+ cancelAtPeriodEnd: boolean;
775
+ /**
776
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
777
+ * `null` for free / seatless plans, or when serving an older FFID
778
+ * (pre-#3464) that does not populate the field. Exposed so callers
779
+ * can display the cancellation effective date without an `/ext/check`
780
+ * round-trip.
781
+ */
782
+ currentPeriodEnd: string | null;
761
783
  }
762
784
  /**
763
785
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
764
- * and `verifyAccessToken()` (JWT or introspect path). `subscription` and
765
- * `organizationId` are scope-gated on `/oauth/userinfo` (FFID #3380, requires
766
- * `subscription:read`); JWT-verify additionally omits `subscription` always.
786
+ * and `verifyAccessToken()` (JWT or introspect path). `subscription`,
787
+ * `organizationId`, and `organizationName` are scope-gated on `/oauth/userinfo`
788
+ * (FFID #3380, requires `subscription:read`); JWT-only verify additionally
789
+ * omits `subscription` and `organizationName` when `includeProfile` is false
790
+ * because the JWT has `org_id` but no `org_name` claim.
767
791
  * See per-field JSDoc — misreading `undefined` is a security regression.
768
792
  */
769
793
  interface FFIDOAuthUserInfo {
@@ -781,6 +805,14 @@ interface FFIDOAuthUserInfo {
781
805
  * — NOT "no organization").
782
806
  */
783
807
  organizationId?: string | null | undefined;
808
+ /**
809
+ * Organization display name, `null` if the resolved organization has no name,
810
+ * or `undefined` if the granted scope omits `subscription:read` on
811
+ * `/oauth/userinfo` (FFID #3514), or if the caller used JWT verification
812
+ * without `includeProfile`. JWT payloads carry `org_id` but no `org_name`
813
+ * claim.
814
+ */
815
+ organizationName?: string | null | undefined;
784
816
  /**
785
817
  * Subscription summary for `(user, service, organization)`. Branch on
786
818
  * `effectiveStatus` (5.3.0+) for access decisions.
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from './types-s3y11uT6.js';
1
+ import { E as EffectiveSubscriptionStatus } from './types-BYdtyO_2.js';
2
2
 
3
3
  /**
4
4
  * Inquiry types exposed by the FFID SDK.
@@ -740,6 +740,8 @@ interface FFIDSubscription {
740
740
  memberRole?: FFIDOAuthUserInfoMemberRole | undefined;
741
741
  /** Organization ID used to resolve subscription access */
742
742
  organizationId?: string | null | undefined;
743
+ /** Organization display name for UI rendering; not used for access resolution */
744
+ organizationName?: string | null | undefined;
743
745
  }
744
746
  /** OAuth userinfo subscription summary */
745
747
  interface FFIDOAuthUserInfoSubscription {
@@ -757,13 +759,35 @@ interface FFIDOAuthUserInfoSubscription {
757
759
  seatModel: FFIDSeatModel | null;
758
760
  memberRole: FFIDOAuthUserInfoMemberRole | null;
759
761
  organizationId: string | null;
762
+ organizationName?: string | null | undefined;
760
763
  hasSeatAssignment: boolean | null;
764
+ /**
765
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
766
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
767
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
768
+ * the server omits it (older FFID pre-#3464), so consumers always see
769
+ * a plain boolean and never branch on an "unknown" sentinel — the
770
+ * companion `effectiveStatus` already returns `'active'` (not
771
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
772
+ * is implicitly false there.
773
+ */
774
+ cancelAtPeriodEnd: boolean;
775
+ /**
776
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
777
+ * `null` for free / seatless plans, or when serving an older FFID
778
+ * (pre-#3464) that does not populate the field. Exposed so callers
779
+ * can display the cancellation effective date without an `/ext/check`
780
+ * round-trip.
781
+ */
782
+ currentPeriodEnd: string | null;
761
783
  }
762
784
  /**
763
785
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
764
- * and `verifyAccessToken()` (JWT or introspect path). `subscription` and
765
- * `organizationId` are scope-gated on `/oauth/userinfo` (FFID #3380, requires
766
- * `subscription:read`); JWT-verify additionally omits `subscription` always.
786
+ * and `verifyAccessToken()` (JWT or introspect path). `subscription`,
787
+ * `organizationId`, and `organizationName` are scope-gated on `/oauth/userinfo`
788
+ * (FFID #3380, requires `subscription:read`); JWT-only verify additionally
789
+ * omits `subscription` and `organizationName` when `includeProfile` is false
790
+ * because the JWT has `org_id` but no `org_name` claim.
767
791
  * See per-field JSDoc — misreading `undefined` is a security regression.
768
792
  */
769
793
  interface FFIDOAuthUserInfo {
@@ -781,6 +805,14 @@ interface FFIDOAuthUserInfo {
781
805
  * — NOT "no organization").
782
806
  */
783
807
  organizationId?: string | null | undefined;
808
+ /**
809
+ * Organization display name, `null` if the resolved organization has no name,
810
+ * or `undefined` if the granted scope omits `subscription:read` on
811
+ * `/oauth/userinfo` (FFID #3514), or if the caller used JWT verification
812
+ * without `includeProfile`. JWT payloads carry `org_id` but no `org_name`
813
+ * claim.
814
+ */
815
+ organizationName?: string | null | undefined;
784
816
  /**
785
817
  * Subscription summary for `(user, service, organization)`. Branch on
786
818
  * `effectiveStatus` (5.3.0+) for access decisions.
@@ -57,22 +57,18 @@ interface FFIDCacheConfig {
57
57
  * This is the FFID-internal runtime-expired case (see
58
58
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
59
59
  *
60
- * **SDK / server asymmetry `active_canceling` is session-only (#3460).**
61
- * The backend `EffectiveSubscriptionStatus` in
62
- * `src/lib/common/subscription-helpers.ts` does NOT currently include
63
- * `active_canceling`; cancellation-scheduled subscriptions surface as plain
64
- * `'active'` on `/api/v1/subscriptions/ext/check` and `/oauth/userinfo`, and
65
- * consumers that branch on the server verdict will not see the new literal.
66
- * The SDK enriches `computeEffectiveStatusFromSession` with
67
- * `cancelAtPeriodEnd` (carried on `FFIDSubscription` for FFID backends from
68
- * #3460 onward older backends omit the field and the helper defaults to
69
- * `false`, preserving the pre-#3460 `'active'` verdict) so `useSubscription`
70
- * can surface a "cancellation scheduled" badge without waiting for an
71
- * `/ext/check` round-trip. The asymmetry is safe because the new literal is
72
- * access-granting (no security regression on the userinfo path); when the
73
- * server's `EffectiveSubscriptionStatus` is later extended to emit
74
- * `active_canceling`, this docstring MUST be updated to drop the
75
- * session-only note.
60
+ * **`active_canceling` is mirrored on the server (#3464, SDK 5.8.0).**
61
+ * Earlier (#3460, SDK 5.7.0) the literal was a session-only enrichment;
62
+ * since FFID #3464 the backend `EffectiveSubscriptionStatus` and both
63
+ * `/api/v1/subscriptions/ext/check` + `/oauth/userinfo` emit
64
+ * `active_canceling` directly when `cancel_at_period_end=true` and the
65
+ * period is still live. SDK consumers receive the same literal whether
66
+ * they read it from the session-derived `computeEffectiveStatusFromSession`
67
+ * or the server-derived `effectiveStatus` on the userinfo / ext-check
68
+ * paths. Pre-#3464 FFID backends omit `cancelAtPeriodEnd` on the wire; the
69
+ * SDK `normalizeUserinfo` boundary defaults it to `false`, so downstream
70
+ * consumers see a plain boolean and `computeEffectiveStatus` returns the
71
+ * original `'active'` verdict for legacy deployments.
76
72
  *
77
73
  * @see /api/v1/subscriptions/ext/check
78
74
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
@@ -267,6 +263,8 @@ interface FFIDSubscription {
267
263
  memberRole?: FFIDOAuthUserInfoMemberRole | undefined;
268
264
  /** Organization ID used to resolve subscription access */
269
265
  organizationId?: string | null | undefined;
266
+ /** Organization display name for UI rendering; not used for access resolution */
267
+ organizationName?: string | null | undefined;
270
268
  }
271
269
  /** OAuth userinfo subscription summary */
272
270
  interface FFIDOAuthUserInfoSubscription {
@@ -284,13 +282,35 @@ interface FFIDOAuthUserInfoSubscription {
284
282
  seatModel: FFIDSeatModel | null;
285
283
  memberRole: FFIDOAuthUserInfoMemberRole | null;
286
284
  organizationId: string | null;
285
+ organizationName?: string | null | undefined;
287
286
  hasSeatAssignment: boolean | null;
287
+ /**
288
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
289
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
290
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
291
+ * the server omits it (older FFID pre-#3464), so consumers always see
292
+ * a plain boolean and never branch on an "unknown" sentinel — the
293
+ * companion `effectiveStatus` already returns `'active'` (not
294
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
295
+ * is implicitly false there.
296
+ */
297
+ cancelAtPeriodEnd: boolean;
298
+ /**
299
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
300
+ * `null` for free / seatless plans, or when serving an older FFID
301
+ * (pre-#3464) that does not populate the field. Exposed so callers
302
+ * can display the cancellation effective date without an `/ext/check`
303
+ * round-trip.
304
+ */
305
+ currentPeriodEnd: string | null;
288
306
  }
289
307
  /**
290
308
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
291
- * and `verifyAccessToken()` (JWT or introspect path). `subscription` and
292
- * `organizationId` are scope-gated on `/oauth/userinfo` (FFID #3380, requires
293
- * `subscription:read`); JWT-verify additionally omits `subscription` always.
309
+ * and `verifyAccessToken()` (JWT or introspect path). `subscription`,
310
+ * `organizationId`, and `organizationName` are scope-gated on `/oauth/userinfo`
311
+ * (FFID #3380, requires `subscription:read`); JWT-only verify additionally
312
+ * omits `subscription` and `organizationName` when `includeProfile` is false
313
+ * because the JWT has `org_id` but no `org_name` claim.
294
314
  * See per-field JSDoc — misreading `undefined` is a security regression.
295
315
  */
296
316
  interface FFIDOAuthUserInfo {
@@ -308,6 +328,14 @@ interface FFIDOAuthUserInfo {
308
328
  * — NOT "no organization").
309
329
  */
310
330
  organizationId?: string | null | undefined;
331
+ /**
332
+ * Organization display name, `null` if the resolved organization has no name,
333
+ * or `undefined` if the granted scope omits `subscription:read` on
334
+ * `/oauth/userinfo` (FFID #3514), or if the caller used JWT verification
335
+ * without `includeProfile`. JWT payloads carry `org_id` but no `org_name`
336
+ * claim.
337
+ */
338
+ organizationName?: string | null | undefined;
311
339
  /**
312
340
  * Subscription summary for `(user, service, organization)`. Branch on
313
341
  * `effectiveStatus` (5.3.0+) for access decisions.
@@ -836,6 +864,7 @@ interface FFIDTokenIntrospectionResponse {
836
864
  token_type?: 'Bearer';
837
865
  client_id?: string;
838
866
  organization_id?: string | null;
867
+ organization_name?: string | null;
839
868
  subscription?: {
840
869
  subscription_id: string | null;
841
870
  status: FFIDOAuthUserInfoSubscription['status'];
@@ -845,6 +874,7 @@ interface FFIDTokenIntrospectionResponse {
845
874
  seat_model: FFIDOAuthUserInfoSubscription['seatModel'];
846
875
  member_role: FFIDOAuthUserInfoSubscription['memberRole'];
847
876
  organization_id: string | null;
877
+ organization_name?: string | null;
848
878
  has_seat_assignment?: boolean;
849
879
  };
850
880
  }
@@ -57,22 +57,18 @@ interface FFIDCacheConfig {
57
57
  * This is the FFID-internal runtime-expired case (see
58
58
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
59
59
  *
60
- * **SDK / server asymmetry `active_canceling` is session-only (#3460).**
61
- * The backend `EffectiveSubscriptionStatus` in
62
- * `src/lib/common/subscription-helpers.ts` does NOT currently include
63
- * `active_canceling`; cancellation-scheduled subscriptions surface as plain
64
- * `'active'` on `/api/v1/subscriptions/ext/check` and `/oauth/userinfo`, and
65
- * consumers that branch on the server verdict will not see the new literal.
66
- * The SDK enriches `computeEffectiveStatusFromSession` with
67
- * `cancelAtPeriodEnd` (carried on `FFIDSubscription` for FFID backends from
68
- * #3460 onward older backends omit the field and the helper defaults to
69
- * `false`, preserving the pre-#3460 `'active'` verdict) so `useSubscription`
70
- * can surface a "cancellation scheduled" badge without waiting for an
71
- * `/ext/check` round-trip. The asymmetry is safe because the new literal is
72
- * access-granting (no security regression on the userinfo path); when the
73
- * server's `EffectiveSubscriptionStatus` is later extended to emit
74
- * `active_canceling`, this docstring MUST be updated to drop the
75
- * session-only note.
60
+ * **`active_canceling` is mirrored on the server (#3464, SDK 5.8.0).**
61
+ * Earlier (#3460, SDK 5.7.0) the literal was a session-only enrichment;
62
+ * since FFID #3464 the backend `EffectiveSubscriptionStatus` and both
63
+ * `/api/v1/subscriptions/ext/check` + `/oauth/userinfo` emit
64
+ * `active_canceling` directly when `cancel_at_period_end=true` and the
65
+ * period is still live. SDK consumers receive the same literal whether
66
+ * they read it from the session-derived `computeEffectiveStatusFromSession`
67
+ * or the server-derived `effectiveStatus` on the userinfo / ext-check
68
+ * paths. Pre-#3464 FFID backends omit `cancelAtPeriodEnd` on the wire; the
69
+ * SDK `normalizeUserinfo` boundary defaults it to `false`, so downstream
70
+ * consumers see a plain boolean and `computeEffectiveStatus` returns the
71
+ * original `'active'` verdict for legacy deployments.
76
72
  *
77
73
  * @see /api/v1/subscriptions/ext/check
78
74
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
@@ -267,6 +263,8 @@ interface FFIDSubscription {
267
263
  memberRole?: FFIDOAuthUserInfoMemberRole | undefined;
268
264
  /** Organization ID used to resolve subscription access */
269
265
  organizationId?: string | null | undefined;
266
+ /** Organization display name for UI rendering; not used for access resolution */
267
+ organizationName?: string | null | undefined;
270
268
  }
271
269
  /** OAuth userinfo subscription summary */
272
270
  interface FFIDOAuthUserInfoSubscription {
@@ -284,13 +282,35 @@ interface FFIDOAuthUserInfoSubscription {
284
282
  seatModel: FFIDSeatModel | null;
285
283
  memberRole: FFIDOAuthUserInfoMemberRole | null;
286
284
  organizationId: string | null;
285
+ organizationName?: string | null | undefined;
287
286
  hasSeatAssignment: boolean | null;
287
+ /**
288
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
289
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
290
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
291
+ * the server omits it (older FFID pre-#3464), so consumers always see
292
+ * a plain boolean and never branch on an "unknown" sentinel — the
293
+ * companion `effectiveStatus` already returns `'active'` (not
294
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
295
+ * is implicitly false there.
296
+ */
297
+ cancelAtPeriodEnd: boolean;
298
+ /**
299
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
300
+ * `null` for free / seatless plans, or when serving an older FFID
301
+ * (pre-#3464) that does not populate the field. Exposed so callers
302
+ * can display the cancellation effective date without an `/ext/check`
303
+ * round-trip.
304
+ */
305
+ currentPeriodEnd: string | null;
288
306
  }
289
307
  /**
290
308
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
291
- * and `verifyAccessToken()` (JWT or introspect path). `subscription` and
292
- * `organizationId` are scope-gated on `/oauth/userinfo` (FFID #3380, requires
293
- * `subscription:read`); JWT-verify additionally omits `subscription` always.
309
+ * and `verifyAccessToken()` (JWT or introspect path). `subscription`,
310
+ * `organizationId`, and `organizationName` are scope-gated on `/oauth/userinfo`
311
+ * (FFID #3380, requires `subscription:read`); JWT-only verify additionally
312
+ * omits `subscription` and `organizationName` when `includeProfile` is false
313
+ * because the JWT has `org_id` but no `org_name` claim.
294
314
  * See per-field JSDoc — misreading `undefined` is a security regression.
295
315
  */
296
316
  interface FFIDOAuthUserInfo {
@@ -308,6 +328,14 @@ interface FFIDOAuthUserInfo {
308
328
  * — NOT "no organization").
309
329
  */
310
330
  organizationId?: string | null | undefined;
331
+ /**
332
+ * Organization display name, `null` if the resolved organization has no name,
333
+ * or `undefined` if the granted scope omits `subscription:read` on
334
+ * `/oauth/userinfo` (FFID #3514), or if the caller used JWT verification
335
+ * without `includeProfile`. JWT payloads carry `org_id` but no `org_name`
336
+ * claim.
337
+ */
338
+ organizationName?: string | null | undefined;
311
339
  /**
312
340
  * Subscription summary for `(user, service, organization)`. Branch on
313
341
  * `effectiveStatus` (5.3.0+) for access decisions.
@@ -836,6 +864,7 @@ interface FFIDTokenIntrospectionResponse {
836
864
  token_type?: 'Bearer';
837
865
  client_id?: string;
838
866
  organization_id?: string | null;
867
+ organization_name?: string | null;
839
868
  subscription?: {
840
869
  subscription_id: string | null;
841
870
  status: FFIDOAuthUserInfoSubscription['status'];
@@ -845,6 +874,7 @@ interface FFIDTokenIntrospectionResponse {
845
874
  seat_model: FFIDOAuthUserInfoSubscription['seatModel'];
846
875
  member_role: FFIDOAuthUserInfoSubscription['memberRole'];
847
876
  organization_id: string | null;
877
+ organization_name?: string | null;
848
878
  has_seat_assignment?: boolean;
849
879
  };
850
880
  }