@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/{chunk-UX6UAFIF.js → chunk-V3SLFECG.js} +6 -3
- package/dist/{entitlement-B3pXM2vO.d.cts → entitlement-CfKncUyH.d.cts} +9 -0
- package/dist/{entitlement-B3pXM2vO.d.ts → entitlement-CfKncUyH.d.ts} +9 -0
- package/dist/index.cjs +19 -1
- package/dist/index.d.cts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +16 -1
- package/dist/introspect.cjs +64 -1
- package/dist/introspect.d.cts +44 -2
- package/dist/introspect.d.ts +44 -2
- package/dist/introspect.js +64 -2
- package/dist/react.cjs +57 -13
- package/dist/react.d.cts +48 -3
- package/dist/react.d.ts +48 -3
- package/dist/react.js +56 -14
- package/dist/server.cjs +23 -1
- package/dist/server.d.cts +32 -4
- package/dist/server.d.ts +32 -4
- package/dist/server.js +22 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
86
|
-
const
|
|
87
|
-
|
|
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-
|
|
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
|
-
/**
|
|
19
|
-
|
|
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 };
|