@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.
- package/LICENSE +27 -0
- package/dist/chunk-UX6UAFIF.js +23 -0
- package/dist/entitlement-B3pXM2vO.d.cts +47 -0
- package/dist/entitlement-B3pXM2vO.d.ts +47 -0
- package/dist/index.cjs +117 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +88 -0
- package/dist/introspect.cjs +117 -0
- package/dist/introspect.d.cts +67 -0
- package/dist/introspect.d.ts +67 -0
- package/dist/introspect.js +106 -0
- package/dist/react.cjs +214 -0
- package/dist/react.d.cts +102 -0
- package/dist/react.d.ts +102 -0
- package/dist/react.js +206 -0
- package/dist/server.cjs +51 -0
- package/dist/server.d.cts +21 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.js +39 -0
- package/package.json +64 -0
|
@@ -0,0 +1,117 @@
|
|
|
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/introspect.ts
|
|
15
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
16
|
+
function createIntrospectionClient(config) {
|
|
17
|
+
const f = config.fetchImpl ?? fetch;
|
|
18
|
+
const base = config.apiBase.replace(/\/$/, "");
|
|
19
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
20
|
+
const secretHeaders = {
|
|
21
|
+
"x-viyv-service-secret": config.serviceSecret
|
|
22
|
+
};
|
|
23
|
+
if (config.clientId) secretHeaders["x-viyv-client-id"] = config.clientId;
|
|
24
|
+
async function call(path, init) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
27
|
+
try {
|
|
28
|
+
return await f(`${base}${path}`, { ...init, signal: controller.signal });
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
async introspectDevice(accessToken) {
|
|
35
|
+
let res;
|
|
36
|
+
try {
|
|
37
|
+
res = await call("/v1/auth/device/introspect", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
...secretHeaders,
|
|
41
|
+
authorization: `Bearer ${accessToken}`,
|
|
42
|
+
"content-type": "application/json"
|
|
43
|
+
},
|
|
44
|
+
body: "{}"
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { status: "unavailable", reason: `introspect transport error: ${errMessage(err)}` };
|
|
48
|
+
}
|
|
49
|
+
if (res.status === 401) {
|
|
50
|
+
return {
|
|
51
|
+
status: "unavailable",
|
|
52
|
+
reason: "introspect unauthorized (service secret / server config)"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
return { status: "unavailable", reason: `introspect http ${res.status}` };
|
|
57
|
+
}
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = await res.json();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { status: "integrity_error", reason: `introspect unparseable body: ${errMessage(err)}` };
|
|
63
|
+
}
|
|
64
|
+
if (!raw || typeof raw !== "object") {
|
|
65
|
+
return { status: "integrity_error", reason: "introspect non-object body" };
|
|
66
|
+
}
|
|
67
|
+
const body = raw;
|
|
68
|
+
if (body.active !== true) {
|
|
69
|
+
return { status: "inactive" };
|
|
70
|
+
}
|
|
71
|
+
if (!body.org_id || !body.user_id) {
|
|
72
|
+
return { status: "integrity_error", reason: "active token missing org_id/user_id" };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
status: "active",
|
|
76
|
+
subject: {
|
|
77
|
+
orgId: body.org_id,
|
|
78
|
+
userId: body.user_id,
|
|
79
|
+
deviceId: body.device_id ?? null,
|
|
80
|
+
scope: body.scope,
|
|
81
|
+
expiresAtUnix: body.exp,
|
|
82
|
+
clientId: body.client_id ?? null,
|
|
83
|
+
email: body.email ?? null,
|
|
84
|
+
name: body.name ?? null,
|
|
85
|
+
orgName: body.org_name ?? null
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
async getOrgEntitlement(orgId) {
|
|
90
|
+
let res;
|
|
91
|
+
try {
|
|
92
|
+
res = await call(`/v1/license/entitlement/by-org?org_id=${encodeURIComponent(orgId)}`, {
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: secretHeaders
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { status: "unavailable", reason: `entitlement transport error: ${errMessage(err)}` };
|
|
98
|
+
}
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
return { status: "unavailable", reason: `entitlement http ${res.status}` };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const wire = await res.json();
|
|
104
|
+
return { status: "ok", entitlement: parseEntitlement(wire) };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return { status: "unavailable", reason: `entitlement unparseable body: ${errMessage(err)}` };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function errMessage(err) {
|
|
112
|
+
return err instanceof Error ? err.message : String(err);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
exports.createIntrospectionClient = createIntrospectionClient;
|
|
116
|
+
//# sourceMappingURL=introspect.cjs.map
|
|
117
|
+
//# sourceMappingURL=introspect.cjs.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { E as Entitlement } from './entitlement-B3pXM2vO.cjs';
|
|
2
|
+
import '@viyv/shared';
|
|
3
|
+
|
|
4
|
+
interface IntrospectionClientConfig {
|
|
5
|
+
/** e.g. "https://api.viyv.io". */
|
|
6
|
+
apiBase: string;
|
|
7
|
+
/**
|
|
8
|
+
* Shared m2m secret (== viyv-account `INTERNAL_SERVICE_SECRET`). CALLER-INJECTED
|
|
9
|
+
* on purpose: this lib never reads it from the environment, so it can never
|
|
10
|
+
* bundle or log the secret. Pass it from your own server config.
|
|
11
|
+
*/
|
|
12
|
+
serviceSecret: string;
|
|
13
|
+
/** Optional caller id, sent as `x-viyv-client-id` for the server's audit log. */
|
|
14
|
+
clientId?: string;
|
|
15
|
+
/** Injectable fetch (Node). Defaults to global fetch. */
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
/** Per-request timeout (ms). Default 5000. A timeout maps to `unavailable`. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Subject resolved from an ACTIVE device access token (camelCase). */
|
|
21
|
+
interface ResolvedDeviceSubject {
|
|
22
|
+
orgId: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
deviceId: string | null;
|
|
25
|
+
scope: string;
|
|
26
|
+
/** Access-token expiry, seconds since the Unix epoch. */
|
|
27
|
+
expiresAtUnix: number;
|
|
28
|
+
clientId: string | null;
|
|
29
|
+
email: string | null;
|
|
30
|
+
name: string | null;
|
|
31
|
+
orgName: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discriminated outcome of introspecting a device access token. Consumers MUST
|
|
35
|
+
* branch on every case — that is the point: the type system forces handling of
|
|
36
|
+
* the tamper (`integrity_error`) and `unavailable` cases that hand-rolled
|
|
37
|
+
* clients have historically conflated with `inactive`.
|
|
38
|
+
*/
|
|
39
|
+
type IntrospectionOutcome = {
|
|
40
|
+
status: "active";
|
|
41
|
+
subject: ResolvedDeviceSubject;
|
|
42
|
+
} | {
|
|
43
|
+
status: "inactive";
|
|
44
|
+
} | {
|
|
45
|
+
status: "unavailable";
|
|
46
|
+
reason: string;
|
|
47
|
+
} | {
|
|
48
|
+
status: "integrity_error";
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/** Outcome of an m2m org-entitlement read. */
|
|
52
|
+
type OrgEntitlementOutcome = {
|
|
53
|
+
status: "ok";
|
|
54
|
+
entitlement: Entitlement;
|
|
55
|
+
} | {
|
|
56
|
+
status: "unavailable";
|
|
57
|
+
reason: string;
|
|
58
|
+
};
|
|
59
|
+
interface IntrospectionClient {
|
|
60
|
+
/** Resolve a device access token to a subject (fail-closed, see taxonomy). */
|
|
61
|
+
introspectDevice(accessToken: string): Promise<IntrospectionOutcome>;
|
|
62
|
+
/** Read an org's entitlement by id (m2m). */
|
|
63
|
+
getOrgEntitlement(orgId: string): Promise<OrgEntitlementOutcome>;
|
|
64
|
+
}
|
|
65
|
+
declare function createIntrospectionClient(config: IntrospectionClientConfig): IntrospectionClient;
|
|
66
|
+
|
|
67
|
+
export { type IntrospectionClient, type IntrospectionClientConfig, type IntrospectionOutcome, type OrgEntitlementOutcome, type ResolvedDeviceSubject, createIntrospectionClient };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { E as Entitlement } from './entitlement-B3pXM2vO.js';
|
|
2
|
+
import '@viyv/shared';
|
|
3
|
+
|
|
4
|
+
interface IntrospectionClientConfig {
|
|
5
|
+
/** e.g. "https://api.viyv.io". */
|
|
6
|
+
apiBase: string;
|
|
7
|
+
/**
|
|
8
|
+
* Shared m2m secret (== viyv-account `INTERNAL_SERVICE_SECRET`). CALLER-INJECTED
|
|
9
|
+
* on purpose: this lib never reads it from the environment, so it can never
|
|
10
|
+
* bundle or log the secret. Pass it from your own server config.
|
|
11
|
+
*/
|
|
12
|
+
serviceSecret: string;
|
|
13
|
+
/** Optional caller id, sent as `x-viyv-client-id` for the server's audit log. */
|
|
14
|
+
clientId?: string;
|
|
15
|
+
/** Injectable fetch (Node). Defaults to global fetch. */
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
/** Per-request timeout (ms). Default 5000. A timeout maps to `unavailable`. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Subject resolved from an ACTIVE device access token (camelCase). */
|
|
21
|
+
interface ResolvedDeviceSubject {
|
|
22
|
+
orgId: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
deviceId: string | null;
|
|
25
|
+
scope: string;
|
|
26
|
+
/** Access-token expiry, seconds since the Unix epoch. */
|
|
27
|
+
expiresAtUnix: number;
|
|
28
|
+
clientId: string | null;
|
|
29
|
+
email: string | null;
|
|
30
|
+
name: string | null;
|
|
31
|
+
orgName: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discriminated outcome of introspecting a device access token. Consumers MUST
|
|
35
|
+
* branch on every case — that is the point: the type system forces handling of
|
|
36
|
+
* the tamper (`integrity_error`) and `unavailable` cases that hand-rolled
|
|
37
|
+
* clients have historically conflated with `inactive`.
|
|
38
|
+
*/
|
|
39
|
+
type IntrospectionOutcome = {
|
|
40
|
+
status: "active";
|
|
41
|
+
subject: ResolvedDeviceSubject;
|
|
42
|
+
} | {
|
|
43
|
+
status: "inactive";
|
|
44
|
+
} | {
|
|
45
|
+
status: "unavailable";
|
|
46
|
+
reason: string;
|
|
47
|
+
} | {
|
|
48
|
+
status: "integrity_error";
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/** Outcome of an m2m org-entitlement read. */
|
|
52
|
+
type OrgEntitlementOutcome = {
|
|
53
|
+
status: "ok";
|
|
54
|
+
entitlement: Entitlement;
|
|
55
|
+
} | {
|
|
56
|
+
status: "unavailable";
|
|
57
|
+
reason: string;
|
|
58
|
+
};
|
|
59
|
+
interface IntrospectionClient {
|
|
60
|
+
/** Resolve a device access token to a subject (fail-closed, see taxonomy). */
|
|
61
|
+
introspectDevice(accessToken: string): Promise<IntrospectionOutcome>;
|
|
62
|
+
/** Read an org's entitlement by id (m2m). */
|
|
63
|
+
getOrgEntitlement(orgId: string): Promise<OrgEntitlementOutcome>;
|
|
64
|
+
}
|
|
65
|
+
declare function createIntrospectionClient(config: IntrospectionClientConfig): IntrospectionClient;
|
|
66
|
+
|
|
67
|
+
export { type IntrospectionClient, type IntrospectionClientConfig, type IntrospectionOutcome, type OrgEntitlementOutcome, type ResolvedDeviceSubject, createIntrospectionClient };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseEntitlement } from './chunk-UX6UAFIF.js';
|
|
2
|
+
|
|
3
|
+
// src/introspect.ts
|
|
4
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
5
|
+
function createIntrospectionClient(config) {
|
|
6
|
+
const f = config.fetchImpl ?? fetch;
|
|
7
|
+
const base = config.apiBase.replace(/\/$/, "");
|
|
8
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
9
|
+
const secretHeaders = {
|
|
10
|
+
"x-viyv-service-secret": config.serviceSecret
|
|
11
|
+
};
|
|
12
|
+
if (config.clientId) secretHeaders["x-viyv-client-id"] = config.clientId;
|
|
13
|
+
async function call(path, init) {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
try {
|
|
17
|
+
return await f(`${base}${path}`, { ...init, signal: controller.signal });
|
|
18
|
+
} finally {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async introspectDevice(accessToken) {
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await call("/v1/auth/device/introspect", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
...secretHeaders,
|
|
30
|
+
authorization: `Bearer ${accessToken}`,
|
|
31
|
+
"content-type": "application/json"
|
|
32
|
+
},
|
|
33
|
+
body: "{}"
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { status: "unavailable", reason: `introspect transport error: ${errMessage(err)}` };
|
|
37
|
+
}
|
|
38
|
+
if (res.status === 401) {
|
|
39
|
+
return {
|
|
40
|
+
status: "unavailable",
|
|
41
|
+
reason: "introspect unauthorized (service secret / server config)"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
return { status: "unavailable", reason: `introspect http ${res.status}` };
|
|
46
|
+
}
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await res.json();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { status: "integrity_error", reason: `introspect unparseable body: ${errMessage(err)}` };
|
|
52
|
+
}
|
|
53
|
+
if (!raw || typeof raw !== "object") {
|
|
54
|
+
return { status: "integrity_error", reason: "introspect non-object body" };
|
|
55
|
+
}
|
|
56
|
+
const body = raw;
|
|
57
|
+
if (body.active !== true) {
|
|
58
|
+
return { status: "inactive" };
|
|
59
|
+
}
|
|
60
|
+
if (!body.org_id || !body.user_id) {
|
|
61
|
+
return { status: "integrity_error", reason: "active token missing org_id/user_id" };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
status: "active",
|
|
65
|
+
subject: {
|
|
66
|
+
orgId: body.org_id,
|
|
67
|
+
userId: body.user_id,
|
|
68
|
+
deviceId: body.device_id ?? null,
|
|
69
|
+
scope: body.scope,
|
|
70
|
+
expiresAtUnix: body.exp,
|
|
71
|
+
clientId: body.client_id ?? null,
|
|
72
|
+
email: body.email ?? null,
|
|
73
|
+
name: body.name ?? null,
|
|
74
|
+
orgName: body.org_name ?? null
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
async getOrgEntitlement(orgId) {
|
|
79
|
+
let res;
|
|
80
|
+
try {
|
|
81
|
+
res = await call(`/v1/license/entitlement/by-org?org_id=${encodeURIComponent(orgId)}`, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
headers: secretHeaders
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { status: "unavailable", reason: `entitlement transport error: ${errMessage(err)}` };
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
return { status: "unavailable", reason: `entitlement http ${res.status}` };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const wire = await res.json();
|
|
93
|
+
return { status: "ok", entitlement: parseEntitlement(wire) };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { status: "unavailable", reason: `entitlement unparseable body: ${errMessage(err)}` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function errMessage(err) {
|
|
101
|
+
return err instanceof Error ? err.message : String(err);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { createIntrospectionClient };
|
|
105
|
+
//# sourceMappingURL=introspect.js.map
|
|
106
|
+
//# sourceMappingURL=introspect.js.map
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var client = require('better-auth/client');
|
|
5
|
+
|
|
6
|
+
// src/entitlement.ts
|
|
7
|
+
function parseEntitlement(wire) {
|
|
8
|
+
return {
|
|
9
|
+
organizationId: wire.organization_id,
|
|
10
|
+
plan: wire.plan,
|
|
11
|
+
addons: wire.addons ?? [],
|
|
12
|
+
products: wire.products ?? [],
|
|
13
|
+
features: wire.features
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function entitlementForProduct(entitlement, product) {
|
|
17
|
+
if (!entitlement) return null;
|
|
18
|
+
return {
|
|
19
|
+
enabled: entitlement.products.includes(product),
|
|
20
|
+
plan: entitlement.plan,
|
|
21
|
+
addons: entitlement.addons,
|
|
22
|
+
features: entitlement.features
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function createAccountClient(config) {
|
|
26
|
+
const client$1 = client.createAuthClient({
|
|
27
|
+
baseURL: config.apiBase,
|
|
28
|
+
basePath: "/v1/auth",
|
|
29
|
+
fetchOptions: { credentials: "include" }
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
raw: client$1,
|
|
33
|
+
signUp: (params) => client$1.signUp.email({
|
|
34
|
+
email: params.email,
|
|
35
|
+
password: params.password,
|
|
36
|
+
name: params.name ?? params.email.split("@")[0] ?? params.email
|
|
37
|
+
}),
|
|
38
|
+
signIn: (params) => client$1.signIn.email({
|
|
39
|
+
email: params.email,
|
|
40
|
+
password: params.password
|
|
41
|
+
}),
|
|
42
|
+
signOut: () => client$1.signOut(),
|
|
43
|
+
getSession: () => client$1.getSession()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/provider.tsx
|
|
48
|
+
async function fetchJson(doFetch, url, signal) {
|
|
49
|
+
const res = await doFetch(url, { credentials: "include", signal });
|
|
50
|
+
if (res.status === 401 || res.status === 404) return null;
|
|
51
|
+
if (!res.ok) throw new Error(`${url} \u2192 ${res.status}`);
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
if (!text || text === "null") return null;
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
var SIGNED_OUT = {
|
|
57
|
+
status: "unauthenticated",
|
|
58
|
+
user: null,
|
|
59
|
+
activeOrg: null,
|
|
60
|
+
entitlement: null
|
|
61
|
+
};
|
|
62
|
+
async function loadAccount(apiBase, opts = {}) {
|
|
63
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
64
|
+
const session = await fetchJson(
|
|
65
|
+
doFetch,
|
|
66
|
+
`${apiBase}/v1/auth/get-session`,
|
|
67
|
+
opts.signal
|
|
68
|
+
);
|
|
69
|
+
if (!session?.user) return SIGNED_OUT;
|
|
70
|
+
const user = {
|
|
71
|
+
id: session.user.id,
|
|
72
|
+
email: session.user.email,
|
|
73
|
+
name: session.user.name ?? null,
|
|
74
|
+
image: session.user.image ?? null
|
|
75
|
+
};
|
|
76
|
+
const [ent, orgsBody] = await Promise.all([
|
|
77
|
+
opts.skipEntitlement ? Promise.resolve(null) : fetchJson(
|
|
78
|
+
doFetch,
|
|
79
|
+
`${apiBase}/v1/license/entitlement`,
|
|
80
|
+
opts.signal
|
|
81
|
+
),
|
|
82
|
+
fetchJson(doFetch, `${apiBase}/v1/users/me/orgs`, opts.signal)
|
|
83
|
+
]);
|
|
84
|
+
const entitlement = ent ? parseEntitlement(ent) : null;
|
|
85
|
+
const activeId = session.session?.activeOrganizationId ?? entitlement?.organizationId ?? null;
|
|
86
|
+
const orgs = orgsBody?.organizations ?? [];
|
|
87
|
+
const match = activeId ? orgs.find((o) => o.organization_id === activeId) : orgs[0];
|
|
88
|
+
const activeOrg = match ? { id: match.organization_id, slug: match.slug ?? null, name: match.name ?? null } : activeId ? { id: activeId, slug: null, name: null } : null;
|
|
89
|
+
return { status: "authenticated", user, activeOrg, entitlement };
|
|
90
|
+
}
|
|
91
|
+
var AccountContext = react.createContext(null);
|
|
92
|
+
function AccountProvider({
|
|
93
|
+
apiBase,
|
|
94
|
+
cookieDomain,
|
|
95
|
+
signInUrl = "https://app.viyv.io/signin",
|
|
96
|
+
skipEntitlement = false,
|
|
97
|
+
children
|
|
98
|
+
}) {
|
|
99
|
+
const clientRef = react.useRef(null);
|
|
100
|
+
if (!clientRef.current) {
|
|
101
|
+
clientRef.current = createAccountClient({ apiBase});
|
|
102
|
+
}
|
|
103
|
+
const [status, setStatus] = react.useState("loading");
|
|
104
|
+
const [user, setUser] = react.useState(null);
|
|
105
|
+
const [activeOrg, setActiveOrg] = react.useState(null);
|
|
106
|
+
const [entitlement, setEntitlement] = react.useState(null);
|
|
107
|
+
const [error, setError] = react.useState(null);
|
|
108
|
+
const load = react.useCallback(
|
|
109
|
+
async (signal) => {
|
|
110
|
+
try {
|
|
111
|
+
setError(null);
|
|
112
|
+
const result = await loadAccount(apiBase, { skipEntitlement, signal });
|
|
113
|
+
setUser(result.user);
|
|
114
|
+
setActiveOrg(result.activeOrg);
|
|
115
|
+
setEntitlement(result.entitlement);
|
|
116
|
+
setStatus(result.status);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (signal?.aborted) return;
|
|
119
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
120
|
+
setUser(null);
|
|
121
|
+
setActiveOrg(null);
|
|
122
|
+
setEntitlement(null);
|
|
123
|
+
setStatus("unauthenticated");
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[apiBase, skipEntitlement]
|
|
127
|
+
);
|
|
128
|
+
react.useEffect(() => {
|
|
129
|
+
const ctrl = new AbortController();
|
|
130
|
+
void load(ctrl.signal);
|
|
131
|
+
return () => ctrl.abort();
|
|
132
|
+
}, [load]);
|
|
133
|
+
const signIn = react.useCallback(
|
|
134
|
+
(returnTo) => {
|
|
135
|
+
if (typeof window === "undefined") return;
|
|
136
|
+
const back = returnTo ?? window.location.href;
|
|
137
|
+
window.location.assign(`${signInUrl}?return_to=${encodeURIComponent(back)}`);
|
|
138
|
+
},
|
|
139
|
+
[signInUrl]
|
|
140
|
+
);
|
|
141
|
+
const signOut = react.useCallback(async () => {
|
|
142
|
+
await clientRef.current?.signOut();
|
|
143
|
+
setUser(null);
|
|
144
|
+
setActiveOrg(null);
|
|
145
|
+
setEntitlement(null);
|
|
146
|
+
setStatus("unauthenticated");
|
|
147
|
+
}, []);
|
|
148
|
+
const value = react.useMemo(
|
|
149
|
+
() => ({
|
|
150
|
+
status,
|
|
151
|
+
user,
|
|
152
|
+
activeOrg,
|
|
153
|
+
entitlement,
|
|
154
|
+
error,
|
|
155
|
+
refresh: () => load(),
|
|
156
|
+
signIn,
|
|
157
|
+
signOut,
|
|
158
|
+
apiBase,
|
|
159
|
+
signInUrl
|
|
160
|
+
}),
|
|
161
|
+
[status, user, activeOrg, entitlement, error, load, signIn, signOut, apiBase, signInUrl]
|
|
162
|
+
);
|
|
163
|
+
return react.createElement(AccountContext.Provider, { value }, children);
|
|
164
|
+
}
|
|
165
|
+
function useAccount() {
|
|
166
|
+
const ctx = react.useContext(AccountContext);
|
|
167
|
+
if (!ctx) throw new Error("useAccount must be used within <AccountProvider>");
|
|
168
|
+
return ctx;
|
|
169
|
+
}
|
|
170
|
+
function useUser() {
|
|
171
|
+
return useAccount().user;
|
|
172
|
+
}
|
|
173
|
+
function useActiveOrg() {
|
|
174
|
+
return useAccount().activeOrg;
|
|
175
|
+
}
|
|
176
|
+
function useEntitlement(product) {
|
|
177
|
+
const { entitlement } = useAccount();
|
|
178
|
+
return react.useMemo(() => entitlementForProduct(entitlement, product), [entitlement, product]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/components.tsx
|
|
182
|
+
function SignInButton({ returnTo, children, className }) {
|
|
183
|
+
const { signIn } = useAccount();
|
|
184
|
+
return react.createElement(
|
|
185
|
+
"button",
|
|
186
|
+
{ type: "button", className, onClick: () => signIn(returnTo) },
|
|
187
|
+
children ?? "Sign in"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
function SignOutButton({ children, className, onSignedOut }) {
|
|
191
|
+
const { signOut } = useAccount();
|
|
192
|
+
return react.createElement(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
type: "button",
|
|
196
|
+
className,
|
|
197
|
+
onClick: async () => {
|
|
198
|
+
await signOut();
|
|
199
|
+
onSignedOut?.();
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
children ?? "Sign out"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
exports.AccountProvider = AccountProvider;
|
|
207
|
+
exports.SignInButton = SignInButton;
|
|
208
|
+
exports.SignOutButton = SignOutButton;
|
|
209
|
+
exports.useAccount = useAccount;
|
|
210
|
+
exports.useActiveOrg = useActiveOrg;
|
|
211
|
+
exports.useEntitlement = useEntitlement;
|
|
212
|
+
exports.useUser = useUser;
|
|
213
|
+
//# sourceMappingURL=react.cjs.map
|
|
214
|
+
//# sourceMappingURL=react.cjs.map
|
package/dist/react.d.cts
ADDED
|
@@ -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 };
|