@feelflow/ffid-sdk 5.18.0 → 5.21.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.
@@ -683,6 +683,217 @@ interface FFIDRemoveMemberResponse {
683
683
  message: string;
684
684
  }
685
685
 
686
+ /**
687
+ * Provisioning types (ext API) — service-key-authenticated, idempotent user /
688
+ * organization provisioning for external batch migration (#3790 / #4127).
689
+ *
690
+ * Mirrors the server contract of `POST /api/v1/ext/users/provision` and
691
+ * `POST /api/v1/ext/organizations/provision`. Responses are modelled as
692
+ * discriminated unions on `dryRun` so consumers narrow with `if (res.dryRun)`
693
+ * and the compiler enforces which fields are present on each branch
694
+ * (`wouldCreate` on dry-run, `created` on a real call).
695
+ */
696
+
697
+ /** Optional business-profile fields applied to a freshly provisioned user. */
698
+ interface FFIDProvisionUserProfileInput {
699
+ /** Display name (1–255 chars). */
700
+ displayName?: string;
701
+ /** Company name (≤255 chars). */
702
+ companyName?: string;
703
+ /** Department (≤255 chars). */
704
+ department?: string;
705
+ /** Job title (≤255 chars). */
706
+ jobTitle?: string;
707
+ /** Phone number (≤30 chars). */
708
+ phone?: string;
709
+ }
710
+ /** Parameters for `provisionUser`. */
711
+ interface FFIDProvisionUserParams {
712
+ /**
713
+ * Email of the user to provision. Normalized (trim + lowercase) server-side
714
+ * before the idempotency lookup, so re-runs with different casing resolve to
715
+ * the same user.
716
+ */
717
+ email: string;
718
+ /** Optional business profile applied only when a new user is created. */
719
+ profile?: FFIDProvisionUserProfileInput;
720
+ /**
721
+ * When `true`, the server performs no writes and returns a
722
+ * {@link FFIDProvisionUserDryRun} describing what a real call would do.
723
+ * @default false
724
+ */
725
+ dryRun?: boolean;
726
+ }
727
+ /** Minimal identity returned for a provisioned / resolved user. */
728
+ interface FFIDProvisionedUser {
729
+ /** User ID (UUID). */
730
+ id: string;
731
+ /** Email address (normalized). */
732
+ email: string;
733
+ }
734
+ /**
735
+ * Real (non-dry-run) user provisioning outcome.
736
+ *
737
+ * Idempotent: an existing email resolves to `created: false` (HTTP 200) instead
738
+ * of creating a duplicate; a new email yields `created: true` (HTTP 201) for a
739
+ * passwordless, email-confirmed user.
740
+ */
741
+ interface FFIDProvisionUserOutcome {
742
+ /** Discriminant — absent/`false` for a real call. */
743
+ dryRun?: false;
744
+ /**
745
+ * `true` → a new passwordless, email-confirmed user was created (HTTP 201).
746
+ * `false` → a user with this email already existed; idempotent no-op (HTTP 200).
747
+ */
748
+ created: boolean;
749
+ user: FFIDProvisionedUser;
750
+ /**
751
+ * Present only when a new user was created **and** a `profile` was supplied.
752
+ * `false` means the user row was created but the authoritative profile write
753
+ * did not complete — some profile fields may not be fully applied, so
754
+ * re-issue the profile to be sure. Absent on the idempotent-existing branch
755
+ * and when no profile was requested.
756
+ */
757
+ profileWritten?: boolean;
758
+ }
759
+ /** Dry-run user provisioning outcome — no writes performed. */
760
+ interface FFIDProvisionUserDryRun {
761
+ /** Discriminant — always `true` for a dry-run. */
762
+ dryRun: true;
763
+ /** Whether a real call would create a new user. */
764
+ wouldCreate: boolean;
765
+ /** `id` is present only when the user already exists. */
766
+ user: {
767
+ id?: string;
768
+ email: string;
769
+ };
770
+ }
771
+ /**
772
+ * Response from `provisionUser`. Narrow with the truthiness of `dryRun`:
773
+ *
774
+ * ```ts
775
+ * if (res.dryRun) {
776
+ * // FFIDProvisionUserDryRun — inspect res.wouldCreate
777
+ * } else {
778
+ * // FFIDProvisionUserOutcome — inspect res.created
779
+ * }
780
+ * ```
781
+ *
782
+ * A real (non-dry-run) response omits `dryRun` on the wire (it is `undefined`,
783
+ * not `false`), so narrow with `if (res.dryRun)` — do **not** use
784
+ * `res.dryRun === false` or `switch (res.dryRun)`, which would miss every real
785
+ * response. This mirrors how `FFIDApiResponse` is narrowed on `data` / `error`.
786
+ */
787
+ type FFIDProvisionUserResponse = FFIDProvisionUserOutcome | FFIDProvisionUserDryRun;
788
+ /** A member to add to the provisioned organization. */
789
+ interface FFIDProvisionOrganizationMemberInput {
790
+ email: string;
791
+ /** `owner` is intentionally excluded — the owner is set via `ownerEmail`. */
792
+ role: FFIDAssignableMemberRole;
793
+ }
794
+ /** Parameters for `provisionOrganization`. */
795
+ interface FFIDProvisionOrganizationParams {
796
+ /** Organization display name (1–255 chars). */
797
+ name: string;
798
+ /**
799
+ * Email of the organization owner. The owner **must** already be an FFID user
800
+ * — provision them via `provisionUser` first, otherwise the call returns an
801
+ * `OWNER_NOT_FOUND` error (HTTP 422).
802
+ */
803
+ ownerEmail: string;
804
+ /** Optional billing contact email. */
805
+ billingEmail?: string;
806
+ /** Members to add idempotently (≤100 per request; validated client-side). */
807
+ members?: FFIDProvisionOrganizationMemberInput[];
808
+ /**
809
+ * When `true`, the server performs no writes and returns a
810
+ * {@link FFIDProvisionOrganizationDryRun} describing what a real call would do.
811
+ * @default false
812
+ */
813
+ dryRun?: boolean;
814
+ }
815
+ /** Minimal organization shape returned by the org provisioning helper. */
816
+ interface FFIDProvisionedOrganization {
817
+ /** Organization ID (UUID). */
818
+ id: string;
819
+ /** Organization display name. */
820
+ name: string;
821
+ /** URL-safe slug. */
822
+ slug: string;
823
+ }
824
+ /** Per-member outcome status of a real (non-dry-run) organization provision. */
825
+ type FFIDProvisionMemberStatus = 'added' | 'already_member' | 'user_not_found';
826
+ /** Per-member outcome status of a dry-run organization provision. */
827
+ type FFIDProvisionMemberPlanStatus = 'would_add' | 'already_member' | 'user_not_found';
828
+ /** Per-member outcome of a real (non-dry-run) organization provision. */
829
+ interface FFIDProvisionMemberResult {
830
+ email: string;
831
+ role: FFIDAssignableMemberRole;
832
+ /**
833
+ * - `added` — membership was inserted.
834
+ * - `already_member` — user was already a member (idempotent no-op).
835
+ * - `user_not_found` — no FFID user exists for this email (skipped).
836
+ */
837
+ status: FFIDProvisionMemberStatus;
838
+ }
839
+ /** Per-member plan of a dry-run organization provision. */
840
+ interface FFIDProvisionMemberPlan {
841
+ email: string;
842
+ role: FFIDAssignableMemberRole;
843
+ /**
844
+ * - `would_add` — user exists and is not yet a member.
845
+ * - `already_member` — user is already a member of the existing org.
846
+ * - `user_not_found` — no FFID user exists for this email.
847
+ */
848
+ status: FFIDProvisionMemberPlanStatus;
849
+ }
850
+ /** Real (non-dry-run) organization provisioning outcome. */
851
+ interface FFIDProvisionOrganizationOutcome {
852
+ /** Discriminant — absent/`false` for a real call. */
853
+ dryRun?: false;
854
+ /**
855
+ * `true` → a brand-new organization was created (HTTP 201).
856
+ * `false` → the owner already owned an org with this name; idempotent (HTTP 200).
857
+ */
858
+ created: boolean;
859
+ organization: FFIDProvisionedOrganization;
860
+ /** Per-member results (added / already_member / user_not_found). */
861
+ members: FFIDProvisionMemberResult[];
862
+ }
863
+ /** Dry-run organization provisioning outcome — no writes performed. */
864
+ interface FFIDProvisionOrganizationDryRun {
865
+ /** Discriminant — always `true` for a dry-run. */
866
+ dryRun: true;
867
+ /**
868
+ * Whether a real call would create a new organization. Coupled to
869
+ * `organization`: `wouldCreate === (organization === null)` — the server
870
+ * returns `organization: null` exactly when it would create a new org.
871
+ */
872
+ wouldCreate: boolean;
873
+ /** `null` when the org does not exist yet (would be created) — see `wouldCreate`. */
874
+ organization: FFIDProvisionedOrganization | null;
875
+ /** Per-member plans (would_add / already_member / user_not_found). */
876
+ members: FFIDProvisionMemberPlan[];
877
+ }
878
+ /**
879
+ * Response from `provisionOrganization`. Narrow on `dryRun`:
880
+ *
881
+ * ```ts
882
+ * if (res.dryRun) {
883
+ * // FFIDProvisionOrganizationDryRun — inspect res.wouldCreate / res.organization (nullable)
884
+ * } else {
885
+ * // FFIDProvisionOrganizationOutcome — inspect res.created / res.members
886
+ * }
887
+ * ```
888
+ *
889
+ * As with `FFIDProvisionUserResponse`, a real response omits `dryRun` on the
890
+ * wire, so narrow with `if (res.dryRun)` — not `res.dryRun === false`.
891
+ *
892
+ * When `ownerEmail` has no FFID user, the call resolves to
893
+ * `{ error: { code: 'OWNER_NOT_FOUND' } }` (HTTP 422) rather than a data branch.
894
+ */
895
+ type FFIDProvisionOrganizationResponse = FFIDProvisionOrganizationOutcome | FFIDProvisionOrganizationDryRun;
896
+
686
897
  /**
687
898
  * Contract resource types (#3787 Phase A)
688
899
  *
@@ -1438,6 +1649,19 @@ interface FFIDSessionResponse {
1438
1649
  organizations: FFIDOrganization[];
1439
1650
  subscriptions: FFIDSubscription[];
1440
1651
  }
1652
+ /**
1653
+ * Optional metadata on successful {@link FFIDApiResponse} values (#4136).
1654
+ *
1655
+ * Present only on the success branch — error responses never carry `meta`.
1656
+ */
1657
+ interface FFIDApiResponseMeta {
1658
+ /**
1659
+ * Set when introspect-strategy verification could not use the configured
1660
+ * cache (adapter read/write failure or `crypto.subtle` unavailable).
1661
+ * Visible without a custom logger; absent when cache operated normally.
1662
+ */
1663
+ verificationCacheDegraded?: true;
1664
+ }
1441
1665
  /**
1442
1666
  * API response wrapper (discriminated union for type safety)
1443
1667
  *
@@ -1447,10 +1671,33 @@ interface FFIDSessionResponse {
1447
1671
  type FFIDApiResponse<T> = {
1448
1672
  data: T;
1449
1673
  error?: undefined;
1674
+ meta?: FFIDApiResponseMeta;
1450
1675
  } | {
1451
1676
  data?: undefined;
1452
1677
  error: FFIDError;
1453
1678
  };
1679
+ /**
1680
+ * Result of `signOut()` (#4119, extended #4136).
1681
+ *
1682
+ * Local credentials (tokens / local state) are always cleared when `signOut()`
1683
+ * resolves without `error`. `revoked` additionally indicates whether the
1684
+ * server-side revocation/sign-out actually succeeded:
1685
+ *
1686
+ * - `revoked: true` — no server-side session/token remains active.
1687
+ * - `revoked: false` — server-side revocation failed (network error or non-2xx
1688
+ * response). The access token may remain valid server-side until natural
1689
+ * expiry. Non-fatal: the local sign-out still completed, but callers that
1690
+ * need guaranteed revocation (e.g. shared-device flows) should surface this.
1691
+ *
1692
+ * Token mode (#4136): `accessTokenRevoked` / `refreshTokenRevoked` report each
1693
+ * RFC 7009 revoke call. `revoked` is `true` only when every applicable token
1694
+ * was revoked successfully. Cookie mode omits the per-token fields.
1695
+ */
1696
+ interface FFIDSignOutResult {
1697
+ revoked: boolean;
1698
+ accessTokenRevoked?: boolean;
1699
+ refreshTokenRevoked?: boolean;
1700
+ }
1454
1701
 
