@viyv/account-client 0.2.0 → 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
@@ -1,8 +1,11 @@
1
+ "use client";
1
2
  'use strict';
2
3
 
3
4
  var react = require('react');
4
5
  var client = require('better-auth/client');
5
6
 
7
+ // src/components.tsx
8
+
6
9
  // src/entitlement.ts
7
10
  function parseEntitlement(wire) {
8
11
  return {
@@ -10,6 +13,7 @@ function parseEntitlement(wire) {
10
13
  plan: wire.plan,
11
14
  addons: wire.addons ?? [],
12
15
  products: wire.products ?? [],
16
+ productPlans: wire.product_plans ?? {},
13
17
  features: wire.features
14
18
  };
15
19
  }
@@ -17,7 +21,9 @@ function entitlementForProduct(entitlement, product) {
17
21
  if (!entitlement) return null;
18
22
  return {
19
23
  enabled: entitlement.products.includes(product),
20
- 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",
21
27
  addons: entitlement.addons,
22
28
  features: entitlement.features
23
29
  };
@@ -39,6 +45,21 @@ function createAccountClient(config) {
39
45
  email: params.email,
40
46
  password: params.password
41
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
+ }),
42
63
  signOut: () => client$1.signOut(),
43
64
  getSession: () => client$1.getSession()
44
65
  };
@@ -57,6 +78,7 @@ var SIGNED_OUT = {
57
78
  status: "unauthenticated",
58
79
  user: null,
59
80
  activeOrg: null,
81
+ orgs: [],
60
82
  entitlement: null
61
83
  };
62
84
  async function loadAccount(apiBase, opts = {}) {
@@ -74,20 +96,23 @@ async function loadAccount(apiBase, opts = {}) {
74
96
  name: session.user.name ?? null,
75
97
  image: session.user.image ?? null
76
98
  };
99
+ const wantedOrg = opts.org?.trim() || null;
100
+ const entUrl = wantedOrg ? `${apiBase}/v1/license/entitlement?org=${encodeURIComponent(wantedOrg)}` : `${apiBase}/v1/license/entitlement`;
77
101
  const [ent, orgsBody] = await Promise.all([
78
- opts.skipEntitlement ? Promise.resolve(null) : fetchJson(
79
- doFetch,
80
- `${apiBase}/v1/license/entitlement`,
81
- opts.signal
82
- ),
102
+ opts.skipEntitlement ? Promise.resolve(null) : fetchJson(doFetch, entUrl, opts.signal),
83
103
  fetchJson(doFetch, `${apiBase}/v1/users/me/orgs`, opts.signal)
84
104
  ]);
85
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
+ }));
86
112
  const activeId = session.session?.activeOrganizationId ?? entitlement?.organizationId ?? null;
87
- const orgs = orgsBody?.organizations ?? [];
88
- const match = activeId ? orgs.find((o) => o.organization_id === activeId) : orgs[0];
89
- const activeOrg = match ? { id: match.organization_id, slug: match.slug ?? null, name: match.name ?? null } : activeId ? { id: activeId, slug: null, name: null } : null;
90
- 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 };
91
116
  }
92
117
  var AccountContext = react.createContext(null);
