@viyv/account-client 0.2.1 → 0.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.
package/dist/react.cjs CHANGED
@@ -13,6 +13,7 @@ function parseEntitlement(wire) {
13
13
  plan: wire.plan,
14
14
  addons: wire.addons ?? [],
15
15
  products: wire.products ?? [],
16
+ productPlans: wire.product_plans ?? {},
16
17
  features: wire.features
17
18
  };
18
19
  }
@@ -20,7 +21,9 @@ function entitlementForProduct(entitlement, product) {
20
21
  if (!entitlement) return null;
21
22
  return {
22
23
  enabled: entitlement.products.includes(product),
23
- plan: entitlement.plan,
24
+ // Per-product billing: report THIS product's own tier (free-start default),
25
+ // not the org-wide effective `plan`.
26
+ plan: entitlement.productPlans[product] ?? "free",
24
27
  addons: entitlement.addons,
25
28
  features: entitlement.features
26
29
  };
@@ -42,6 +45,21 @@ function createAccountClient(config) {
42
45
  email: params.email,
43
46
  password: params.password
44
47
  }),
48
+ signInWithProvider: (provider, options) => client$1.signIn.social({
49
+ provider,
50
+ callbackURL: options.callbackURL,
51
+ errorCallbackURL: options.errorCallbackURL
52
+ }),
53
+ listAccounts: () => client$1.listAccounts(),
54
+ linkSocial: (provider, options) => client$1.linkSocial({
55
+ provider,
56
+ callbackURL: options.callbackURL,
57
+ errorCallbackURL: options.errorCallbackURL
58
+ }),
59
+ unlinkAccount: (params) => client$1.unlinkAccount({
60
+ providerId: params.providerId,
61
+ accountId: params.accountId
62
+ }),
45
63
  signOut: () => client$1.signOut(),
46
64
  getSession: () => client$1.getSession()
47
65
  };
@@ -60,6 +78,7 @@ var SIGNED_OUT = {
60
78
  status: "unauthenticated",
61
79
  user: null,
62
80
  activeOrg: null,
81
+ orgs: [],
63
82
  entitlement: null
64
83
  };
65
84
  async function loadAccount(apiBase, opts = {}) {
@@ -77,20 +96,23 @@ async function loadAccount(apiBase, opts = {}) {
77
96
  name: session.user.name ?? null,
78
97
  image: session.user.image ?? null
79
98
  };
99
+ const wantedOrg = opts.org?.trim() || null;
100
+ const entUrl = wantedOrg ? `${apiBase}/v1/license/entitlement?org=${encodeURIComponent(wantedOrg)}` : `${apiBase}/v1/license/entitlement`;
80
101
  const [ent, orgsBody] = await Promise.all([
81
- opts.skipEntitlement ? Promise.resolve(null) : fetchJson(
82
- doFetch,
83
- `${apiBase}/v1/license/entitlement`,
84
- opts.signal
85
- ),
102
+ opts.skipEntitlement ? Promise.resolve(null) : fetchJson(doFetch, entUrl, opts.signal),
86
103
  fetchJson(doFetch, `${apiBase}/v1/users/me/orgs`, opts.signal)
87
104
  ]);
88
105
  const entitlement = ent ? parseEntitlement(ent) : null;
106
+ const orgsWire = orgsBody?.organizations ?? [];
107
+ const orgs = orgsWire.map((o) => ({
108
+ id: o.organization_id,
109
+ slug: o.slug ?? null,
110
+ name: o.name ?? null
111
+ }));
89
112
  const activeId = session.session?.activeOrganizationId ?? entitlement?.organizationId ?? null;
90
- const orgs = orgsBody?.organizations ?? [];
91
- const match = activeId ? orgs.find((o) => o.organization_id === activeId) : orgs[0];
92
- const activeOrg = match ? { id: match.organization_id, slug: match.slug ?? null, name: match.name ?? null } : activeId ? { id: activeId, slug: null, name: null } : null;
93
- return { status: "authenticated", user, activeOrg, entitlement };
113
+ const match = wantedOrg ? orgs.find((o) => o.slug === wantedOrg || o.id === wantedOrg) : activeId ? orgs.find((o) => o.id === activeId) : orgs[0];
114
+ const activeOrg = match ?? (!wantedOrg && activeId ? { id: activeId, slug: null, name: null } : null);
115
+ return { status: "authenticated", user, activeOrg, orgs, entitlement };
94
116
  }
95
117
  var AccountContext = react.createContext(null);