1455
1702
  /**
1456
1703
  * Response shape from `GET /api/v1/ext/analytics/config?service=<code>`.
@@ -1484,23 +1731,33 @@ interface FFIDAnalyticsConfig {
1484
1731
  * has been fired **3 times within 60 seconds**. Caller should surface a
1485
1732
  * manual "再度ログインする" UI rather than retry automatically (#2406 / #2411).
1486
1733
  *
1487
- * SDK 2.18.0 only ships `'redirect_loop_detected'`. Other failure paths
1488
- * (SSR environment, PKCE generation failure, empty organizationId) currently
1489
- * return `error` without a `code`. New codes will be added in future minor
1490
- * versions — treat this union as forward-extensible and do NOT exhaustively
1491
- * `switch` over it without a `default` branch for consumer code that must
1492
- * stay compatible across SDK upgrades.
1734
+ * - `'state_persistence_failed'`: `redirectToAuthorize()` could not persist
1735
+ * the OAuth CSRF `state` (storage unavailable / quota). Redirect is aborted
1736
+ * fail-closed (#4136) callers should surface a manual retry UI.
1737
+ *
1738
+ * Other failure paths (SSR environment, PKCE generation failure, empty
1739
+ * organizationId) currently return `error` without a `code`. Treat this union
1740
+ * as forward-extensible and do NOT exhaustively `switch` over it without a
1741
+ * `default` branch for consumer code that must stay compatible across SDK
1742
+ * upgrades.
1493
1743
  */