93
118
  function AccountProvider({
@@ -95,6 +120,7 @@ function AccountProvider({
95
120
  cookieDomain,
96
121
  signInUrl = "https://app.viyv.io/signin",
97
122
  skipEntitlement = false,
123
+ org,
98
124
  devAccount,
99
125
  children
100
126
  }) {
@@ -105,15 +131,17 @@ function AccountProvider({
105
131
  const [status, setStatus] = react.useState("loading");
106
132
  const [user, setUser] = react.useState(null);
107
133
  const [activeOrg, setActiveOrg] = react.useState(null);
134
+ const [orgs, setOrgs] = react.useState([]);
108
135
  const [entitlement, setEntitlement] = react.useState(null);
109
136
  const [error, setError] = react.useState(null);
110
137
  const load = react.useCallback(
111
138
  async (signal) => {
112
139
  try {
113
140
  setError(null);
114
- const result = await loadAccount(apiBase, { skipEntitlement, signal, devAccount });
141
+ const result = await loadAccount(apiBase, { skipEntitlement, org, signal, devAccount });
115
142
  setUser(result.user);
116
143
  setActiveOrg(result.activeOrg);
144
+ setOrgs(result.orgs);
117
145
  setEntitlement(result.entitlement);
118
146
  setStatus(result.status);
119
147
  } catch (err) {
@@ -121,11 +149,12 @@ function AccountProvider({
121
149
  setError(err instanceof Error ? err : new Error(String(err)));
122
150
  setUser(null);
123
151
  setActiveOrg(null);
152
+ setOrgs([]);
124
153
  setEntitlement(null);
125
154
  setStatus("unauthenticated");
126
155
  }
127
156
  },
128
- [apiBase, skipEntitlement, devAccount]
157
+ [apiBase, skipEntitlement, org, devAccount]
129
158
  );
130
159
  react.useEffect(() => {
131
160
  const ctrl = new AbortController();
@@ -144,6 +173,7 @@ function AccountProvider({
144
173
  await clientRef.current?.signOut();
145
174
  setUser(null);
146
175
  setActiveOrg(null);
176
+ setOrgs([]);
147
177
  setEntitlement(null);
148
178
  setStatus("unauthenticated");
149
179
  }, []);
@@ -152,6 +182,7 @@ function AccountProvider({
152
182
  status,
153
183
  user,
154
184
  activeOrg,
185
+ orgs,
155
186
  entitlement,
156
187
  error,
157
188
  refresh: () => load(),
@@ -160,7 +191,7 @@ function AccountProvider({
160
191
  apiBase,
161
192
  signInUrl
162
193
  }),
163
- [status, user, activeOrg, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
194
+ [status, user, activeOrg, orgs, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
164
195
  );
165
196
  return react.createElement(AccountContext.Provider, { value }, children);
166
197
  }
@@ -175,6 +206,17 @@ function useUser() {
175
206
  function useActiveOrg() {
176
207
  return useAccount().activeOrg;
177
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
+ }
178
220
  function useEntitlement(product) {
179
221
  const { entitlement } = useAccount();
180
222
  return react.useMemo(() => entitlementForProduct(entitlement, product), [entitlement, product]);
@@ -212,6 +254,8 @@ exports.loadAccount = loadAccount;
212
254
  exports.useAccount = useAccount;
213
255
  exports.useActiveOrg = useActiveOrg;
214
256
  exports.useEntitlement = useEntitlement;
257
+ exports.useOrgFromSlug = useOrgFromSlug;
258
+ exports.useOrgs = useOrgs;
215
259
  exports.useUser = useUser;
216
260
  //# sourceMappingURL=react.cjs.map
217
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
@@ -1,6 +1,9 @@
1
+ "use client";
1
2
  import { createContext, useRef, useState, useCallback, useEffect, useMemo, createElement, useContext } from 'react';
2
3
  import { createAuthClient } from 'better-auth/client';
3
4
 
5
+ // src/components.tsx
6
+
4
7
  // src/entitlement.ts
5
8
  function parseEntitlement(wire) {
6
9
  return {
@@ -8,6 +11,7 @@ function parseEntitlement(wire) {
8
11
  plan: wire.plan,
9
12
  addons: wire.addons ?? [],
10
13
  products: wire.products ?? [],
14
+ productPlans: wire.product_plans ?? {},
11
15
  features: wire.features
12
16
  };
13
17
  }
@@ -15,7 +19,9 @@ function entitlementForProduct(entitlement, product) {
15
19
  if (!entitlement) return null;
16
20
  return {
17
21
  enabled: entitlement.products.includes(product),
18
- 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",
19
25
  addons: entitlement.addons,
20
26
  features: entitlement.features
21
27
  };
@@ -37,6 +43,21 @@ function createAccountClient(config) {
37
43
  email: params.email,
38
44
  password: params.password
39
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
+ }),
40
61
  signOut: () => client.signOut(),
41
62
  getSession: () => client.getSession()
42
63
  };
@@ -55,6 +76,7 @@ var SIGNED_OUT = {
55
76
  status: "unauthenticated",
56
77
  user: null,
57
78
  activeOrg: null,
79
+ orgs: [],
58
80
  entitlement: null
59
81
  };
60
82
  async function loadAccount(apiBase, opts = {}) {
@@ -72,20 +94,23 @@ async function loadAccount(apiBase, opts = {}) {
72
94
  name: session.user.name ?? null,
73
95
  image: session.user.image ?? null
74
96
  };
97
+ const wantedOrg = opts.org?.trim() || null;
98
+ const entUrl = wantedOrg ? `${apiBase}/v1/license/entitlement?org=${encodeURIComponent(wantedOrg)}` : `${apiBase}/v1/license/entitlement`;
75
99
  const [ent, orgsBody] = await Promise.all([
76
- opts.skipEntitlement ? Promise.resolve(null) : fetchJson(
77
- doFetch,
78
- `${apiBase}/v1/license/entitlement`,
79
- opts.signal
80
- ),
100
+ opts.skipEntitlement ? Promise.resolve(null) : fetchJson(doFetch, entUrl, opts.signal),
81
101
  fetchJson(doFetch, `${apiBase}/v1/users/me/orgs`, opts.signal)
82
102
  ]);
83
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
+ }));
84
110
  const activeId = session.session?.activeOrganizationId ?? entitlement?.organizationId ?? null;
85
- const orgs = orgsBody?.organizations ?? [];
86
- const match = activeId ? orgs.find((o) => o.organization_id === activeId) : orgs[0];
87
- const activeOrg = match ? { id: match.organization_id, slug: match.slug ?? null, name: match.name ?? null } : activeId ? { id: activeId, slug: null, name: null } : null;
88
- 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 };
89
114
  }
90
115
  var AccountContext = createContext(null);
91
116
  function AccountProvider({
@@ -93,6 +118,7 @@ function AccountProvider({
93
118
  cookieDomain,
94
119
  signInUrl = "https://app.viyv.io/signin",
95
120
  skipEntitlement = false,
121
+ org,
96
122
  devAccount,
97
123
  children
98
124
  }) {
@@ -103,15 +129,17 @@ function AccountProvider({
103
129
  const [status, setStatus] = useState("loading");
104
130
  const [user, setUser] = useState(null);
105
131
  const [activeOrg, setActiveOrg] = useState(null);
132
+ const [orgs, setOrgs] = useState([]);
106
133
  const [entitlement, setEntitlement] = useState(null);
107
134
  const [error, setError] = useState(null);
108
135
  const load = useCallback(
109
136
  async (signal) => {
110
137
  try {
111
138
  setError(null);
112
- const result = await loadAccount(apiBase, { skipEntitlement, signal, devAccount });
139
+ const result = await loadAccount(apiBase, { skipEntitlement, org, signal, devAccount });
113
140
  setUser(result.user);
114
141
  setActiveOrg(result.activeOrg);
142
+ setOrgs(result.orgs);
115
143
  setEntitlement(result.entitlement);
116
144
  setStatus(result.status);
117
145
  } catch (err) {
@@ -119,11 +147,12 @@ function AccountProvider({
119
147
  setError(err instanceof Error ? err : new Error(String(err)));
120
148
  setUser(null);
121
149
  setActiveOrg(null);
150
+ setOrgs([]);
122
151
  setEntitlement(null);
123
152
  setStatus("unauthenticated");
124
153
  }
125
154
  },
126
- [apiBase, skipEntitlement, devAccount]
155
+ [apiBase, skipEntitlement, org, devAccount]
127
156
  );
128
157
  useEffect(() => {
129
158
  const ctrl = new AbortController();
@@ -142,6 +171,7 @@ function AccountProvider({
142
171
  await clientRef.current?.signOut();
143
172
  setUser(null);
144
173
  setActiveOrg(null);
174
+ setOrgs([]);
145
175
  setEntitlement(null);
146
176
  setStatus("unauthenticated");
147
177
  }, []);
@@ -150,6 +180,7 @@ function AccountProvider({
150
180
  status,
151
181
  user,
152
182
  activeOrg,
183
+ orgs,
153
184
  entitlement,
154
185
  error,
155
186
  refresh: () => load(),
@@ -158,7 +189,7 @@ function AccountProvider({
158
189
  apiBase,
159
190
  signInUrl
160
191
  }),
161
- [status, user, activeOrg, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
192
+ [status, user, activeOrg, orgs, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
162
193
  );
163
194
  return createElement(AccountContext.Provider, { value }, children);
164
195
  }
@@ -173,6 +204,17 @@ function useUser() {
173
204
  function useActiveOrg() {
174
205
  return useAccount().activeOrg;
175
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
+ }
176
218
  function useEntitlement(product) {
177
219
  const { entitlement } = useAccount();
178
220
  return useMemo(() => entitlementForProduct(entitlement, product), [entitlement, product]);
@@ -203,6 +245,6 @@ function SignOutButton({ children, className, onSignedOut }) {
203
245
  );
204
246
  }
205
247
 
206
- export { AccountProvider, SignInButton, SignOutButton, loadAccount, useAccount, useActiveOrg, useEntitlement, useUser };
248
+ export { AccountProvider, SignInButton, SignOutButton, loadAccount, useAccount, useActiveOrg, useEntitlement, useOrgFromSlug, useOrgs, useUser };
207
249
  //# sourceMappingURL=react.js.map
208
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 };