96
118
  function AccountProvider({
@@ -98,6 +120,7 @@ function AccountProvider({
98
120
  cookieDomain,
99
121
  signInUrl = "https://app.viyv.io/signin",
100
122
  skipEntitlement = false,
123
+ org,
101
124
  devAccount,
102
125
  children
103
126
  }) {
@@ -108,15 +131,17 @@ function AccountProvider({
108
131
  const [status, setStatus] = react.useState("loading");
109
132
  const [user, setUser] = react.useState(null);
110
133
  const [activeOrg, setActiveOrg] = react.useState(null);
134
+ const [orgs, setOrgs] = react.useState([]);
111
135
  const [entitlement, setEntitlement] = react.useState(null);
112
136
  const [error, setError] = react.useState(null);
113
137
  const load = react.useCallback(
114
138
  async (signal) => {
115
139
  try {
116
140
  setError(null);
117
- const result = await loadAccount(apiBase, { skipEntitlement, signal, devAccount });
141
+ const result = await loadAccount(apiBase, { skipEntitlement, org, signal, devAccount });
118
142
  setUser(result.user);
119
143
  setActiveOrg(result.activeOrg);
144
+ setOrgs(result.orgs);
120
145
  setEntitlement(result.entitlement);
121
146
  setStatus(result.status);
122
147
  } catch (err) {
@@ -124,11 +149,12 @@ function AccountProvider({
124
149
  setError(err instanceof Error ? err : new Error(String(err)));
125
150
  setUser(null);
126
151
  setActiveOrg(null);
152
+ setOrgs([]);
127
153
  setEntitlement(null);
128
154
  setStatus("unauthenticated");
129
155
  }
130
156
  },
131
- [apiBase, skipEntitlement, devAccount]
157
+ [apiBase, skipEntitlement, org, devAccount]
132
158
  );
133
159
  react.useEffect(() => {
134
160
  const ctrl = new AbortController();
@@ -147,6 +173,7 @@ function AccountProvider({
147
173
  await clientRef.current?.signOut();
148
174
  setUser(null);
149
175
  setActiveOrg(null);
176
+ setOrgs([]);
150
177
  setEntitlement(null);
151
178
  setStatus("unauthenticated");
152
179
  }, []);
@@ -155,6 +182,7 @@ function AccountProvider({
155
182
  status,
156
183
  user,
157
184
  activeOrg,
185
+ orgs,
158
186
  entitlement,
159
187
  error,
160
188
  refresh: () => load(),
@@ -163,7 +191,7 @@ function AccountProvider({
163
191
  apiBase,
164
192
  signInUrl
165
193
  }),
166
- [status, user, activeOrg, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
194
+ [status, user, activeOrg, orgs, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
167
195
  );
168
196
  return react.createElement(AccountContext.Provider, { value }, children);
169
197
  }
@@ -178,6 +206,17 @@ function useUser() {
178
206
  function useActiveOrg() {
179
207
  return useAccount().activeOrg;
180
208
  }
209
+ function useOrgs() {
210
+ return useAccount().orgs;
211
+ }
212
+ function useOrgFromSlug(slug) {
213
+ const orgs = useAccount().orgs;
214
+ return react.useMemo(() => {
215
+ const wanted = slug?.trim();
216
+ if (!wanted) return null;
217
+ return orgs.find((o) => o.slug === wanted || o.id === wanted) ?? null;
218
+ }, [orgs, slug]);
219
+ }
181
220
  function useEntitlement(product) {
182
221
  const { entitlement } = useAccount();
183
222
  return react.useMemo(() => entitlementForProduct(entitlement, product), [entitlement, product]);
@@ -215,6 +254,8 @@ exports.loadAccount = loadAccount;
215
254
  exports.useAccount = useAccount;
216
255
  exports.useActiveOrg = useActiveOrg;
217
256
  exports.useEntitlement = useEntitlement;
257
+ exports.useOrgFromSlug = useOrgFromSlug;
258
+ exports.useOrgs = useOrgs;
218
259
  exports.useUser = useUser;
219
260
  //# sourceMappingURL=react.cjs.map
220
261
  //# sourceMappingURL=react.cjs.map
package/dist/react.d.cts CHANGED
@@ -43,16 +43,24 @@ interface AccountOrg {
43
43
  /** Shape of GET /v1/license/entitlement (docs/09 A-4). */
44
44
  interface Entitlement {
45
45
  organizationId: string;
46
+ /**
47
+ * The org's effective (highest) tier across its products — a back-compat
48
+ * summary. Per-product billing (2026-06-13): the authoritative per-product
49
+ * tier is in {@link Entitlement.productPlans}.
50
+ */
46
51
  plan: PlanTier;
47
52
  addons: string[];
48
53
  /** Product slugs this org may use (free-start lists the agent family). */
49
54
  products: ProductSlug[];
55
+ /** Per-product tier (each enabled product → its plan; free-start = "free"). */
56
+ productPlans: Partial<Record<ProductSlug, PlanTier>>;
50
57
  features: EntitlementFeatures;
51
58
  }
52
59
  /** Per-product entitlement view returned by useEntitlement(slug). */
53
60
  interface ProductEntitlement {
54
61
  /** Whether the product is enabled for this org. */
55
62
  enabled: boolean;
63
+ /** THIS product's tier (per-product billing) — not the org-wide summary. */
56
64
  plan: PlanTier;
57
65
  addons: string[];
58
66
  /** The full feature map (callers branch on the flags they care about). */
@@ -64,6 +72,8 @@ interface LoadedAccount {
64
72
  status: "authenticated" | "unauthenticated";
65
73
  user: AccountUser | null;
66
74
  activeOrg: AccountOrg | null;
75
+ /** Every org the user belongs to (for switchers + URL-slug resolution, docs/14). */
76
+ orgs: AccountOrg[];
67
77
  entitlement: Entitlement | null;
68
78
  }
69
79
  /**
@@ -76,6 +86,13 @@ declare function loadAccount(apiBase: string, opts?: {
76
86
  skipEntitlement?: boolean;
77
87
  signal?: AbortSignal;
78
88
  fetchImpl?: typeof fetch;
89
+ /**
90
+ * URL-scoped org (docs/14): an org id OR slug. When set, the resolved
91
+ * `activeOrg` is THIS org (membership-verified via the user's org list) and
92
+ * the entitlement is read for it (`?org=`), instead of the cookie active org.
93
+ * Omit for the legacy "session active org" behavior.
94
+ */
95
+ org?: string | null;
79
96
  /**
80
97
  * Dev escape hatch: when supplied, short-circuit ALL network I/O and resolve
81
98
  * to this synthetic account. Intended for local `next dev` against an
@@ -86,7 +103,15 @@ declare function loadAccount(apiBase: string, opts?: {
86
103
  interface AccountContextValue {
87
104
  status: AccountStatus;
88
105
  user: AccountUser | null;
106
+ /**
107
+ * The org in scope: the URL org when <AccountProvider org=...> is set
108
+ * (docs/14), else the session's default/active org. Read it for "the org this
109
+ * page is about". For "which org should a bare entry default to", the session
110
+ * active org is what the API falls back to.
111
+ */
89
112
  activeOrg: AccountOrg | null;
113
+ /** Every org the user belongs to (switchers + slug guards). */
114
+ orgs: AccountOrg[];
90
115
  entitlement: Entitlement | null;
91
116
  error: Error | null;
92
117
  /** Re-fetch session + entitlement (e.g. after returning from checkout). */
@@ -107,6 +132,13 @@ interface AccountProviderProps {
107
132
  signInUrl?: string;
108
133
  /** Skip the entitlement fetch (session-only consumers). Default false. */
109
134
  skipEntitlement?: boolean;
135
+ /**
136
+ * URL-scoped org (docs/14): an org id OR slug, typically the `:slug` segment of
137
+ * a `/o/:slug/...` route. When set, `activeOrg` + `entitlement` reflect THIS
138
+ * org (membership-verified), not the cookie active org — so switching org in a
139
+ * sibling product never re-scopes this one. Changing it re-loads the provider.
140
+ */
141
+ org?: string | null;
110
142
  /**
111
143
  * Dev escape hatch: a synthetic account to render instead of fetching from
112
144
  * api.viyv.io. Intended for local `next dev` (gate it on a NEXT_PUBLIC_* flag
@@ -115,17 +147,30 @@ interface AccountProviderProps {
115
147
  devAccount?: LoadedAccount | null;
116
148
  children?: ReactNode;
117
149
  }
118
- declare function AccountProvider({ apiBase, cookieDomain, signInUrl, skipEntitlement, devAccount, children, }: AccountProviderProps): react.FunctionComponentElement<react.ProviderProps<AccountContextValue | null>>;
150
+ declare function AccountProvider({ apiBase, cookieDomain, signInUrl, skipEntitlement, org, devAccount, children, }: AccountProviderProps): react.FunctionComponentElement<react.ProviderProps<AccountContextValue | null>>;
119
151
  /** Read the full account context. Throws if used outside <AccountProvider>. */
120
152
  declare function useAccount(): AccountContextValue;
121
153
  /** The signed-in user, or null when unauthenticated/loading. */
122
154
  declare function useUser(): AccountUser | null;
123
- /** The active organization, or null. */
155
+ /**
156
+ * The org in scope, or null. When <AccountProvider org=...> is set this is the
157
+ * URL org (docs/14); otherwise the session's default/active org. Treat it as
158
+ * "the org this page is about", not a global mutable selection.
159
+ */
124
160
  declare function useActiveOrg(): AccountOrg | null;
161
+ /** Every org the signed-in user belongs to (for switchers). Empty when signed out. */
162
+ declare function useOrgs(): AccountOrg[];
163
+ /**
164
+ * Resolve a URL slug (or org id) to one of the user's orgs, or null when they
165
+ * are not a member (docs/14). Use it in a `/o/:slug` layout to decide between
166
+ * rendering and a 404 / org-picker — the resolution is purely client-side over
167
+ * the already-fetched org list, so it never leaks non-member orgs.
168
+ */
169
+ declare function useOrgFromSlug(slug: string | null | undefined): AccountOrg | null;
125
170
  /**
126
171
  * Per-product entitlement view. Returns null while loading / unauthenticated.
127
172
  * Web products gate features on the returned `enabled` + `features` flags.
128
173
  */
129
174
  declare function useEntitlement(product: ProductSlug): ProductEntitlement | null;
130
175
 
131
- export { type AccountContextValue, type AccountOrg, AccountProvider, type AccountProviderProps, type AccountStatus, type AccountUser, type Entitlement, type LoadedAccount, type ProductEntitlement, SignInButton, type SignInButtonProps, SignOutButton, type SignOutButtonProps, loadAccount, useAccount, useActiveOrg, useEntitlement, useUser };
176
+ export { type AccountContextValue, type AccountOrg, AccountProvider, type AccountProviderProps, type AccountStatus, type AccountUser, type Entitlement, type LoadedAccount, type ProductEntitlement, SignInButton, type SignInButtonProps, SignOutButton, type SignOutButtonProps, loadAccount, useAccount, useActiveOrg, useEntitlement, useOrgFromSlug, useOrgs, useUser };
package/dist/react.d.ts CHANGED
@@ -43,16 +43,24 @@ interface AccountOrg {
43
43
  /** Shape of GET /v1/license/entitlement (docs/09 A-4). */
44
44
  interface Entitlement {
45
45
  organizationId: string;
46
+ /**
47
+ * The org's effective (highest) tier across its products — a back-compat
48
+ * summary. Per-product billing (2026-06-13): the authoritative per-product
49
+ * tier is in {@link Entitlement.productPlans}.
50
+ */
46
51
  plan: PlanTier;
47
52
  addons: string[];
48
53
  /** Product slugs this org may use (free-start lists the agent family). */
49
54
  products: ProductSlug[];
55
+ /** Per-product tier (each enabled product → its plan; free-start = "free"). */
56
+ productPlans: Partial<Record<ProductSlug, PlanTier>>;
50
57
  features: EntitlementFeatures;
51
58
  }
52
59
  /** Per-product entitlement view returned by useEntitlement(slug). */
53
60
  interface ProductEntitlement {
54
61
  /** Whether the product is enabled for this org. */
55
62
  enabled: boolean;
63
+ /** THIS product's tier (per-product billing) — not the org-wide summary. */
56
64
  plan: PlanTier;
57
65
  addons: string[];
58
66
  /** The full feature map (callers branch on the flags they care about). */
@@ -64,6 +72,8 @@ interface LoadedAccount {
64
72
  status: "authenticated" | "unauthenticated";
65
73
  user: AccountUser | null;
66
74
  activeOrg: AccountOrg | null;
75
+ /** Every org the user belongs to (for switchers + URL-slug resolution, docs/14). */
76
+ orgs: AccountOrg[];
67
77
  entitlement: Entitlement | null;
68
78
  }
69
79
  /**
@@ -76,6 +86,13 @@ declare function loadAccount(apiBase: string, opts?: {
76
86
  skipEntitlement?: boolean;
77
87
  signal?: AbortSignal;
78
88
  fetchImpl?: typeof fetch;
89
+ /**
90
+ * URL-scoped org (docs/14): an org id OR slug. When set, the resolved
91
+ * `activeOrg` is THIS org (membership-verified via the user's org list) and
92
+ * the entitlement is read for it (`?org=`), instead of the cookie active org.
93
+ * Omit for the legacy "session active org" behavior.
94
+ */
95
+ org?: string | null;
79
96
  /**
80
97
  * Dev escape hatch: when supplied, short-circuit ALL network I/O and resolve
81
98
  * to this synthetic account. Intended for local `next dev` against an
@@ -86,7 +103,15 @@ declare function loadAccount(apiBase: string, opts?: {
86
103
  interface AccountContextValue {
87
104
  status: AccountStatus;
88
105
  user: AccountUser | null;
106
+ /**
107
+ * The org in scope: the URL org when <AccountProvider org=...> is set
108
+ * (docs/14), else the session's default/active org. Read it for "the org this
109
+ * page is about". For "which org should a bare entry default to", the session
110
+ * active org is what the API falls back to.
111
+ */
89
112
  activeOrg: AccountOrg | null;
113
+ /** Every org the user belongs to (switchers + slug guards). */
114
+ orgs: AccountOrg[];
90
115
  entitlement: Entitlement | null;
91
116
  error: Error | null;
92
117
  /** Re-fetch session + entitlement (e.g. after returning from checkout). */
@@ -107,6 +132,13 @@ interface AccountProviderProps {
107
132
  signInUrl?: string;
108
133
  /** Skip the entitlement fetch (session-only consumers). Default false. */
109
134
  skipEntitlement?: boolean;
135
+ /**
136
+ * URL-scoped org (docs/14): an org id OR slug, typically the `:slug` segment of
137
+ * a `/o/:slug/...` route. When set, `activeOrg` + `entitlement` reflect THIS
138
+ * org (membership-verified), not the cookie active org — so switching org in a
139
+ * sibling product never re-scopes this one. Changing it re-loads the provider.
140
+ */
141
+ org?: string | null;
110
142
  /**
111
143
  * Dev escape hatch: a synthetic account to render instead of fetching from
112
144
  * api.viyv.io. Intended for local `next dev` (gate it on a NEXT_PUBLIC_* flag
@@ -115,17 +147,30 @@ interface AccountProviderProps {
115
147
  devAccount?: LoadedAccount | null;
116
148
  children?: ReactNode;
117
149
  }
118
- declare function AccountProvider({ apiBase, cookieDomain, signInUrl, skipEntitlement, devAccount, children, }: AccountProviderProps): react.FunctionComponentElement<react.ProviderProps<AccountContextValue | null>>;
150
+ declare function AccountProvider({ apiBase, cookieDomain, signInUrl, skipEntitlement, org, devAccount, children, }: AccountProviderProps): react.FunctionComponentElement<react.ProviderProps<AccountContextValue | null>>;
119
151
  /** Read the full account context. Throws if used outside <AccountProvider>. */
120
152
  declare function useAccount(): AccountContextValue;
121
153
  /** The signed-in user, or null when unauthenticated/loading. */
122
154
  declare function useUser(): AccountUser | null;
123
- /** The active organization, or null. */
155
+ /**
156
+ * The org in scope, or null. When <AccountProvider org=...> is set this is the
157
+ * URL org (docs/14); otherwise the session's default/active org. Treat it as
158
+ * "the org this page is about", not a global mutable selection.
159
+ */
124
160
  declare function useActiveOrg(): AccountOrg | null;
161
+ /** Every org the signed-in user belongs to (for switchers). Empty when signed out. */
162
+ declare function useOrgs(): AccountOrg[];
163
+ /**
164
+ * Resolve a URL slug (or org id) to one of the user's orgs, or null when they
165
+ * are not a member (docs/14). Use it in a `/o/:slug` layout to decide between
166
+ * rendering and a 404 / org-picker — the resolution is purely client-side over
167
+ * the already-fetched org list, so it never leaks non-member orgs.
168
+ */
169
+ declare function useOrgFromSlug(slug: string | null | undefined): AccountOrg | null;
125
170
  /**
126
171
  * Per-product entitlement view. Returns null while loading / unauthenticated.
127
172
  * Web products gate features on the returned `enabled` + `features` flags.
128
173
  */
129
174
  declare function useEntitlement(product: ProductSlug): ProductEntitlement | null;
130
175
 
131
- export { type AccountContextValue, type AccountOrg, AccountProvider, type AccountProviderProps, type AccountStatus, type AccountUser, type Entitlement, type LoadedAccount, type ProductEntitlement, SignInButton, type SignInButtonProps, SignOutButton, type SignOutButtonProps, loadAccount, useAccount, useActiveOrg, useEntitlement, useUser };
176
+ export { type AccountContextValue, type AccountOrg, AccountProvider, type AccountProviderProps, type AccountStatus, type AccountUser, type Entitlement, type LoadedAccount, type ProductEntitlement, SignInButton, type SignInButtonProps, SignOutButton, type SignOutButtonProps, loadAccount, useAccount, useActiveOrg, useEntitlement, useOrgFromSlug, useOrgs, useUser };
package/dist/react.js CHANGED
@@ -11,6 +11,7 @@ function parseEntitlement(wire) {
11
11
  plan: wire.plan,
12
12
  addons: wire.addons ?? [],
13
13
  products: wire.products ?? [],
14
+ productPlans: wire.product_plans ?? {},
14
15
  features: wire.features
15
16
  };
16
17
  }
@@ -18,7 +19,9 @@ function entitlementForProduct(entitlement, product) {
18
19
  if (!entitlement) return null;
19
20
  return {
20
21
  enabled: entitlement.products.includes(product),
21
- plan: entitlement.plan,
22
+ // Per-product billing: report THIS product's own tier (free-start default),
23
+ // not the org-wide effective `plan`.
24
+ plan: entitlement.productPlans[product] ?? "free",
22
25
  addons: entitlement.addons,
23
26
  features: entitlement.features
24
27
  };
@@ -40,6 +43,21 @@ function createAccountClient(config) {
40
43
  email: params.email,
41
44
  password: params.password
42
45
  }),
46
+ signInWithProvider: (provider, options) => client.signIn.social({
47
+ provider,
48
+ callbackURL: options.callbackURL,
49
+ errorCallbackURL: options.errorCallbackURL
50
+ }),
51
+ listAccounts: () => client.listAccounts(),
52
+ linkSocial: (provider, options) => client.linkSocial({
53
+ provider,
54
+ callbackURL: options.callbackURL,
55
+ errorCallbackURL: options.errorCallbackURL
56
+ }),
57
+ unlinkAccount: (params) => client.unlinkAccount({
58
+ providerId: params.providerId,
59
+ accountId: params.accountId
60
+ }),
43
61
  signOut: () => client.signOut(),
44
62
  getSession: () => client.getSession()
45
63
  };
@@ -58,6 +76,7 @@ var SIGNED_OUT = {
58
76
  status: "unauthenticated",
59
77
  user: null,
60
78
  activeOrg: null,
79
+ orgs: [],
61
80
  entitlement: null
62
81
  };
63
82
  async function loadAccount(apiBase, opts = {}) {
@@ -75,20 +94,23 @@ async function loadAccount(apiBase, opts = {}) {
75
94
  name: session.user.name ?? null,
76
95
  image: session.user.image ?? null
77
96
  };
97
+ const wantedOrg = opts.org?.trim() || null;
98
+ const entUrl = wantedOrg ? `${apiBase}/v1/license/entitlement?org=${encodeURIComponent(wantedOrg)}` : `${apiBase}/v1/license/entitlement`;
78
99
  const [ent, orgsBody] = await Promise.all([
79
- opts.skipEntitlement ? Promise.resolve(null) : fetchJson(
80
- doFetch,
81
- `${apiBase}/v1/license/entitlement`,
82
- opts.signal
83
- ),
100
+ opts.skipEntitlement ? Promise.resolve(null) : fetchJson(doFetch, entUrl, opts.signal),
84
101
  fetchJson(doFetch, `${apiBase}/v1/users/me/orgs`, opts.signal)
85
102
  ]);
86
103
  const entitlement = ent ? parseEntitlement(ent) : null;
104
+ const orgsWire = orgsBody?.organizations ?? [];
105
+ const orgs = orgsWire.map((o) => ({
106
+ id: o.organization_id,
107
+ slug: o.slug ?? null,
108
+ name: o.name ?? null
109
+ }));
87
110
  const activeId = session.session?.activeOrganizationId ?? entitlement?.organizationId ?? null;
88
- const orgs = orgsBody?.organizations ?? [];
89
- const match = activeId ? orgs.find((o) => o.organization_id === activeId) : orgs[0];
90
- const activeOrg = match ? { id: match.organization_id, slug: match.slug ?? null, name: match.name ?? null } : activeId ? { id: activeId, slug: null, name: null } : null;
91
- return { status: "authenticated", user, activeOrg, entitlement };
111
+ const match = wantedOrg ? orgs.find((o) => o.slug === wantedOrg || o.id === wantedOrg) : activeId ? orgs.find((o) => o.id === activeId) : orgs[0];
112
+ const activeOrg = match ?? (!wantedOrg && activeId ? { id: activeId, slug: null, name: null } : null);
113
+ return { status: "authenticated", user, activeOrg, orgs, entitlement };
92
114
  }
93
115
  var AccountContext = createContext(null);
94
116
  function AccountProvider({
@@ -96,6 +118,7 @@ function AccountProvider({
96
118
  cookieDomain,
97
119
  signInUrl = "https://app.viyv.io/signin",
98
120
  skipEntitlement = false,
121
+ org,
99
122
  devAccount,
100
123
  children
101
124
  }) {
@@ -106,15 +129,17 @@ function AccountProvider({
106
129
  const [status, setStatus] = useState("loading");
107
130
  const [user, setUser] = useState(null);
108
131
  const [activeOrg, setActiveOrg] = useState(null);
132
+ const [orgs, setOrgs] = useState([]);
109
133
  const [entitlement, setEntitlement] = useState(null);
110
134
  const [error, setError] = useState(null);
111
135
  const load = useCallback(
112
136
  async (signal) => {
113
137
  try {
114
138
  setError(null);
115
- const result = await loadAccount(apiBase, { skipEntitlement, signal, devAccount });
139
+ const result = await loadAccount(apiBase, { skipEntitlement, org, signal, devAccount });
116
140
  setUser(result.user);
117
141
  setActiveOrg(result.activeOrg);
142
+ setOrgs(result.orgs);
118
143
  setEntitlement(result.entitlement);
119
144
  setStatus(result.status);
120
145
  } catch (err) {
@@ -122,11 +147,12 @@ function AccountProvider({
122
147
  setError(err instanceof Error ? err : new Error(String(err)));
123
148
  setUser(null);
124
149
  setActiveOrg(null);
150
+ setOrgs([]);
125
151
  setEntitlement(null);
126
152
  setStatus("unauthenticated");
127
153
  }
128
154
  },
129
- [apiBase, skipEntitlement, devAccount]
155
+ [apiBase, skipEntitlement, org, devAccount]
130
156
  );
131
157
  useEffect(() => {
132
158
  const ctrl = new AbortController();
@@ -145,6 +171,7 @@ function AccountProvider({
145
171
  await clientRef.current?.signOut();
146
172
  setUser(null);
147
173
  setActiveOrg(null);
174
+ setOrgs([]);
148
175
  setEntitlement(null);
149
176
  setStatus("unauthenticated");
150
177
  }, []);
@@ -153,6 +180,7 @@ function AccountProvider({
153
180
  status,
154
181
  user,
155
182
  activeOrg,
183
+ orgs,
156
184
  entitlement,
157
185
  error,
158
186
  refresh: () => load(),
@@ -161,7 +189,7 @@ function AccountProvider({
161
189
  apiBase,
162
190
  signInUrl
163
191
  }),
164
- [status, user, activeOrg, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
192
+ [status, user, activeOrg, orgs, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
165
193
  );
166
194
  return createElement(AccountContext.Provider, { value }, children);
167
195
  }
@@ -176,6 +204,17 @@ function useUser() {
176
204
  function useActiveOrg() {
177
205
  return useAccount().activeOrg;
178
206
  }
207
+ function useOrgs() {
208
+ return useAccount().orgs;
209
+ }
210
+ function useOrgFromSlug(slug) {
211
+ const orgs = useAccount().orgs;
212
+ return useMemo(() => {
213
+ const wanted = slug?.trim();
214
+ if (!wanted) return null;
215
+ return orgs.find((o) => o.slug === wanted || o.id === wanted) ?? null;
216
+ }, [orgs, slug]);
217
+ }
179
218
  function useEntitlement(product) {
180
219
  const { entitlement } = useAccount();
181
220
  return useMemo(() => entitlementForProduct(entitlement, product), [entitlement, product]);
@@ -206,6 +245,6 @@ function SignOutButton({ children, className, onSignedOut }) {
206
245
  );
207
246
  }
208
247
 
209
- export { AccountProvider, SignInButton, SignOutButton, loadAccount, useAccount, useActiveOrg, useEntitlement, useUser };
248
+ export { AccountProvider, SignInButton, SignOutButton, loadAccount, useAccount, useActiveOrg, useEntitlement, useOrgFromSlug, useOrgs, useUser };
210
249
  //# sourceMappingURL=react.js.map
211
250
  //# sourceMappingURL=react.js.map
package/dist/server.cjs CHANGED
@@ -7,6 +7,7 @@ function parseEntitlement(wire) {
7
7
  plan: wire.plan,
8
8
  addons: wire.addons ?? [],
9
9
  products: wire.products ?? [],
10
+ productPlans: wire.product_plans ?? {},
10
11
  features: wire.features
11
12
  };
12
13
  }
@@ -38,14 +39,35 @@ async function getCurrentSession(config) {
38
39
  };
39
40
  }
40
41
  async function getEntitlement(config) {
42
+ const qs = config.org ? `?org=${encodeURIComponent(config.org)}` : "";
41
43
  const wire = await getJson(
42
- `${config.apiBase}/v1/license/entitlement`,
44
+ `${config.apiBase}/v1/license/entitlement${qs}`,
43
45
  config.cookie
44
46
  );
45
47
  return wire ? parseEntitlement(wire) : null;
46
48
  }
49
+ async function listUserOrgs(config) {
50
+ const body = await getJson(
51
+ `${config.apiBase}/v1/users/me/orgs`,
52
+ config.cookie
53
+ );
54
+ return (body?.organizations ?? []).map((o) => ({
55
+ id: o.organization_id,
56
+ slug: o.slug ?? null,
57
+ name: o.name ?? null,
58
+ role: o.role ?? null
59
+ }));
60
+ }
61
+ async function resolveOrgFromSlug(config) {
62
+ const wanted = config.slug.trim();
63
+ if (!wanted) return null;
64
+ const orgs = await listUserOrgs(config);
65
+ return orgs.find((o) => o.slug === wanted || o.id === wanted) ?? null;
66
+ }
47
67
 
48
68
  exports.getCurrentSession = getCurrentSession;
49
69
  exports.getEntitlement = getEntitlement;
70
+ exports.listUserOrgs = listUserOrgs;
71
+ exports.resolveOrgFromSlug = resolveOrgFromSlug;
50
72
  //# sourceMappingURL=server.cjs.map
51
73
  //# sourceMappingURL=server.cjs.map
package/dist/server.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { a as AccountUser, E as Entitlement } from './entitlement-B3pXM2vO.cjs';
1
+ import { a as AccountUser, E as Entitlement } from './entitlement-CfKncUyH.cjs';
2
+ export { A as AccountOrg } from './entitlement-CfKncUyH.cjs';
2
3
  import '@viyv/shared';
3
4
 
4
5
  interface ServerSessionConfig {
@@ -10,12 +11,39 @@ interface ServerSessionConfig {
10
11
  */
11
12
  cookie: string;
12
13
  }
14
+ /** An org the signed-in user belongs to, plus their role in it. */
15
+ interface ResolvedOrg {
16
+ id: string;
17
+ slug: string | null;
18
+ name: string | null;
19
+ /** The signed-in user's role in this org (e.g. "owner" | "admin" | "member"). */
20
+ role: string | null;
21
+ }
13
22
  /** Resolve the signed-in user from forwarded cookies (or null). */
14
23
  declare function getCurrentSession(config: ServerSessionConfig): Promise<{
15
24
  user: AccountUser;
16
25
  activeOrganizationId: string | null;
17
26
  } | null>;
18
- /** Resolve the org entitlement from forwarded cookies (or null). */
19
- declare function getEntitlement(config: ServerSessionConfig): Promise<Entitlement | null>;
27
+ /**
28
+ * Resolve the org entitlement from forwarded cookies (or null).
29
+ *
30
+ * Pass `org` (an org id or slug) to read a SPECIFIC org's entitlement — the
31
+ * URL-scoped read (docs/14). The API membership-verifies it; a non-member org
32
+ * resolves to null. Omit `org` for the legacy cookie active-org behavior.
33
+ */
34
+ declare function getEntitlement(config: ServerSessionConfig & {
35
+ org?: string | null;
36
+ }): Promise<Entitlement | null>;
37
+ /** List every org the signed-in user belongs to (from forwarded cookies). */
38
+ declare function listUserOrgs(config: ServerSessionConfig): Promise<ResolvedOrg[]>;
39
+ /**
40
+ * Resolve the org named by a URL slug (or id) to a membership-verified org, or
41
+ * null when the signed-in user is NOT a member of it (docs/14). This is the
42
+ * server-side seam a product uses to turn `/o/:slug/...` into the org it scopes
43
+ * its queries to — the URL says *which* org, this verifies the user may see it.
44
+ */
45
+ declare function resolveOrgFromSlug(config: ServerSessionConfig & {
46
+ slug: string;
47
+ }): Promise<ResolvedOrg | null>;
20
48
 
21
- export { type ServerSessionConfig, getCurrentSession, getEntitlement };
49
+ export { type ResolvedOrg, type ServerSessionConfig, getCurrentSession, getEntitlement, listUserOrgs, resolveOrgFromSlug };