@viyv/account-client 0.1.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.
@@ -0,0 +1,102 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { PlanTier, ProductSlug, EntitlementFeatures } from '@viyv/shared';
4
+ export { EntitlementFeatures, PlanTier, ProductSlug } from '@viyv/shared';
5
+
6
+ interface SignInButtonProps {
7
+ /** Where to return after sign-in. Default: current URL. */
8
+ returnTo?: string;
9
+ children?: ReactNode;
10
+ className?: string;
11
+ }
12
+ /** Redirects to the hosted sign-in page (app.viyv.io) with a return_to. */
13
+ declare function SignInButton({ returnTo, children, className }: SignInButtonProps): react.DetailedReactHTMLElement<{
14
+ type: string;
15
+ className: string | undefined;
16
+ onClick: () => void;
17
+ }, HTMLElement>;
18
+ interface SignOutButtonProps {
19
+ children?: ReactNode;
20
+ className?: string;
21
+ onSignedOut?: () => void;
22
+ }
23
+ /** Signs out via better-auth and clears local account state. */
24
+ declare function SignOutButton({ children, className, onSignedOut }: SignOutButtonProps): react.DetailedReactHTMLElement<{
25
+ type: string;
26
+ className: string | undefined;
27
+ onClick: () => Promise<void>;
28
+ }, HTMLElement>;
29
+
30
+ /** The signed-in user as returned by better-auth's get-session. */
31
+ interface AccountUser {
32
+ id: string;
33
+ email: string;
34
+ name?: string | null;
35
+ image?: string | null;
36
+ }
37
+ /** Active organization, when the session carries one. */
38
+ interface AccountOrg {
39
+ id: string;
40
+ slug?: string | null;
41
+ name?: string | null;
42
+ }
43
+ /** Shape of GET /v1/license/entitlement (docs/09 A-4). */
44
+ interface Entitlement {
45
+ organizationId: string;
46
+ plan: PlanTier;
47
+ addons: string[];
48
+ /** Product slugs this org may use (free-start lists the agent family). */
49
+ products: ProductSlug[];
50
+ features: EntitlementFeatures;
51
+ }
52
+ /** Per-product entitlement view returned by useEntitlement(slug). */
53
+ interface ProductEntitlement {
54
+ /** Whether the product is enabled for this org. */
55
+ enabled: boolean;
56
+ plan: PlanTier;
57
+ addons: string[];
58
+ /** The full feature map (callers branch on the flags they care about). */
59
+ features: EntitlementFeatures;
60
+ }
61
+
62
+ type AccountStatus = "loading" | "authenticated" | "unauthenticated";
63
+ interface AccountContextValue {
64
+ status: AccountStatus;
65
+ user: AccountUser | null;
66
+ activeOrg: AccountOrg | null;
67
+ entitlement: Entitlement | null;
68
+ error: Error | null;
69
+ /** Re-fetch session + entitlement (e.g. after returning from checkout). */
70
+ refresh: () => Promise<void>;
71
+ /** Redirect to the hosted sign-in page (app.viyv.io) with a return_to. */
72
+ signIn: (returnTo?: string) => void;
73
+ /** Sign out via better-auth and drop local state. */
74
+ signOut: () => Promise<void>;
75
+ apiBase: string;
76
+ signInUrl: string;
77
+ }
78
+ interface AccountProviderProps {
79
+ /** e.g. "https://api.viyv.io" */
80
+ apiBase: string;
81
+ /** e.g. ".viyv.io" — kept for parity with docs/04 (cookies are server-set). */
82
+ cookieDomain?: string;
83
+ /** Hosted sign-in page. Default "https://app.viyv.io/signin". */
84
+ signInUrl?: string;
85
+ /** Skip the entitlement fetch (session-only consumers). Default false. */
86
+ skipEntitlement?: boolean;
87
+ children?: ReactNode;
88
+ }
89
+ declare function AccountProvider({ apiBase, cookieDomain, signInUrl, skipEntitlement, children, }: AccountProviderProps): react.FunctionComponentElement<react.ProviderProps<AccountContextValue | null>>;
90
+ /** Read the full account context. Throws if used outside <AccountProvider>. */
91
+ declare function useAccount(): AccountContextValue;
92
+ /** The signed-in user, or null when unauthenticated/loading. */
93
+ declare function useUser(): AccountUser | null;
94
+ /** The active organization, or null. */
95
+ declare function useActiveOrg(): AccountOrg | null;
96
+ /**
97
+ * Per-product entitlement view. Returns null while loading / unauthenticated.
98
+ * Web products gate features on the returned `enabled` + `features` flags.
99
+ */
100
+ declare function useEntitlement(product: ProductSlug): ProductEntitlement | null;
101
+
102
+ export { type AccountContextValue, type AccountOrg, AccountProvider, type AccountProviderProps, type AccountStatus, type AccountUser, type Entitlement, type ProductEntitlement, SignInButton, type SignInButtonProps, SignOutButton, type SignOutButtonProps, useAccount, useActiveOrg, useEntitlement, useUser };
package/dist/react.js ADDED
@@ -0,0 +1,206 @@
1
+ import { createContext, useRef, useState, useCallback, useEffect, useMemo, createElement, useContext } from 'react';
2
+ import { createAuthClient } from 'better-auth/client';
3
+
4
+ // src/entitlement.ts
5
+ function parseEntitlement(wire) {
6
+ return {
7
+ organizationId: wire.organization_id,
8
+ plan: wire.plan,
9
+ addons: wire.addons ?? [],
10
+ products: wire.products ?? [],
11
+ features: wire.features
12
+ };
13
+ }
14
+ function entitlementForProduct(entitlement, product) {
15
+ if (!entitlement) return null;
16
+ return {
17
+ enabled: entitlement.products.includes(product),
18
+ plan: entitlement.plan,
19
+ addons: entitlement.addons,
20
+ features: entitlement.features
21
+ };
22
+ }
23
+ function createAccountClient(config) {
24
+ const client = createAuthClient({
25
+ baseURL: config.apiBase,
26
+ basePath: "/v1/auth",
27
+ fetchOptions: { credentials: "include" }
28
+ });
29
+ return {
30
+ raw: client,
31
+ signUp: (params) => client.signUp.email({
32
+ email: params.email,
33
+ password: params.password,
34
+ name: params.name ?? params.email.split("@")[0] ?? params.email
35
+ }),
36
+ signIn: (params) => client.signIn.email({
37
+ email: params.email,
38
+ password: params.password
39
+ }),
40
+ signOut: () => client.signOut(),
41
+ getSession: () => client.getSession()
42
+ };
43
+ }
44
+
45
+ // src/provider.tsx
46
+ async function fetchJson(doFetch, url, signal) {
47
+ const res = await doFetch(url, { credentials: "include", signal });
48
+ if (res.status === 401 || res.status === 404) return null;
49
+ if (!res.ok) throw new Error(`${url} \u2192 ${res.status}`);
50
+ const text = await res.text();
51
+ if (!text || text === "null") return null;
52
+ return JSON.parse(text);
53
+ }
54
+ var SIGNED_OUT = {
55
+ status: "unauthenticated",
56
+ user: null,
57
+ activeOrg: null,
58
+ entitlement: null
59
+ };
60
+ async function loadAccount(apiBase, opts = {}) {
61
+ const doFetch = opts.fetchImpl ?? fetch;
62
+ const session = await fetchJson(
63
+ doFetch,
64
+ `${apiBase}/v1/auth/get-session`,
65
+ opts.signal
66
+ );
67
+ if (!session?.user) return SIGNED_OUT;
68
+ const user = {
69
+ id: session.user.id,
70
+ email: session.user.email,
71
+ name: session.user.name ?? null,
72
+ image: session.user.image ?? null
73
+ };
74
+ const [ent, orgsBody] = await Promise.all([
75
+ opts.skipEntitlement ? Promise.resolve(null) : fetchJson(
76
+ doFetch,
77
+ `${apiBase}/v1/license/entitlement`,
78
+ opts.signal
79
+ ),
80
+ fetchJson(doFetch, `${apiBase}/v1/users/me/orgs`, opts.signal)
81
+ ]);
82
+ const entitlement = ent ? parseEntitlement(ent) : null;
83
+ const activeId = session.session?.activeOrganizationId ?? entitlement?.organizationId ?? null;
84
+ const orgs = orgsBody?.organizations ?? [];
85
+ const match = activeId ? orgs.find((o) => o.organization_id === activeId) : orgs[0];
86
+ const activeOrg = match ? { id: match.organization_id, slug: match.slug ?? null, name: match.name ?? null } : activeId ? { id: activeId, slug: null, name: null } : null;
87
+ return { status: "authenticated", user, activeOrg, entitlement };
88
+ }
89
+ var AccountContext = createContext(null);
90
+ function AccountProvider({
91
+ apiBase,
92
+ cookieDomain,
93
+ signInUrl = "https://app.viyv.io/signin",
94
+ skipEntitlement = false,
95
+ children
96
+ }) {
97
+ const clientRef = useRef(null);
98
+ if (!clientRef.current) {
99
+ clientRef.current = createAccountClient({ apiBase});
100
+ }
101
+ const [status, setStatus] = useState("loading");
102
+ const [user, setUser] = useState(null);
103
+ const [activeOrg, setActiveOrg] = useState(null);
104
+ const [entitlement, setEntitlement] = useState(null);
105
+ const [error, setError] = useState(null);
106
+ const load = useCallback(
107
+ async (signal) => {
108
+ try {
109
+ setError(null);
110
+ const result = await loadAccount(apiBase, { skipEntitlement, signal });
111
+ setUser(result.user);
112
+ setActiveOrg(result.activeOrg);
113
+ setEntitlement(result.entitlement);
114
+ setStatus(result.status);
115
+ } catch (err) {
116
+ if (signal?.aborted) return;
117
+ setError(err instanceof Error ? err : new Error(String(err)));
118
+ setUser(null);
119
+ setActiveOrg(null);
120
+ setEntitlement(null);
121
+ setStatus("unauthenticated");
122
+ }
123
+ },
124
+ [apiBase, skipEntitlement]
125
+ );
126
+ useEffect(() => {
127
+ const ctrl = new AbortController();
128
+ void load(ctrl.signal);
129
+ return () => ctrl.abort();
130
+ }, [load]);
131
+ const signIn = useCallback(
132
+ (returnTo) => {
133
+ if (typeof window === "undefined") return;
134
+ const back = returnTo ?? window.location.href;
135
+ window.location.assign(`${signInUrl}?return_to=${encodeURIComponent(back)}`);
136
+ },
137
+ [signInUrl]
138
+ );
139
+ const signOut = useCallback(async () => {
140
+ await clientRef.current?.signOut();
141
+ setUser(null);
142
+ setActiveOrg(null);
143
+ setEntitlement(null);
144
+ setStatus("unauthenticated");
145
+ }, []);
146
+ const value = useMemo(
147
+ () => ({
148
+ status,
149
+ user,
150
+ activeOrg,
151
+ entitlement,
152
+ error,
153
+ refresh: () => load(),
154
+ signIn,
155
+ signOut,
156
+ apiBase,
157
+ signInUrl
158
+ }),
159
+ [status, user, activeOrg, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
160
+ );
161
+ return createElement(AccountContext.Provider, { value }, children);
162
+ }
163
+ function useAccount() {
164
+ const ctx = useContext(AccountContext);
165
+ if (!ctx) throw new Error("useAccount must be used within <AccountProvider>");
166
+ return ctx;
167
+ }
168
+ function useUser() {
169
+ return useAccount().user;
170
+ }
171
+ function useActiveOrg() {
172
+ return useAccount().activeOrg;
173
+ }
174
+ function useEntitlement(product) {
175
+ const { entitlement } = useAccount();
176
+ return useMemo(() => entitlementForProduct(entitlement, product), [entitlement, product]);
177
+ }
178
+
179
+ // src/components.tsx
180
+ function SignInButton({ returnTo, children, className }) {
181
+ const { signIn } = useAccount();
182
+ return createElement(
183
+ "button",
184
+ { type: "button", className, onClick: () => signIn(returnTo) },
185
+ children ?? "Sign in"
186
+ );
187
+ }
188
+ function SignOutButton({ children, className, onSignedOut }) {
189
+ const { signOut } = useAccount();
190
+ return createElement(
191
+ "button",
192
+ {
193
+ type: "button",
194
+ className,
195
+ onClick: async () => {
196
+ await signOut();
197
+ onSignedOut?.();
198
+ }
199
+ },
200
+ children ?? "Sign out"
201
+ );
202
+ }
203
+
204
+ export { AccountProvider, SignInButton, SignOutButton, useAccount, useActiveOrg, useEntitlement, useUser };
205
+ //# sourceMappingURL=react.js.map
206
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ // src/entitlement.ts
4
+ function parseEntitlement(wire) {
5
+ return {
6
+ organizationId: wire.organization_id,
7
+ plan: wire.plan,
8
+ addons: wire.addons ?? [],
9
+ products: wire.products ?? [],
10
+ features: wire.features
11
+ };
12
+ }
13
+
14
+ // src/server.ts
15
+ async function getJson(url, cookie) {
16
+ const res = await fetch(url, {
17
+ headers: { cookie },
18
+ // Server fetch: never cache an auth-scoped read.
19
+ cache: "no-store"
20
+ });
21
+ if (res.status === 401 || res.status === 404) return null;
22
+ if (!res.ok) throw new Error(`${url} \u2192 ${res.status}`);
23
+ const text = await res.text();
24
+ if (!text || text === "null") return null;
25
+ return JSON.parse(text);
26
+ }
27
+ async function getCurrentSession(config) {
28
+ const session = await getJson(`${config.apiBase}/v1/auth/get-session`, config.cookie);
29
+ if (!session?.user) return null;
30
+ return {
31
+ user: {
32
+ id: session.user.id,
33
+ email: session.user.email,
34
+ name: session.user.name ?? null,
35
+ image: session.user.image ?? null
36
+ },
37
+ activeOrganizationId: session.session?.activeOrganizationId ?? null
38
+ };
39
+ }
40
+ async function getEntitlement(config) {
41
+ const wire = await getJson(
42
+ `${config.apiBase}/v1/license/entitlement`,
43
+ config.cookie
44
+ );
45
+ return wire ? parseEntitlement(wire) : null;
46
+ }
47
+
48
+ exports.getCurrentSession = getCurrentSession;
49
+ exports.getEntitlement = getEntitlement;
50
+ //# sourceMappingURL=server.cjs.map
51
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1,21 @@
1
+ import { a as AccountUser, E as Entitlement } from './entitlement-B3pXM2vO.cjs';
2
+ import '@viyv/shared';
3
+
4
+ interface ServerSessionConfig {
5
+ /** e.g. "https://api.viyv.io" */
6
+ apiBase: string;
7
+ /**
8
+ * The incoming `Cookie` header to forward (server has no ambient cookie jar).
9
+ * In Next RSC: `cookies().toString()` or `headers().get("cookie")`.
10
+ */
11
+ cookie: string;
12
+ }
13
+ /** Resolve the signed-in user from forwarded cookies (or null). */
14
+ declare function getCurrentSession(config: ServerSessionConfig): Promise<{
15
+ user: AccountUser;
16
+ activeOrganizationId: string | null;
17
+ } | null>;
18
+ /** Resolve the org entitlement from forwarded cookies (or null). */
19
+ declare function getEntitlement(config: ServerSessionConfig): Promise<Entitlement | null>;
20
+
21
+ export { type ServerSessionConfig, getCurrentSession, getEntitlement };
@@ -0,0 +1,21 @@
1
+ import { a as AccountUser, E as Entitlement } from './entitlement-B3pXM2vO.js';
2
+ import '@viyv/shared';
3
+
4
+ interface ServerSessionConfig {
5
+ /** e.g. "https://api.viyv.io" */
6
+ apiBase: string;
7
+ /**
8
+ * The incoming `Cookie` header to forward (server has no ambient cookie jar).
9
+ * In Next RSC: `cookies().toString()` or `headers().get("cookie")`.
10
+ */
11
+ cookie: string;
12
+ }
13
+ /** Resolve the signed-in user from forwarded cookies (or null). */
14
+ declare function getCurrentSession(config: ServerSessionConfig): Promise<{
15
+ user: AccountUser;
16
+ activeOrganizationId: string | null;
17
+ } | null>;
18
+ /** Resolve the org entitlement from forwarded cookies (or null). */
19
+ declare function getEntitlement(config: ServerSessionConfig): Promise<Entitlement | null>;
20
+
21
+ export { type ServerSessionConfig, getCurrentSession, getEntitlement };
package/dist/server.js ADDED
@@ -0,0 +1,39 @@
1
+ import { parseEntitlement } from './chunk-UX6UAFIF.js';
2
+
3
+ // src/server.ts
4
+ async function getJson(url, cookie) {
5
+ const res = await fetch(url, {
6
+ headers: { cookie },
7
+ // Server fetch: never cache an auth-scoped read.
8
+ cache: "no-store"
9
+ });
10
+ if (res.status === 401 || res.status === 404) return null;
11
+ if (!res.ok) throw new Error(`${url} \u2192 ${res.status}`);
12
+ const text = await res.text();
13
+ if (!text || text === "null") return null;
14
+ return JSON.parse(text);
15
+ }
16
+ async function getCurrentSession(config) {
17
+ const session = await getJson(`${config.apiBase}/v1/auth/get-session`, config.cookie);
18
+ if (!session?.user) return null;
19
+ return {
20
+ user: {
21
+ id: session.user.id,
22
+ email: session.user.email,
23
+ name: session.user.name ?? null,
24
+ image: session.user.image ?? null
25
+ },
26
+ activeOrganizationId: session.session?.activeOrganizationId ?? null
27
+ };
28
+ }
29
+ async function getEntitlement(config) {
30
+ const wire = await getJson(
31
+ `${config.apiBase}/v1/license/entitlement`,
32
+ config.cookie
33
+ );
34
+ return wire ? parseEntitlement(wire) : null;
35
+ }
36
+
37
+ export { getCurrentSession, getEntitlement };
38
+ //# sourceMappingURL=server.js.map
39
+ //# sourceMappingURL=server.js.map
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@viyv/account-client",
3
+ "version": "0.1.0",
4
+ "description": "React SDK for viyv account SSO (useUser, useEntitlement, AccountProvider).",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./react": {
16
+ "types": "./dist/react.d.ts",
17
+ "import": "./dist/react.js",
18
+ "require": "./dist/react.cjs"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/server.d.ts",
22
+ "import": "./dist/server.js",
23
+ "require": "./dist/server.cjs"
24
+ },
25
+ "./introspect": {
26
+ "types": "./dist/introspect.d.ts",
27
+ "import": "./dist/introspect.js",
28
+ "require": "./dist/introspect.cjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "!dist/**/*.map"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "sideEffects": false,
39
+ "peerDependencies": {
40
+ "react": ">=18"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "react": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "dependencies": {
48
+ "better-auth": "^1.6.12"
49
+ },
50
+ "devDependencies": {
51
+ "@types/react": "^19.2.3",
52
+ "react": "^19.2.6",
53
+ "tsup": "^8.5.1",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^3.2.4",
56
+ "@viyv/shared": "0.0.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "typecheck": "tsc --noEmit",
61
+ "lint": "biome check .",
62
+ "test": "vitest run"
63
+ }
64
+ }