@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/{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 +54 -13
- package/dist/react.d.cts +48 -3
- package/dist/react.d.ts +48 -3
- package/dist/react.js +53 -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
|
@@ -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
|
-
|
|
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
|
|
91
|
-
const
|
|
92
|
-
|
|
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
|
-
/**
|
|
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
|
@@ -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
|
-
|
|
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
|
|
89
|
-
const
|
|
90
|
-
|
|
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-
|
|
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 };
|