@kelviq/js-sdk 1.0.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.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Kamil Bysiec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: "JavaScript SDK"
3
+ description: "Documentation for the Kelviq JavaScript SDK"
4
+ icon: "js"
5
+ ---
6
+
7
+ The `@kelviq/js-sdk` is a lightweight, plain TypeScript library designed for interacting with the Kelviq entitlement service. It allows you to:
8
+
9
+ - Fetch all feature entitlements for a specific organization and customer.
10
+ - Cache these entitlements client-side for efficient and repeated access.
11
+ - Check if a user has access to a particular feature.
12
+ - Retrieve specific entitlement details, including boolean flags, numeric configurations, and metered usage data.
13
+ - Manually refresh entitlement data from the server.
14
+
15
+ ### Docs - https://docs.kelviq.com/frontend-integration/js-sdk
@@ -0,0 +1,175 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export interface Entitlement {
4
+ featureId: string;
5
+ hasAccess: boolean;
6
+ type: "boolean" | "customizable" | "metered";
7
+ }
8
+ export interface BooleanEntitlement extends Entitlement {
9
+ type: "boolean";
10
+ }
11
+ export interface ConfigEntitlement extends Entitlement {
12
+ type: "customizable";
13
+ configuration: number | null;
14
+ }
15
+ export interface MeteredEntitlement extends Entitlement {
16
+ type: "metered";
17
+ limit: number | null;
18
+ used: number;
19
+ remaining: number | null;
20
+ resetAt?: string | null;
21
+ hardLimit?: boolean;
22
+ }
23
+ export type AnyEntitlement = BooleanEntitlement | ConfigEntitlement | MeteredEntitlement;
24
+ export type EntitlementMap = Record<string, AnyEntitlement>;
25
+ /**
26
+ * Configuration for an API request.
27
+ * @template ReqBody The type of the request body.
28
+ */
29
+ export interface ApiRequestConfig<ReqBody = any> {
30
+ /** The URL for the request. */
31
+ url: string;
32
+ /** HTTP method (GET, POST, PUT, DELETE, etc.). Defaults to GET. */
33
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD";
34
+ /** Custom headers for the request. */
35
+ headers?: Record<string, string>;
36
+ /** Data to be sent as the request body. */
37
+ body?: ReqBody;
38
+ /** Timeout for the request in milliseconds. Defaults to 5000ms. */
39
+ timeout?: number;
40
+ /** Maximum number of retry attempts for failed requests. Defaults to 3. */
41
+ maxRetries?: number;
42
+ /** Base delay (in milliseconds) for exponential backoff between retries. Defaults to 1000ms. */
43
+ backoffBaseDelay?: number;
44
+ /** Optional: The static authentication token string. */
45
+ accessToken?: string | null;
46
+ /** Optional: Query parameters to be appended to the URL. */
47
+ queryParams?: Record<string, string | number | boolean | undefined>;
48
+ }
49
+ /**
50
+ * Represents the structure of a successful API response.
51
+ * @template ResData The type of the data in the response body.
52
+ */
53
+ export interface ApiResponse<ResData> {
54
+ /** HTTP status code of the response. */
55
+ status: number;
56
+ /** HTTP status text of the response. */
57
+ statusText: string;
58
+ /** Parsed data from the response body. */
59
+ data: ResData;
60
+ }
61
+ /** Optional behavioral configuration options for the KelviqProvider. */
62
+ export interface KelviqApiOptions {
63
+ /** Optional: Maximum number of retries for API requests. Defaults to 3. */
64
+ maxRetries?: number;
65
+ /** Optional: Timeout for API requests in milliseconds. Defaults to 5000. */
66
+ timeout?: number;
67
+ /** Optional: Base delay (in milliseconds) for exponential backoff strategy in retries.
68
+ * If not provided, the `apiRequest` service's default will be used.
69
+ */
70
+ backoffBaseDelay?: number;
71
+ }
72
+ /**
73
+ * Configuration options for initializing the KelviqClient.
74
+ */
75
+ export interface KelviqClientOptions {
76
+ /** Base URL for the Kelviq API (e.g., https://edge.api.kelviq.com/api/v1). Required. */
77
+ apiUrl?: string;
78
+ /** Path for fetching all entitlements (e.g., "/entitlement-check"). Required. */
79
+ entitlementsPath?: string;
80
+ /** The customer ID, sent as a query parameter. Required. */
81
+ customerId?: string;
82
+ /** Optional: The static authentication token string for API requests. */
83
+ accessToken: string;
84
+ environment?: "sandbox" | "production";
85
+ /**
86
+ * If true, `fetchAllEntitlements()` will be called immediately upon client creation using `createKelviqClient`.
87
+ * Defaults to `false`.
88
+ */
89
+ initializeAndFetch?: boolean;
90
+ /** Optional callback for handling errors that occur within the SDK during entitlement fetching. */
91
+ onError?: (error: Error) => void;
92
+ apiConfig?: KelviqApiOptions;
93
+ }
94
+ declare function apiRequest<ResData = any, ReqBody = any>(config: ApiRequestConfig<ReqBody>): Promise<ApiResponse<ResData>>;
95
+ declare class KelviqClient {
96
+ private readonly options;
97
+ private entitlementsCache;
98
+ private isFetching;
99
+ private lastFetchError;
100
+ private readonly apiRequestService;
101
+ /**
102
+ * Creates an instance of KelviqClient.
103
+ * Prefer using the `createKelviqClient` factory function for instantiation.
104
+ * @internal
105
+ * @param options Configuration options for the client.
106
+ * @param customApiRequest Optional: A custom function to handle API requests, conforming to ApiRequestConfig and ApiResponse.
107
+ */
108
+ constructor(options: KelviqClientOptions, customApiRequest?: typeof apiRequest);
109
+ /**
110
+ * Fetches all entitlements from the API for the configured orgId and customerId,
111
+ * processes them, and caches the result.
112
+ * Subsequent calls will return cached data unless `forceRefresh` is true.
113
+ *
114
+ * @param forceRefresh - If true, fetches from the API even if data is already cached. Defaults to false.
115
+ * @returns A promise that resolves to the map of entitlements (EntitlementMap).
116
+ * @throws Will throw an error if the API request fails after all retries.
117
+ */
118
+ fetchAllEntitlements(forceRefresh?: boolean): Promise<EntitlementMap>;
119
+ /**
120
+ * Retrieves a specific entitlement from the local cache.
121
+ * Note: `fetchAllEntitlements()` must be called successfully at least once before using this method,
122
+ * or the cache will be empty.
123
+ *
124
+ * @param featureId The unique key of the feature.
125
+ * @param type The expected type of the entitlement ('boolean', 'config', 'metered').
126
+ * @returns The processed entitlement object if found in the cache and the type matches, otherwise `null`.
127
+ * Logs a warning if entitlements haven't been fetched or if a type mismatch occurs.
128
+ */
129
+ getEntitlement<T extends AnyEntitlement>(featureId: string, type: T["type"]): T | null;
130
+ /**
131
+ * Checks if the user has access to a specific feature based on cached entitlements.
132
+ * Note: `fetchAllEntitlements()` should be called successfully at least once before using this method.
133
+ *
134
+ * @param featureId The key of the feature.
135
+ * @returns `true` if access is granted, `false` if denied or the feature is not found in the cache.
136
+ * Logs a warning and returns `false` if entitlements haven't been fetched.
137
+ */
138
+ hasAccess(featureId: string): boolean;
139
+ /**
140
+ * Returns the currently cached map of all entitlements.
141
+ * `fetchAllEntitlements()` should be called successfully at least once.
142
+ * @returns The EntitlementMap (a record of featureKey to entitlement data) or `null` if not fetched or an error occurred during the last fetch.
143
+ */
144
+ getAllEntitlements(): EntitlementMap | null;
145
+ /**
146
+ * Indicates if an entitlement fetch operation is currently in progress.
147
+ * @returns `true` if a fetch is in progress, `false` otherwise.
148
+ */
149
+ isLoading(): boolean;
150
+ /**
151
+ * Returns the last error that occurred during an entitlement fetch operation.
152
+ * @returns An `Error` object if the last fetch failed, otherwise `null`.
153
+ */
154
+ getLastError(): Error | null;
155
+ /**
156
+ * Clears the local entitlement cache and resets loading and error states.
157
+ * After calling this, `fetchAllEntitlements()` will need to be called again to populate the cache.
158
+ */
159
+ clearCache(): void;
160
+ }
161
+ /**
162
+ * Factory function to create and initialize a KelviqClient instance.
163
+ * This is the recommended way to get an SDK client.
164
+ * @param options Configuration options for the client.
165
+ * @param customApiRequest Optional: A custom function to handle API requests, useful for testing or custom HTTP clients.
166
+ * @returns An instance of KelviqClient.
167
+ */
168
+ declare function kelviqSDK(// Renamed from createKelviqClient
169
+ options: KelviqClientOptions, customApiRequest?: typeof apiRequest): KelviqClient;
170
+
171
+ export {
172
+ kelviqSDK as default,
173
+ };
174
+
175
+ export {};
@@ -0,0 +1 @@
1
+ var kelviqSDK=function(){"use strict";var P=Object.defineProperty;var b=(d,m,y)=>m in d?P(d,m,{enumerable:!0,configurable:!0,writable:!0,value:y}):d[m]=y;var A=(d,m,y)=>b(d,typeof m!="symbol"?m+"":m,y);function d(s){const n={};return!s||!Array.isArray(s.entitlements)?(console.warn("[Kelviq SDK] transformApiEntitlements: Invalid or empty entitlements array in API response.",s),n):(s.entitlements.forEach(e=>{if(!e||typeof e.featureId!="string"||typeof e.featureType!="string"){console.warn("[Kelviq SDK] transformApiEntitlements: Skipping invalid raw entitlement object:",e);return}let t=null;const f={featureId:e.featureId,hasAccess:e.hasAccess};if(e.featureType==="BOOLEAN")t={...f,type:"boolean"};else if(e.featureType==="CUSTOMIZABLE"){const r=e;t={...f,type:"customizable",configuration:typeof r.value=="number"?r.value:null}}else if(e.featureType==="METER"){const r=e,E=typeof r.usageLimit=="number"?r.usageLimit:null,o=typeof r.currentUsage=="number"?r.currentUsage:0;t={...f,type:"metered",limit:E,used:o,remaining:E!==null?E-o:null,resetAt:r.resetAt,hardLimit:r.hardLimit}}else{const r=e;console.warn(`[Kelviq SDK] transformApiEntitlements: Encountered unknown or unhandled entitlement featureType: '${String(r.featureType)}' for feature '${String(r.feature)}'. This entitlement will be ignored.`)}t&&(n[t.featureId]=t)}),n)}const m="GET",y=5e3,R=3,$=1e3;function F(s){const{method:n=m,headers:e={},body:t,timeout:f=y,maxRetries:r=R,backoffBaseDelay:E=$,queryParams:o,accessToken:v}=s;let a=s.url,g=0;return new Promise((T,l)=>{if(o){const c=new URLSearchParams;for(const p in o)o[p]!==void 0&&c.append(p,String(o[p]));c.toString()&&(a=a+(a.includes("?")?"&":"?")+c.toString())}function u(){const c=new XMLHttpRequest;c.open(n,a,!0),c.timeout=f;const p={Accept:"application/json","X-Requested-With":"XMLHttpRequest",...e};n!=="GET"&&t&&typeof t=="object"&&!(t instanceof FormData)&&(p["Content-Type"]||(p["Content-Type"]="application/json")),v&&(p.Authorization=`Bearer ${v}`);for(const[i,h]of Object.entries(p))c.setRequestHeader(i,h);c.onload=function(){const{status:i,statusText:h,responseText:S}=c;if(i>=200&&i<300)try{const q=S?JSON.parse(S):{};T({status:i,statusText:h,data:q})}catch(q){const D=q instanceof Error?q:new Error(String(q));console.error(`Kelviq SDK (apiRequest): Invalid JSON response for URL ${a}. Error: ${D.message}. Response: ${S}`),l(new Error(`Invalid JSON response: ${D.message}`))}else K(`Request to ${a} failed with status ${i} ${h}. Response: ${S}`)},c.onerror=()=>K(`Network error for URL ${a}. The request could not be completed.`),c.ontimeout=()=>K(`Request to ${a} timed out.`);function K(i){if(g<r){g++;const h=Math.min(E*Math.pow(2,g-1),3e4);console.warn(`Kelviq SDK (apiRequest): Retrying request to ${a}. Attempt ${g}/${r}. Error: ${i}. Retrying in ${h}ms.`),setTimeout(u,h)}else l(new Error(`${i} (Max retries ${r} reached)`))}try{let i=t;t&&typeof t=="object"&&!(t instanceof FormData)&&p["Content-Type"]==="application/json"&&(i=JSON.stringify(t)),c.send(i)}catch(i){const h=i instanceof Error?i:new Error(String(i));console.error(`Kelviq SDK (apiRequest): Error sending request to ${a}.`,h),l(new Error(`Failed to send request: ${h.message}`))}}u()})}const L="https://edge.api.kelviq.com/api/v1/",w="https://edge.sandboxapi.kelviq.com/api/v1/",C="entitlements/";class I{constructor(n,e){A(this,"options");A(this,"entitlementsCache",null);A(this,"isFetching",!1);A(this,"lastFetchError",null);A(this,"apiRequestService");if(!n||!n.customerId)throw new Error("[Kelviq SDK] KelviqClient: CustomerId is required in options.");this.options={...n,apiConfig:n.apiConfig||{},entitlementsPath:n.entitlementsPath||C},this.apiRequestService=e||F}async fetchAllEntitlements(n=!1){if(this.isFetching)return Promise.reject(new Error("[Kelviq SDK] An entitlement fetch is already in progress."));if(this.entitlementsCache&&!n)return Promise.resolve(this.entitlementsCache);this.isFetching=!0,this.lastFetchError=null;const{apiUrl:e=this.options.environment==="sandbox"?w:L,entitlementsPath:t,customerId:f,accessToken:r,apiConfig:E,onError:o}=this.options,v=e.replace(/\/$/,""),a=t.replace(/^\//,""),g=`${v}/${a}`,T={customer_id:f};try{const l=await this.apiRequestService({url:g,method:"GET",queryParams:T,accessToken:r,...E||{}});if(l&&l.data&&Array.isArray(l.data.entitlements))return this.entitlementsCache=d(l.data),this.isFetching=!1,this.entitlementsCache;{const u=new Error("[Kelviq SDK] Received empty, malformed, or invalid data structure from entitlements API.");throw this.lastFetchError=u,this.isFetching=!1,this.entitlementsCache=null,o&&o(u),u}}catch(l){const u=l instanceof Error?l:new Error(String(l));throw this.lastFetchError=u,this.isFetching=!1,this.entitlementsCache=null,o&&o(u),u}}getEntitlement(n,e){if(!this.entitlementsCache)return console.warn(`[Kelviq SDK] getEntitlement: Entitlements not fetched or cache is empty for featureId '${n}'. Call fetchAllEntitlements() first.`),null;const t=this.entitlementsCache[n];return t?t.type===e?t:(console.warn(`[Kelviq SDK] getEntitlement: Type mismatch for featureId '${n}'. Expected type '${e}', but cached type is '${t.type}'.`),null):null}hasAccess(n){if(!this.entitlementsCache)return console.warn(`[Kelviq SDK] hasAccess: Entitlements not fetched yet for featureId '${n}'. Call fetchAllEntitlements() first. Returning false.`),!1;const e=this.entitlementsCache[n];return e?e.hasAccess:!1}getAllEntitlements(){return this.entitlementsCache}isLoading(){return this.isFetching}getLastError(){return this.lastFetchError}clearCache(){this.entitlementsCache=null,this.isFetching=!1,this.lastFetchError=null,console.log("[Kelviq SDK] Entitlement cache cleared.")}}function U(s,n){const e={...s,initializeAndFetch:s.initializeAndFetch||!0,apiConfig:s.apiConfig||{},environment:s.environment||"production",entitlementsPath:s.entitlementsPath||C},t=new I(e,n);return e.initializeAndFetch===!0&&t.fetchAllEntitlements().catch(f=>{console.error("[Kelviq SDK] Initial fetch on client creation failed:",f.message)}),t}return U}();
@@ -0,0 +1,295 @@
1
+ var K = Object.defineProperty;
2
+ var C = (r, t, e) => t in r ? K(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
3
+ var E = (r, t, e) => C(r, typeof t != "symbol" ? t + "" : t, e);
4
+ function R(r) {
5
+ const t = {};
6
+ return !r || !Array.isArray(r.entitlements) ? (console.warn(
7
+ "[Kelviq SDK] transformApiEntitlements: Invalid or empty entitlements array in API response.",
8
+ r
9
+ ), t) : (r.entitlements.forEach((e) => {
10
+ if (!e || typeof e.featureId != "string" || typeof e.featureType != "string") {
11
+ console.warn(
12
+ "[Kelviq SDK] transformApiEntitlements: Skipping invalid raw entitlement object:",
13
+ e
14
+ );
15
+ return;
16
+ }
17
+ let n = null;
18
+ const m = {
19
+ featureId: e.featureId,
20
+ hasAccess: e.hasAccess
21
+ };
22
+ if (e.featureType === "BOOLEAN")
23
+ n = {
24
+ ...m,
25
+ type: "boolean"
26
+ };
27
+ else if (e.featureType === "CUSTOMIZABLE") {
28
+ const i = e;
29
+ n = {
30
+ ...m,
31
+ type: "customizable",
32
+ configuration: typeof i.value == "number" ? i.value : null
33
+ };
34
+ } else if (e.featureType === "METER") {
35
+ const i = e, p = typeof i.usageLimit == "number" ? i.usageLimit : null, o = typeof i.currentUsage == "number" ? i.currentUsage : 0;
36
+ n = {
37
+ ...m,
38
+ type: "metered",
39
+ limit: p,
40
+ used: o,
41
+ remaining: p !== null ? p - o : null,
42
+ resetAt: i.resetAt,
43
+ hardLimit: i.hardLimit
44
+ };
45
+ } else {
46
+ const i = e;
47
+ console.warn(
48
+ `[Kelviq SDK] transformApiEntitlements: Encountered unknown or unhandled entitlement featureType: '${String(i.featureType)}' for feature '${String(i.feature)}'. This entitlement will be ignored.`
49
+ );
50
+ }
51
+ n && (t[n.featureId] = n);
52
+ }), t);
53
+ }
54
+ const $ = "GET", D = 5e3, F = 3, L = 1e3;
55
+ function w(r) {
56
+ const {
57
+ method: t = $,
58
+ headers: e = {},
59
+ // Renamed to avoid conflict with internal 'headers' variable
60
+ body: n,
61
+ timeout: m = D,
62
+ maxRetries: i = F,
63
+ backoffBaseDelay: p = L,
64
+ queryParams: o,
65
+ accessToken: g
66
+ // Use the direct accessToken
67
+ } = r;
68
+ let a = r.url, d = 0;
69
+ return new Promise((q, l) => {
70
+ if (o) {
71
+ const c = new URLSearchParams();
72
+ for (const f in o)
73
+ o[f] !== void 0 && c.append(f, String(o[f]));
74
+ c.toString() && (a = a + (a.includes("?") ? "&" : "?") + c.toString());
75
+ }
76
+ function u() {
77
+ const c = new XMLHttpRequest();
78
+ c.open(t, a, !0), c.timeout = m;
79
+ const f = {
80
+ Accept: "application/json",
81
+ "X-Requested-With": "XMLHttpRequest",
82
+ ...e
83
+ // Apply custom headers, allowing them to override defaults
84
+ };
85
+ t !== "GET" && n && typeof n == "object" && !(n instanceof FormData) && (f["Content-Type"] || (f["Content-Type"] = "application/json")), g && (f.Authorization = `Bearer ${g}`);
86
+ for (const [s, h] of Object.entries(f))
87
+ c.setRequestHeader(s, h);
88
+ c.onload = function() {
89
+ const { status: s, statusText: h, responseText: A } = c;
90
+ if (s >= 200 && s < 300)
91
+ try {
92
+ const y = A ? JSON.parse(A) : {};
93
+ q({ status: s, statusText: h, data: y });
94
+ } catch (y) {
95
+ const S = y instanceof Error ? y : new Error(String(y));
96
+ console.error(
97
+ `Kelviq SDK (apiRequest): Invalid JSON response for URL ${a}. Error: ${S.message}. Response: ${A}`
98
+ ), l(new Error(`Invalid JSON response: ${S.message}`));
99
+ }
100
+ else
101
+ v(
102
+ `Request to ${a} failed with status ${s} ${h}. Response: ${A}`
103
+ );
104
+ }, c.onerror = () => v(
105
+ `Network error for URL ${a}. The request could not be completed.`
106
+ ), c.ontimeout = () => v(`Request to ${a} timed out.`);
107
+ function v(s) {
108
+ if (d < i) {
109
+ d++;
110
+ const h = Math.min(
111
+ p * Math.pow(2, d - 1),
112
+ 3e4
113
+ // Cap retry delay at 30 seconds
114
+ );
115
+ console.warn(
116
+ `Kelviq SDK (apiRequest): Retrying request to ${a}. Attempt ${d}/${i}. Error: ${s}. Retrying in ${h}ms.`
117
+ ), setTimeout(u, h);
118
+ } else
119
+ l(
120
+ new Error(`${s} (Max retries ${i} reached)`)
121
+ );
122
+ }
123
+ try {
124
+ let s = n;
125
+ n && typeof n == "object" && !(n instanceof FormData) && f["Content-Type"] === "application/json" && (s = JSON.stringify(n)), c.send(s);
126
+ } catch (s) {
127
+ const h = s instanceof Error ? s : new Error(String(s));
128
+ console.error(
129
+ `Kelviq SDK (apiRequest): Error sending request to ${a}.`,
130
+ h
131
+ ), l(new Error(`Failed to send request: ${h.message}`));
132
+ }
133
+ }
134
+ u();
135
+ });
136
+ }
137
+ const I = "https://edge.api.kelviq.com/api/v1/", U = "https://edge.sandboxapi.kelviq.com/api/v1/", T = "entitlements/";
138
+ class P {
139
+ /**
140
+ * Creates an instance of KelviqClient.
141
+ * Prefer using the `createKelviqClient` factory function for instantiation.
142
+ * @internal
143
+ * @param options Configuration options for the client.
144
+ * @param customApiRequest Optional: A custom function to handle API requests, conforming to ApiRequestConfig and ApiResponse.
145
+ */
146
+ constructor(t, e) {
147
+ E(this, "options");
148
+ E(this, "entitlementsCache", null);
149
+ E(this, "isFetching", !1);
150
+ E(this, "lastFetchError", null);
151
+ E(this, "apiRequestService");
152
+ if (!t || !t.customerId)
153
+ throw new Error(
154
+ "[Kelviq SDK] KelviqClient: CustomerId is required in options."
155
+ );
156
+ this.options = {
157
+ ...t,
158
+ apiConfig: t.apiConfig || {},
159
+ entitlementsPath: t.entitlementsPath || T
160
+ }, this.apiRequestService = e || w;
161
+ }
162
+ /**
163
+ * Fetches all entitlements from the API for the configured orgId and customerId,
164
+ * processes them, and caches the result.
165
+ * Subsequent calls will return cached data unless `forceRefresh` is true.
166
+ *
167
+ * @param forceRefresh - If true, fetches from the API even if data is already cached. Defaults to false.
168
+ * @returns A promise that resolves to the map of entitlements (EntitlementMap).
169
+ * @throws Will throw an error if the API request fails after all retries.
170
+ */
171
+ async fetchAllEntitlements(t = !1) {
172
+ if (this.isFetching)
173
+ return Promise.reject(
174
+ new Error("[Kelviq SDK] An entitlement fetch is already in progress.")
175
+ );
176
+ if (this.entitlementsCache && !t)
177
+ return Promise.resolve(this.entitlementsCache);
178
+ this.isFetching = !0, this.lastFetchError = null;
179
+ const {
180
+ apiUrl: e = this.options.environment === "sandbox" ? U : I,
181
+ entitlementsPath: n,
182
+ customerId: m,
183
+ accessToken: i,
184
+ apiConfig: p,
185
+ onError: o
186
+ } = this.options, g = e.replace(/\/$/, ""), a = n.replace(/^\//, ""), d = `${g}/${a}`, q = {
187
+ customer_id: m
188
+ };
189
+ try {
190
+ const l = await this.apiRequestService(
191
+ {
192
+ url: d,
193
+ method: "GET",
194
+ queryParams: q,
195
+ accessToken: i,
196
+ ...p || {}
197
+ }
198
+ );
199
+ if (l && l.data && Array.isArray(l.data.entitlements))
200
+ return this.entitlementsCache = R(l.data), this.isFetching = !1, this.entitlementsCache;
201
+ {
202
+ const u = new Error(
203
+ "[Kelviq SDK] Received empty, malformed, or invalid data structure from entitlements API."
204
+ );
205
+ throw this.lastFetchError = u, this.isFetching = !1, this.entitlementsCache = null, o && o(u), u;
206
+ }
207
+ } catch (l) {
208
+ const u = l instanceof Error ? l : new Error(String(l));
209
+ throw this.lastFetchError = u, this.isFetching = !1, this.entitlementsCache = null, o && o(u), u;
210
+ }
211
+ }
212
+ /**
213
+ * Retrieves a specific entitlement from the local cache.
214
+ * Note: `fetchAllEntitlements()` must be called successfully at least once before using this method,
215
+ * or the cache will be empty.
216
+ *
217
+ * @param featureId The unique key of the feature.
218
+ * @param type The expected type of the entitlement ('boolean', 'config', 'metered').
219
+ * @returns The processed entitlement object if found in the cache and the type matches, otherwise `null`.
220
+ * Logs a warning if entitlements haven't been fetched or if a type mismatch occurs.
221
+ */
222
+ getEntitlement(t, e) {
223
+ if (!this.entitlementsCache)
224
+ return console.warn(
225
+ `[Kelviq SDK] getEntitlement: Entitlements not fetched or cache is empty for featureId '${t}'. Call fetchAllEntitlements() first.`
226
+ ), null;
227
+ const n = this.entitlementsCache[t];
228
+ return n ? n.type === e ? n : (console.warn(
229
+ `[Kelviq SDK] getEntitlement: Type mismatch for featureId '${t}'. Expected type '${e}', but cached type is '${n.type}'.`
230
+ ), null) : null;
231
+ }
232
+ /**
233
+ * Checks if the user has access to a specific feature based on cached entitlements.
234
+ * Note: `fetchAllEntitlements()` should be called successfully at least once before using this method.
235
+ *
236
+ * @param featureId The key of the feature.
237
+ * @returns `true` if access is granted, `false` if denied or the feature is not found in the cache.
238
+ * Logs a warning and returns `false` if entitlements haven't been fetched.
239
+ */
240
+ hasAccess(t) {
241
+ if (!this.entitlementsCache)
242
+ return console.warn(
243
+ `[Kelviq SDK] hasAccess: Entitlements not fetched yet for featureId '${t}'. Call fetchAllEntitlements() first. Returning false.`
244
+ ), !1;
245
+ const e = this.entitlementsCache[t];
246
+ return e ? e.hasAccess : !1;
247
+ }
248
+ /**
249
+ * Returns the currently cached map of all entitlements.
250
+ * `fetchAllEntitlements()` should be called successfully at least once.
251
+ * @returns The EntitlementMap (a record of featureKey to entitlement data) or `null` if not fetched or an error occurred during the last fetch.
252
+ */
253
+ getAllEntitlements() {
254
+ return this.entitlementsCache;
255
+ }
256
+ /**
257
+ * Indicates if an entitlement fetch operation is currently in progress.
258
+ * @returns `true` if a fetch is in progress, `false` otherwise.
259
+ */
260
+ isLoading() {
261
+ return this.isFetching;
262
+ }
263
+ /**
264
+ * Returns the last error that occurred during an entitlement fetch operation.
265
+ * @returns An `Error` object if the last fetch failed, otherwise `null`.
266
+ */
267
+ getLastError() {
268
+ return this.lastFetchError;
269
+ }
270
+ /**
271
+ * Clears the local entitlement cache and resets loading and error states.
272
+ * After calling this, `fetchAllEntitlements()` will need to be called again to populate the cache.
273
+ */
274
+ clearCache() {
275
+ this.entitlementsCache = null, this.isFetching = !1, this.lastFetchError = null, console.log("[Kelviq SDK] Entitlement cache cleared.");
276
+ }
277
+ }
278
+ function _(r, t) {
279
+ const e = {
280
+ ...r,
281
+ initializeAndFetch: r.initializeAndFetch || !0,
282
+ apiConfig: r.apiConfig || {},
283
+ environment: r.environment || "production",
284
+ entitlementsPath: r.entitlementsPath || T
285
+ }, n = new P(e, t);
286
+ return e.initializeAndFetch === !0 && n.fetchAllEntitlements().catch((m) => {
287
+ console.error(
288
+ "[Kelviq SDK] Initial fetch on client creation failed:",
289
+ m.message
290
+ );
291
+ }), n;
292
+ }
293
+ export {
294
+ _ as default
295
+ };
@@ -0,0 +1 @@
1
+ (function(f,o){typeof exports=="object"&&typeof module<"u"?module.exports=o():typeof define=="function"&&define.amd?define(o):(f=typeof globalThis<"u"?globalThis:f||self,f.kelviqSDK=o())})(this,function(){"use strict";var b=Object.defineProperty;var P=(f,o,y)=>o in f?b(f,o,{enumerable:!0,configurable:!0,writable:!0,value:y}):f[o]=y;var A=(f,o,y)=>P(f,typeof o!="symbol"?o+"":o,y);function f(s){const n={};return!s||!Array.isArray(s.entitlements)?(console.warn("[Kelviq SDK] transformApiEntitlements: Invalid or empty entitlements array in API response.",s),n):(s.entitlements.forEach(e=>{if(!e||typeof e.featureId!="string"||typeof e.featureType!="string"){console.warn("[Kelviq SDK] transformApiEntitlements: Skipping invalid raw entitlement object:",e);return}let t=null;const d={featureId:e.featureId,hasAccess:e.hasAccess};if(e.featureType==="BOOLEAN")t={...d,type:"boolean"};else if(e.featureType==="CUSTOMIZABLE"){const i=e;t={...d,type:"customizable",configuration:typeof i.value=="number"?i.value:null}}else if(e.featureType==="METER"){const i=e,E=typeof i.usageLimit=="number"?i.usageLimit:null,l=typeof i.currentUsage=="number"?i.currentUsage:0;t={...d,type:"metered",limit:E,used:l,remaining:E!==null?E-l:null,resetAt:i.resetAt,hardLimit:i.hardLimit}}else{const i=e;console.warn(`[Kelviq SDK] transformApiEntitlements: Encountered unknown or unhandled entitlement featureType: '${String(i.featureType)}' for feature '${String(i.feature)}'. This entitlement will be ignored.`)}t&&(n[t.featureId]=t)}),n)}const o="GET",y=5e3,R=3,$=1e3;function F(s){const{method:n=o,headers:e={},body:t,timeout:d=y,maxRetries:i=R,backoffBaseDelay:E=$,queryParams:l,accessToken:T}=s;let a=s.url,g=0;return new Promise((S,c)=>{if(l){const u=new URLSearchParams;for(const p in l)l[p]!==void 0&&u.append(p,String(l[p]));u.toString()&&(a=a+(a.includes("?")?"&":"?")+u.toString())}function h(){const u=new XMLHttpRequest;u.open(n,a,!0),u.timeout=d;const p={Accept:"application/json","X-Requested-With":"XMLHttpRequest",...e};n!=="GET"&&t&&typeof t=="object"&&!(t instanceof FormData)&&(p["Content-Type"]||(p["Content-Type"]="application/json")),T&&(p.Authorization=`Bearer ${T}`);for(const[r,m]of Object.entries(p))u.setRequestHeader(r,m);u.onload=function(){const{status:r,statusText:m,responseText:v}=u;if(r>=200&&r<300)try{const q=v?JSON.parse(v):{};S({status:r,statusText:m,data:q})}catch(q){const D=q instanceof Error?q:new Error(String(q));console.error(`Kelviq SDK (apiRequest): Invalid JSON response for URL ${a}. Error: ${D.message}. Response: ${v}`),c(new Error(`Invalid JSON response: ${D.message}`))}else K(`Request to ${a} failed with status ${r} ${m}. Response: ${v}`)},u.onerror=()=>K(`Network error for URL ${a}. The request could not be completed.`),u.ontimeout=()=>K(`Request to ${a} timed out.`);function K(r){if(g<i){g++;const m=Math.min(E*Math.pow(2,g-1),3e4);console.warn(`Kelviq SDK (apiRequest): Retrying request to ${a}. Attempt ${g}/${i}. Error: ${r}. Retrying in ${m}ms.`),setTimeout(h,m)}else c(new Error(`${r} (Max retries ${i} reached)`))}try{let r=t;t&&typeof t=="object"&&!(t instanceof FormData)&&p["Content-Type"]==="application/json"&&(r=JSON.stringify(t)),u.send(r)}catch(r){const m=r instanceof Error?r:new Error(String(r));console.error(`Kelviq SDK (apiRequest): Error sending request to ${a}.`,m),c(new Error(`Failed to send request: ${m.message}`))}}h()})}const L="https://edge.api.kelviq.com/api/v1/",w="https://edge.sandboxapi.kelviq.com/api/v1/",C="entitlements/";class I{constructor(n,e){A(this,"options");A(this,"entitlementsCache",null);A(this,"isFetching",!1);A(this,"lastFetchError",null);A(this,"apiRequestService");if(!n||!n.customerId)throw new Error("[Kelviq SDK] KelviqClient: CustomerId is required in options.");this.options={...n,apiConfig:n.apiConfig||{},entitlementsPath:n.entitlementsPath||C},this.apiRequestService=e||F}async fetchAllEntitlements(n=!1){if(this.isFetching)return Promise.reject(new Error("[Kelviq SDK] An entitlement fetch is already in progress."));if(this.entitlementsCache&&!n)return Promise.resolve(this.entitlementsCache);this.isFetching=!0,this.lastFetchError=null;const{apiUrl:e=this.options.environment==="sandbox"?w:L,entitlementsPath:t,customerId:d,accessToken:i,apiConfig:E,onError:l}=this.options,T=e.replace(/\/$/,""),a=t.replace(/^\//,""),g=`${T}/${a}`,S={customer_id:d};try{const c=await this.apiRequestService({url:g,method:"GET",queryParams:S,accessToken:i,...E||{}});if(c&&c.data&&Array.isArray(c.data.entitlements))return this.entitlementsCache=f(c.data),this.isFetching=!1,this.entitlementsCache;{const h=new Error("[Kelviq SDK] Received empty, malformed, or invalid data structure from entitlements API.");throw this.lastFetchError=h,this.isFetching=!1,this.entitlementsCache=null,l&&l(h),h}}catch(c){const h=c instanceof Error?c:new Error(String(c));throw this.lastFetchError=h,this.isFetching=!1,this.entitlementsCache=null,l&&l(h),h}}getEntitlement(n,e){if(!this.entitlementsCache)return console.warn(`[Kelviq SDK] getEntitlement: Entitlements not fetched or cache is empty for featureId '${n}'. Call fetchAllEntitlements() first.`),null;const t=this.entitlementsCache[n];return t?t.type===e?t:(console.warn(`[Kelviq SDK] getEntitlement: Type mismatch for featureId '${n}'. Expected type '${e}', but cached type is '${t.type}'.`),null):null}hasAccess(n){if(!this.entitlementsCache)return console.warn(`[Kelviq SDK] hasAccess: Entitlements not fetched yet for featureId '${n}'. Call fetchAllEntitlements() first. Returning false.`),!1;const e=this.entitlementsCache[n];return e?e.hasAccess:!1}getAllEntitlements(){return this.entitlementsCache}isLoading(){return this.isFetching}getLastError(){return this.lastFetchError}clearCache(){this.entitlementsCache=null,this.isFetching=!1,this.lastFetchError=null,console.log("[Kelviq SDK] Entitlement cache cleared.")}}function U(s,n){const e={...s,initializeAndFetch:s.initializeAndFetch||!0,apiConfig:s.apiConfig||{},environment:s.environment||"production",entitlementsPath:s.entitlementsPath||C},t=new I(e,n);return e.initializeAndFetch===!0&&t.fetchAllEntitlements().catch(d=>{console.error("[Kelviq SDK] Initial fetch on client creation failed:",d.message)}),t}return U});
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@kelviq/js-sdk",
3
+ "version": "1.0.0",
4
+ "module": "./dist/kelviq-js-sdk.js",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "package.json",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/kelviq-js-sdk.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./dist/": {
17
+ "import": "./dist/",
18
+ "types": "./dist/"
19
+ }
20
+ },
21
+ "types": "./dist/index.d.ts",
22
+ "scripts": {
23
+ "dev": "vite --host",
24
+ "build": "rimraf dist/**/* && tsc && vite build && dts-bundle-generator --config ./dts-bundle-generator.config.ts",
25
+ "test": "vitest",
26
+ "test:coverage": "vitest --coverage",
27
+ "lint:scripts": "eslint . --ext .ts",
28
+ "lint:styles": "stylelint ./**/*.{css,scss}",
29
+ "format:scripts": "prettier . --write",
30
+ "format:styles": "stylelint ./**/*.{css,scss} --fix",
31
+ "format": "npm run format:scripts && npm run format:styles",
32
+ "prepare": "husky && echo 'npx lint-staged' > .husky/pre-commit && git add .husky/pre-commit",
33
+ "uninstall-husky": "npm uninstall husky --no-save && git config --unset core.hooksPath && npx rimraf .husky"
34
+ },
35
+ "devDependencies": {
36
+ "@types/jsdom": "^21.1.7",
37
+ "@types/node": "^22.0.0",
38
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
39
+ "@typescript-eslint/parser": "^7.18.0",
40
+ "@vitest/coverage-v8": "^2.0.4",
41
+ "copyfiles": "^2.4.1",
42
+ "dts-bundle-generator": "^9.5.1",
43
+ "eslint": "^8.57.0",
44
+ "eslint-config-prettier": "^9.1.0",
45
+ "eslint-plugin-prettier": "^5.2.1",
46
+ "husky": "^9.1.4",
47
+ "lint-staged": "^15.2.7",
48
+ "postcss": "^8.4.40",
49
+ "postcss-scss": "^4.0.9",
50
+ "prettier": "^3.3.3",
51
+ "rimraf": "^6.0.1",
52
+ "stylelint": "^16.8.1",
53
+ "stylelint-config-recommended": "^14.0.1",
54
+ "stylelint-config-sass-guidelines": "^12.0.0",
55
+ "stylelint-order": "^6.0.4",
56
+ "stylelint-prettier": "^5.0.2",
57
+ "ts-node": "^10.9.2",
58
+ "typescript": "^5.3.3",
59
+ "vite": "^5.3.5",
60
+ "vitest": "^2.0.4"
61
+ }
62
+ }