1494
- type FFIDRedirectErrorCode = 'redirect_loop_detected';
1744
+ type FFIDRedirectErrorCode = 'redirect_loop_detected' | 'state_persistence_failed';
1495
1745
  /**
1496
1746
  * Result of a redirect operation (redirectToLogin / redirectToAuthorize / redirectToLogout)
1497
1747
  *
1498
1748
  * Structured return type so callers can inspect failure reasons
1499
1749
  * instead of receiving a bare `false`. When `code` is set, branch on it
1500
1750
  * for programmatic handling; otherwise log `error` for humans.
1751
+ *
1752
+ * `loopDetectionDisabled` (#4119): set to `true` when sessionStorage is
1753
+ * unavailable (iOS WebKit private mode / eviction) so redirect-loop detection
1754
+ * ran **fail-open** — the redirect fired, but repeated-redirect protection is
1755
+ * disabled for this browser session. Callers that need loop protection on
1756
+ * such browsers should add their own max-retry guard.
1501
1757
  */
1502
1758
  type FFIDRedirectResult = {
1503
1759
  success: true;
1760
+ loopDetectionDisabled?: boolean;
1504
1761
  } | {
1505
1762
  success: false;
1506
1763
  error: string;
@@ -1695,7 +1952,7 @@ interface RedirectToAuthorizeOptions {
1695
1952
  /** Creates an FFID API client instance */
1696
1953
  declare function createFFIDClient(config: FFIDConfig): {
1697
1954
  getSession: () => Promise<FFIDApiResponse<FFIDSessionResponse>>;
1698
- signOut: () => Promise<FFIDApiResponse<void>>;
1955
+ signOut: () => Promise<FFIDApiResponse<FFIDSignOutResult>>;
1699
1956
  redirectToLogin: (options?: RedirectToAuthorizeOptions) => Promise<FFIDRedirectResult>;
1700
1957
  redirectToAuthorize: (options?: RedirectToAuthorizeOptions) => Promise<FFIDRedirectResult>;
1701
1958
  redirectToLogout: (postLogoutRedirectUri?: string) => FFIDRedirectResult;
@@ -1732,6 +1989,8 @@ declare function createFFIDClient(config: FFIDConfig): {
1732
1989
  organizationId: string;
1733
1990
  userId: string;
1734
1991
  }) => Promise<FFIDApiResponse<FFIDRemoveMemberResponse>>;
1992
+ provisionUser: (params: FFIDProvisionUserParams) => Promise<FFIDApiResponse<FFIDProvisionUserResponse>>;
1993
+ provisionOrganization: (params: FFIDProvisionOrganizationParams) => Promise<FFIDApiResponse<FFIDProvisionOrganizationResponse>>;
1735
1994
  getProfile: (options?: FFIDProfileCallOptions) => Promise<FFIDApiResponse<FFIDUserProfile>>;
1736
1995
  updateProfile: (data: FFIDUpdateUserProfileRequest, options?: FFIDProfileCallOptions) => Promise<FFIDApiResponse<FFIDUserProfile>>;
1737
1996
  getLoginHistory: (params?: FFIDGetLoginHistoryParams) => Promise<FFIDApiResponse<FFIDLoginHistoryResponse>>;
@@ -1815,4 +2074,4 @@ declare function createFFIDClient(config: FFIDConfig): {
1815
2074
  /** Type of the FFID client */
1816
2075
  type FFIDClient = ReturnType<typeof createFFIDClient>;
1817
2076
 
1818
- export { type FFIDUpdateUserProfileRequest as A, type FFIDUser as B, type FFIDUserProfile as C, type TokenStore as D, createFFIDClient as E, type FFIDLogger as F, createTokenStore as G, type TokenData as T, type FFIDError as a, type FFIDCacheAdapter as b, type FFIDVerifyAccessTokenOptions as c, type FFIDApiResponse as d, type FFIDOAuthUserInfo as e, type FFIDAddMemberParams as f, type FFIDAddMemberRequest as g, type FFIDAddMemberResponse as h, type FFIDAssignableMemberRole as i, type FFIDCacheConfig as j, type FFIDClient as k, type FFIDConfig as l, type FFIDListMembersResponse as m, type FFIDMemberRole as n, type FFIDOrganization as o, type FFIDOrganizationMember as p, type FFIDOtpSendResponse as q, type FFIDOtpVerifyResponse as r, type FFIDPasswordResetConfirmResponse as s, type FFIDPasswordResetResponse as t, type FFIDPasswordResetVerifyResponse as u, type FFIDProfileCallOptions as v, type FFIDRemoveMemberResponse as w, type FFIDResetSessionResponse as x, type FFIDSubscription as y, type FFIDUpdateMemberRoleResponse as z };
2077
+ export { type FFIDProvisionOrganizationDryRun as A, type FFIDProvisionOrganizationMemberInput as B, type FFIDProvisionOrganizationOutcome as C, type FFIDProvisionOrganizationParams as D, type FFIDProvisionOrganizationResponse as E, type FFIDLogger as F, type FFIDProvisionUserDryRun as G, type FFIDProvisionUserOutcome as H, type FFIDProvisionUserParams as I, type FFIDProvisionUserProfileInput as J, type FFIDProvisionUserResponse as K, type FFIDProvisionedOrganization as L, type FFIDProvisionedUser as M, type FFIDRemoveMemberResponse as N, type FFIDResetSessionResponse as O, type FFIDSubscription as P, type FFIDUpdateMemberRoleResponse as Q, type FFIDUpdateUserProfileRequest as R, type FFIDUser as S, type FFIDUserProfile as T, type TokenData as U, type TokenStore as V, createFFIDClient as W, createTokenStore as X, type FFIDError as a, type FFIDCacheAdapter as b, type FFIDVerifyAccessTokenOptions as c, type FFIDApiResponse as d, type FFIDOAuthUserInfo as e, type FFIDAddMemberParams as f, type FFIDAddMemberRequest as g, type FFIDAddMemberResponse as h, type FFIDAssignableMemberRole as i, type FFIDCacheConfig as j, type FFIDClient as k, type FFIDConfig as l, type FFIDListMembersResponse as m, type FFIDMemberRole as n, type FFIDOrganization as o, type FFIDOrganizationMember as p, type FFIDOtpSendResponse as q, type FFIDOtpVerifyResponse as r, type FFIDPasswordResetConfirmResponse as s, type FFIDPasswordResetResponse as t, type FFIDPasswordResetVerifyResponse as u, type FFIDProfileCallOptions as v, type FFIDProvisionMemberPlan as w, type FFIDProvisionMemberPlanStatus as x, type FFIDProvisionMemberResult as y, type FFIDProvisionMemberStatus as z };
@@ -683,6 +683,217 @@ interface FFIDRemoveMemberResponse {
683
683
  message: string;
684
684
  }
685
685
 
686
+ /**
687
+ * Provisioning types (ext API) — service-key-authenticated, idempotent user /
688
+ * organization provisioning for external batch migration (#3790 / #4127).
689
+ *
690
+ * Mirrors the server contract of `POST /api/v1/ext/users/provision` and
691
+ * `POST /api/v1/ext/organizations/provision`. Responses are modelled as
692
+ * discriminated unions on `dryRun` so consumers narrow with `if (res.dryRun)`
693
+ * and the compiler enforces which fields are present on each branch
694
+ * (`wouldCreate` on dry-run, `created` on a real call).
695
+ */
696
+
697
+ /** Optional business-profile fields applied to a freshly provisioned user. */
698
+ interface FFIDProvisionUserProfileInput {
699
+ /** Display name (1–255 chars). */
700
+ displayName?: string;
701
+ /** Company name (≤255 chars). */
702
+ companyName?: string;
703
+ /** Department (≤255 chars). */
704
+ department?: string;
705
+ /** Job title (≤255 chars). */
706
+ jobTitle?: string;
707
+ /** Phone number (≤30 chars). */
708
+ phone?: string;
709
+ }
710
+ /** Parameters for `provisionUser`. */
711
+ interface FFIDProvisionUserParams {
712
+ /**
713
+ * Email of the user to provision. Normalized (trim + lowercase) server-side
714
+ * before the idempotency lookup, so re-runs with different casing resolve to
715
+ * the same user.
716
+ */
717
+ email: string;
718
+ /** Optional business profile applied only when a new user is created. */
719
+ profile?: FFIDProvisionUserProfileInput;
720
+ /**
721
+ * When `true`, the server performs no writes and returns a
722
+ * {@link FFIDProvisionUserDryRun} describing what a real call would do.
723
+ * @default false
724
+ */
725
+ dryRun?: boolean;
726
+ }
727
+ /** Minimal identity returned for a provisioned / resolved user. */
728
+ interface FFIDProvisionedUser {
729
+ /** User ID (UUID). */
730
+ id: string;
731
+ /** Email address (normalized). */
732
+ email: string;
733
+ }
734
+ /**
735
+ * Real (non-dry-run) user provisioning outcome.
736
+ *
737
+ * Idempotent: an existing email resolves to `created: false` (HTTP 200) instead
738
+ * of creating a duplicate; a new email yields `created: true` (HTTP 201) for a
739
+ * passwordless, email-confirmed user.
740
+ */
741
+ interface FFIDProvisionUserOutcome {
742
+ /** Discriminant — absent/`false` for a real call. */
743
+ dryRun?: false;
744
+ /**
745
+ * `true` → a new passwordless, email-confirmed user was created (HTTP 201).
746
+ * `false` → a user with this email already existed; idempotent no-op (HTTP 200).
747
+ */
748
+ created: boolean;
749
+ user: FFIDProvisionedUser;
750
+ /**
751
+ * Present only when a new user was created **and** a `profile` was supplied.
752
+ * `false` means the user row was created but the authoritative profile write
753
+ * did not complete — some profile fields may not be fully applied, so
754
+ * re-issue the profile to be sure. Absent on the idempotent-existing branch
755
+ * and when no profile was requested.
756
+ */
757
+ profileWritten?: boolean;
758
+ }
759
+ /** Dry-run user provisioning outcome — no writes performed. */
760
+ interface FFIDProvisionUserDryRun {
761
+ /** Discriminant — always `true` for a dry-run. */
762
+ dryRun: true;
763
+ /** Whether a real call would create a new user. */
764
+ wouldCreate: boolean;
765
+ /** `id` is present only when the user already exists. */
766
+ user: {
767
+ id?: string;
768
+ email: string;
769
+ };
770
+ }
771
+ /**
772
+ * Response from `provisionUser`. Narrow with the truthiness of `dryRun`:
773
+ *
774
+ * ```ts
775
+ * if (res.dryRun) {
776
+ * // FFIDProvisionUserDryRun — inspect res.wouldCreate
777
+ * } else {
778
+ * // FFIDProvisionUserOutcome — inspect res.created
779
+ * }
780
+ * ```
781
+ *
782
+ * A real (non-dry-run) response omits `dryRun` on the wire (it is `undefined`,
783
+ * not `false`), so narrow with `if (res.dryRun)` — do **not** use
784
+ * `res.dryRun === false` or `switch (res.dryRun)`, which would miss every real
785
+ * response. This mirrors how `FFIDApiResponse` is narrowed on `data` / `error`.
786
+ */
787
+ type FFIDProvisionUserResponse = FFIDProvisionUserOutcome | FFIDProvisionUserDryRun;
788
+ /** A member to add to the provisioned organization. */
789
+ interface FFIDProvisionOrganizationMemberInput {
790
+ email: string;
791
+ /** `owner` is intentionally excluded — the owner is set via `ownerEmail`. */
792
+ role: FFIDAssignableMemberRole;
793
+ }
794
+ /** Parameters for `provisionOrganization`. */
795
+ interface FFIDProvisionOrganizationParams {
796
+ /** Organization display name (1–255 chars). */
797
+ name: string;
798
+ /**
799
+ * Email of the organization owner. The owner **must** already be an FFID user
800
+ * — provision them via `provisionUser` first, otherwise the call returns an
801
+ * `OWNER_NOT_FOUND` error (HTTP 422).
802
+ */
803
+ ownerEmail: string;
804
+ /** Optional billing contact email. */
805
+ billingEmail?: string;
806
+ /** Members to add idempotently (≤100 per request; validated client-side). */
807
+ members?: FFIDProvisionOrganizationMemberInput[];
808
+ /**
809
+ * When `true`, the server performs no writes and returns a
810
+ * {@link FFIDProvisionOrganizationDryRun} describing what a real call would do.
811
+ * @default false
812
+ */
813
+ dryRun?: boolean;
814
+ }
815
+ /** Minimal organization shape returned by the org provisioning helper. */
816
+ interface FFIDProvisionedOrganization {
817
+ /** Organization ID (UUID). */
818
+ id: string;
819
+ /** Organization display name. */
820
+ name: string;
821
+ /** URL-safe slug. */
822
+ slug: string;
823
+ }
824
+ /** Per-member outcome status of a real (non-dry-run) organization provision. */
825
+ type FFIDProvisionMemberStatus = 'added' | 'already_member' | 'user_not_found';
826
+ /** Per-member outcome status of a dry-run organization provision. */
827
+ type FFIDProvisionMemberPlanStatus = 'would_add' | 'already_member' | 'user_not_found';
828
+ /** Per-member outcome of a real (non-dry-run) organization provision. */
829
+ interface FFIDProvisionMemberResult {
830
+ email: string;
831
+ role: FFIDAssignableMemberRole;
832
+ /**
833
+ * - `added` — membership was inserted.
834
+ * - `already_member` — user was already a member (idempotent no-op).
835
+ * - `user_not_found` — no FFID user exists for this email (skipped).
836
+ */
837
+ status: FFIDProvisionMemberStatus;
838
+ }
839
+ /** Per-member plan of a dry-run organization provision. */
840
+ interface FFIDProvisionMemberPlan {
841
+ email: string;
842
+ role: FFIDAssignableMemberRole;
843
+ /**
844
+ * - `would_add` — user exists and is not yet a member.
845
+ * - `already_member` — user is already a member of the existing org.
846
+ * - `user_not_found` — no FFID user exists for this email.
847
+ */
848
+ status: FFIDProvisionMemberPlanStatus;
849
+ }
850
+ /** Real (non-dry-run) organization provisioning outcome. */
851
+ interface FFIDProvisionOrganizationOutcome {
852
+ /** Discriminant — absent/`false` for a real call. */
853
+ dryRun?: false;
854
+ /**
855
+ * `true` → a brand-new organization was created (HTTP 201).
856
+ * `false` → the owner already owned an org with this name; idempotent (HTTP 200).
857
+ */
858
+ created: boolean;
859
+ organization: FFIDProvisionedOrganization;
860
+ /** Per-member results (added / already_member / user_not_found). */
861
+ members: FFIDProvisionMemberResult[];
862
+ }
863
+ /** Dry-run organization provisioning outcome — no writes performed. */
864
+ interface FFIDProvisionOrganizationDryRun {
865
+ /** Discriminant — always `true` for a dry-run. */
866
+ dryRun: true;
867
+ /**
868
+ * Whether a real call would create a new organization. Coupled to
869
+ * `organization`: `wouldCreate === (organization === null)` — the server
870
+ * returns `organization: null` exactly when it would create a new org.
871
+ */
872
+ wouldCreate: boolean;
873
+ /** `null` when the org does not exist yet (would be created) — see `wouldCreate`. */
874
+ organization: FFIDProvisionedOrganization | null;
875
+ /** Per-member plans (would_add / already_member / user_not_found). */
876
+ members: FFIDProvisionMemberPlan[];
877
+ }
878
+ /**
879
+ * Response from `provisionOrganization`. Narrow on `dryRun`:
880
+ *
881
+ * ```ts
882
+ * if (res.dryRun) {
883
+ * // FFIDProvisionOrganizationDryRun — inspect res.wouldCreate / res.organization (nullable)
884
+ * } else {
885
+ * // FFIDProvisionOrganizationOutcome — inspect res.created / res.members
886
+ * }
887
+ * ```
888
+ *
889
+ * As with `FFIDProvisionUserResponse`, a real response omits `dryRun` on the
890
+ * wire, so narrow with `if (res.dryRun)` — not `res.dryRun === false`.
891
+ *
892
+ * When `ownerEmail` has no FFID user, the call resolves to
893
+ * `{ error: { code: 'OWNER_NOT_FOUND' } }` (HTTP 422) rather than a data branch.
894
+ */
895
+ type FFIDProvisionOrganizationResponse = FFIDProvisionOrganizationOutcome | FFIDProvisionOrganizationDryRun;
896
+
686
897
  /**
687
898
  * Contract resource types (#3787 Phase A)
688
899
  *
@@ -1438,6 +1649,19 @@ interface FFIDSessionResponse {
1438
1649
  organizations: FFIDOrganization[];
1439
1650
  subscriptions: FFIDSubscription[];
1440
1651
  }
1652
+ /**
1653
+ * Optional metadata on successful {@link FFIDApiResponse} values (#4136).
1654
+ *
1655
+ * Present only on the success branch — error responses never carry `meta`.
1656
+ */
1657
+ interface FFIDApiResponseMeta {
1658
+ /**
1659
+ * Set when introspect-strategy verification could not use the configured
1660
+ * cache (adapter read/write failure or `crypto.subtle` unavailable).
1661
+ * Visible without a custom logger; absent when cache operated normally.
1662
+ */
1663
+ verificationCacheDegraded?: true;
1664
+ }
1441
1665
  /**
1442
1666
  * API response wrapper (discriminated union for type safety)
1443
1667
  *
@@ -1447,10 +1671,33 @@ interface FFIDSessionResponse {
1447
1671
  type FFIDApiResponse<T> = {
1448
1672
  data: T;
1449
1673
  error?: undefined;
1674
+ meta?: FFIDApiResponseMeta;
1450
1675
  } | {
1451
1676
  data?: undefined;
1452
1677
  error: FFIDError;
1453
1678
  };
1679
+ /**
1680
+ * Result of `signOut()` (#4119, extended #4136).
1681
+ *
1682
+ * Local credentials (tokens / local state) are always cleared when `signOut()`
1683
+ * resolves without `error`. `revoked` additionally indicates whether the
1684
+ * server-side revocation/sign-out actually succeeded:
1685
+ *
1686
+ * - `revoked: true` — no server-side session/token remains active.
1687
+ * - `revoked: false` — server-side revocation failed (network error or non-2xx
1688
+ * response). The access token may remain valid server-side until natural
1689
+ * expiry. Non-fatal: the local sign-out still completed, but callers that
1690
+ * need guaranteed revocation (e.g. shared-device flows) should surface this.
1691
+ *
1692
+ * Token mode (#4136): `accessTokenRevoked` / `refreshTokenRevoked` report each
1693
+ * RFC 7009 revoke call. `revoked` is `true` only when every applicable token
1694
+ * was revoked successfully. Cookie mode omits the per-token fields.
1695
+ */
1696
+ interface FFIDSignOutResult {
1697
+ revoked: boolean;
1698
+ accessTokenRevoked?: boolean;
1699
+ refreshTokenRevoked?: boolean;
1700
+ }
1454
1701
 
1455
1702
  /**
1456
1703
  * Response shape from `GET /api/v1/ext/analytics/config?service=<code>`.
@@ -1484,23 +1731,33 @@ interface FFIDAnalyticsConfig {
1484
1731
  * has been fired **3 times within 60 seconds**. Caller should surface a
1485
1732
  * manual "再度ログインする" UI rather than retry automatically (#2406 / #2411).
1486
1733
  *
1487
- * SDK 2.18.0 only ships `'redirect_loop_detected'`. Other failure paths
1488
- * (SSR environment, PKCE generation failure, empty organizationId) currently
1489
- * return `error` without a `code`. New codes will be added in future minor
1490
- * versions — treat this union as forward-extensible and do NOT exhaustively
1491
- * `switch` over it without a `default` branch for consumer code that must
1492
- * stay compatible across SDK upgrades.
1734
+ * - `'state_persistence_failed'`: `redirectToAuthorize()` could not persist
1735
+ * the OAuth CSRF `state` (storage unavailable / quota). Redirect is aborted
1736
+ * fail-closed (#4136) callers should surface a manual retry UI.
1737
+ *
1738
+ * Other failure paths (SSR environment, PKCE generation failure, empty
1739
+ * organizationId) currently return `error` without a `code`. Treat this union
1740
+ * as forward-extensible and do NOT exhaustively `switch` over it without a
1741
+ * `default` branch for consumer code that must stay compatible across SDK
1742
+ * upgrades.
1493
1743
  */
1494
- type FFIDRedirectErrorCode = 'redirect_loop_detected';
1744
+ type FFIDRedirectErrorCode = 'redirect_loop_detected' | 'state_persistence_failed';
1495
1745
  /**
1496
1746
  * Result of a redirect operation (redirectToLogin / redirectToAuthorize / redirectToLogout)
1497
1747
  *
1498
1748
  * Structured return type so callers can inspect failure reasons
1499
1749
  * instead of receiving a bare `false`. When `code` is set, branch on it
1500
1750
  * for programmatic handling; otherwise log `error` for humans.
1751
+ *
1752
+ * `loopDetectionDisabled` (#4119): set to `true` when sessionStorage is
1753
+ * unavailable (iOS WebKit private mode / eviction) so redirect-loop detection
1754
+ * ran **fail-open** — the redirect fired, but repeated-redirect protection is
1755
+ * disabled for this browser session. Callers that need loop protection on
1756
+ * such browsers should add their own max-retry guard.
1501
1757
  */
1502
1758
  type FFIDRedirectResult = {
1503
1759
  success: true;
1760
+ loopDetectionDisabled?: boolean;
1504
1761
  } | {
1505
1762
  success: false;
1506
1763
  error: string;
@@ -1695,7 +1952,7 @@ interface RedirectToAuthorizeOptions {
1695
1952
  /** Creates an FFID API client instance */
1696
1953
  declare function createFFIDClient(config: FFIDConfig): {
1697
1954
  getSession: () => Promise<FFIDApiResponse<FFIDSessionResponse>>;
1698
- signOut: () => Promise<FFIDApiResponse<void>>;
1955
+ signOut: () => Promise<FFIDApiResponse<FFIDSignOutResult>>;
1699
1956
  redirectToLogin: (options?: RedirectToAuthorizeOptions) => Promise<FFIDRedirectResult>;
1700
1957
  redirectToAuthorize: (options?: RedirectToAuthorizeOptions) => Promise<FFIDRedirectResult>;
1701
1958
  redirectToLogout: (postLogoutRedirectUri?: string) => FFIDRedirectResult;
@@ -1732,6 +1989,8 @@ declare function createFFIDClient(config: FFIDConfig): {
1732
1989
  organizationId: string;
1733
1990
  userId: string;
1734
1991
  }) => Promise<FFIDApiResponse<FFIDRemoveMemberResponse>>;
1992
+ provisionUser: (params: FFIDProvisionUserParams) => Promise<FFIDApiResponse<FFIDProvisionUserResponse>>;
1993
+ provisionOrganization: (params: FFIDProvisionOrganizationParams) => Promise<FFIDApiResponse<FFIDProvisionOrganizationResponse>>;
1735
1994
  getProfile: (options?: FFIDProfileCallOptions) => Promise<FFIDApiResponse<FFIDUserProfile>>;
1736
1995
  updateProfile: (data: FFIDUpdateUserProfileRequest, options?: FFIDProfileCallOptions) => Promise<FFIDApiResponse<FFIDUserProfile>>;
1737
1996
  getLoginHistory: (params?: FFIDGetLoginHistoryParams) => Promise<FFIDApiResponse<FFIDLoginHistoryResponse>>;
@@ -1815,4 +2074,4 @@ declare function createFFIDClient(config: FFIDConfig): {
1815
2074
  /** Type of the FFID client */
1816
2075
  type FFIDClient = ReturnType<typeof createFFIDClient>;
1817
2076
 
1818
- export { type FFIDUpdateUserProfileRequest as A, type FFIDUser as B, type FFIDUserProfile as C, type TokenStore as D, createFFIDClient as E, type FFIDLogger as F, createTokenStore as G, type TokenData as T, type FFIDError as a, type FFIDCacheAdapter as b, type FFIDVerifyAccessTokenOptions as c, type FFIDApiResponse as d, type FFIDOAuthUserInfo as e, type FFIDAddMemberParams as f, type FFIDAddMemberRequest as g, type FFIDAddMemberResponse as h, type FFIDAssignableMemberRole as i, type FFIDCacheConfig as j, type FFIDClient as k, type FFIDConfig as l, type FFIDListMembersResponse as m, type FFIDMemberRole as n, type FFIDOrganization as o, type FFIDOrganizationMember as p, type FFIDOtpSendResponse as q, type FFIDOtpVerifyResponse as r, type FFIDPasswordResetConfirmResponse as s, type FFIDPasswordResetResponse as t, type FFIDPasswordResetVerifyResponse as u, type FFIDProfileCallOptions as v, type FFIDRemoveMemberResponse as w, type FFIDResetSessionResponse as x, type FFIDSubscription as y, type FFIDUpdateMemberRoleResponse as z };
2077
+ export { type FFIDProvisionOrganizationDryRun as A, type FFIDProvisionOrganizationMemberInput as B, type FFIDProvisionOrganizationOutcome as C, type FFIDProvisionOrganizationParams as D, type FFIDProvisionOrganizationResponse as E, type FFIDLogger as F, type FFIDProvisionUserDryRun as G, type FFIDProvisionUserOutcome as H, type FFIDProvisionUserParams as I, type FFIDProvisionUserProfileInput as J, type FFIDProvisionUserResponse as K, type FFIDProvisionedOrganization as L, type FFIDProvisionedUser as M, type FFIDRemoveMemberResponse as N, type FFIDResetSessionResponse as O, type FFIDSubscription as P, type FFIDUpdateMemberRoleResponse as Q, type FFIDUpdateUserProfileRequest as R, type FFIDUser as S, type FFIDUserProfile as T, type TokenData as U, type TokenStore as V, createFFIDClient as W, createTokenStore as X, type FFIDError as a, type FFIDCacheAdapter as b, type FFIDVerifyAccessTokenOptions as c, type FFIDApiResponse as d, type FFIDOAuthUserInfo as e, type FFIDAddMemberParams as f, type FFIDAddMemberRequest as g, type FFIDAddMemberResponse as h, type FFIDAssignableMemberRole as i, type FFIDCacheConfig as j, type FFIDClient as k, type FFIDConfig as l, type FFIDListMembersResponse as m, type FFIDMemberRole as n, type FFIDOrganization as o, type FFIDOrganizationMember as p, type FFIDOtpSendResponse as q, type FFIDOtpVerifyResponse as r, type FFIDPasswordResetConfirmResponse as s, type FFIDPasswordResetResponse as t, type FFIDPasswordResetVerifyResponse as u, type FFIDProfileCallOptions as v, type FFIDProvisionMemberPlan as w, type FFIDProvisionMemberPlanStatus as x, type FFIDProvisionMemberResult as y, type FFIDProvisionMemberStatus as z };