@entitlehub/sdk 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/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @entitlehub/sdk
2
+
3
+ One entitlement API across **App Store, Google Play, Stripe, and web** — for browsers, React Native, Expo, and Node. Stop checking raw product IDs; ask *"what is this user entitled to right now?"*
4
+
5
+ ```bash
6
+ npm install @entitlehub/sdk
7
+ ```
8
+
9
+ Zero dependencies. Uses the global `fetch` (Node 18+, all modern browsers, React Native, Expo).
10
+
11
+ ## Client (in your app) — publishable key
12
+
13
+ Reads only. Safe to ship in your app. **Never put a secret (`sk_`) key here.**
14
+
15
+ ```ts
16
+ import { EntitleHub } from "@entitlehub/sdk";
17
+
18
+ const eh = new EntitleHub({
19
+ apiKey: "pk_live_…", // your publishable key
20
+ appUserId: user.id, // your own stable user id
21
+ });
22
+
23
+ const info = await eh.getCustomerInfo();
24
+ if (info.isActive("pro")) {
25
+ unlockProFeatures();
26
+ }
27
+
28
+ // full detail when you need it
29
+ const pro = info.active["pro"];
30
+ // → { entitlement: "pro", status: "active", store: "app_store", expires_at, will_renew, … }
31
+ ```
32
+
33
+ ### React / React Native
34
+
35
+ ```tsx
36
+ const eh = new EntitleHub({ apiKey: "pk_live_…", appUserId: user.id });
37
+
38
+ useEffect(() => {
39
+ const unsub = eh.addCustomerInfoUpdateListener((info) => setPro(info.isActive("pro")));
40
+ eh.getCustomerInfo(); // triggers the listener
41
+ return unsub;
42
+ }, []);
43
+ ```
44
+
45
+ ### After your own login / logout
46
+
47
+ ```ts
48
+ await eh.logIn(newUserId); // switches user, refreshes, clears cache
49
+ eh.logOut(); // forget cached info
50
+ ```
51
+
52
+ ### API (client)
53
+
54
+ | Method | Returns | Notes |
55
+ |---|---|---|
56
+ | `getCustomerInfo({ fetchPolicy })` | `CustomerInfo` | `cache-first` (default) or `network-only`. Cached `cacheTtlMs` (5 min default). |
57
+ | `isEntitled(id)` | `boolean` | Convenience over cached info. |
58
+ | `checkEntitlement(id)` | `CheckResult` | Always hits the server; includes a `reason` when inactive. |
59
+ | `getOfferings()` | `Offerings` | Entitlements + products, for building a paywall. |
60
+ | `addCustomerInfoUpdateListener(fn)` | `() => void` | Returns an unsubscribe. |
61
+
62
+ ## Server (in your backend) — secret key
63
+
64
+ Report purchases and grant entitlements. **Server only.**
65
+
66
+ ```ts
67
+ import { EntitleHubServer } from "@entitlehub/sdk";
68
+
69
+ const eh = new EntitleHubServer({ apiKey: process.env.ENTITLEHUB_SECRET_KEY! });
70
+
71
+ // After the store purchase is confirmed on-device, report it from your server:
72
+ const info = await eh.reportPurchase(userId, {
73
+ store: "app_store",
74
+ storeProductId: "pro_monthly",
75
+ });
76
+ info.isActive("pro"); // → true
77
+
78
+ // Grant directly (promo / comp / support), no purchase:
79
+ await eh.grantEntitlement(userId, "pro", { durationDays: 30 });
80
+ ```
81
+
82
+ | Method | Returns |
83
+ |---|---|
84
+ | `reportPurchase(userId, { store, storeProductId, isSandbox?, transactionId? })` | `CustomerInfo` |
85
+ | `grantEntitlement(userId, id, { durationDays?, isSandbox? })` | `CustomerInfo` |
86
+ | `getCustomerInfo(userId)` / `check(userId, id)` | `CustomerInfo` / `CheckResult` |
87
+
88
+ ## Already have an IAP setup?
89
+
90
+ Keep making the actual store purchase with your existing IAP flow (StoreKit / Play Billing /
91
+ `expo-iap` / `react-native-iap`), then `reportPurchase(...)` from your server and read entitlements
92
+ with `getCustomerInfo()`. See the full guide at [entitlehub.com/docs](https://entitlehub.com).
93
+
94
+ ## Errors
95
+
96
+ Every failure throws an `EntitleHubError` with `.status` and `.code` (`auth` | `http` | `network` | `config`).
97
+
98
+ ```ts
99
+ import { EntitleHubError } from "@entitlehub/sdk";
100
+ try { await eh.getCustomerInfo(); }
101
+ catch (e) { if (e instanceof EntitleHubError && e.code === "auth") relogin(); }
102
+ ```
103
+
104
+ ## Self-hosting
105
+
106
+ Point the SDK at your own EntitleHub with `baseUrl`:
107
+
108
+ ```ts
109
+ new EntitleHub({ apiKey, appUserId, baseUrl: "https://entitlements.yourco.com/v1" });
110
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var d=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var x=(n,e)=>{for(var t in e)d(n,t,{get:e[t],enumerable:!0})},v=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of k(e))!E.call(n,i)&&i!==t&&d(n,i,{get:()=>e[i],enumerable:!(r=C(e,i))||r.enumerable});return n};var O=n=>v(d({},"__esModule",{value:!0}),n);var T={};x(T,{CustomerInfo:()=>a,EntitleHub:()=>l,EntitleHubError:()=>s,EntitleHubServer:()=>f});module.exports=O(T);var a=class{constructor(e){this.appUserId=e.app_user_id,this.entitlements=e.active_entitlements??[];let t={};for(let r of this.entitlements)t[r.entitlement]=r;this.active=t}isActive(e){return!!this.active[e]}get activeEntitlementIds(){return Object.keys(this.active)}expirationDate(e){let t=this.active[e];if(t)return t.expires_at?new Date(t.expires_at):null}};var s=class extends Error{constructor(e,t,r="error"){super(e),this.name="EntitleHubError",this.status=t,this.code=r}},P=n=>new Promise(e=>setTimeout(e,n));async function c(n,e,t,r){let i=n.fetchImpl??globalThis.fetch;if(!i)throw new s("No fetch implementation available. On Node <18, pass fetchImpl.",0,"no_fetch");let h=`${n.baseUrl.replace(/\/$/,"")}${t}`,y=e==="GET"?n.retries??2:0,m;for(let u=0;u<=y;u++){let b=new AbortController,g=n.timeoutMs?setTimeout(()=>b.abort(),n.timeoutMs):void 0;try{let o=await i(h,{method:e,headers:{Authorization:`Bearer ${n.apiKey}`,...r?{"Content-Type":"application/json"}:{}},body:r?JSON.stringify(r):void 0,signal:b.signal}),I=await o.text(),p=I?_(I):{};if(!o.ok){let w=`HTTP ${o.status}`;throw p&&typeof p=="object"&&"error"in p&&(w=String(p.error)),new s(w,o.status,o.status===401||o.status===403?"auth":"http")}return p}catch(o){if(m=o,o instanceof s)throw o;u<y&&await P(250*(u+1))}finally{g&&clearTimeout(g)}}throw new s(`Network request failed: ${String(m?.message??m)}`,0,"network")}function _(n){try{return JSON.parse(n)}catch{return{}}}var l=class{constructor(e){this.listeners=new Set;if(!e.apiKey)throw new s("apiKey is required.",0,"config");if(!e.appUserId)throw new s("appUserId is required.",0,"config");if(e.apiKey.startsWith("sk_"))throw new s("Never use a secret (sk_) key in the client SDK \u2014 use your publishable (pk_) key.",0,"config");this.appUserId=e.appUserId,this.cacheTtlMs=e.cacheTtlMs??5*6e4,this.http={baseUrl:e.baseUrl??"https://entitlehub.com/v1",apiKey:e.apiKey,fetchImpl:e.fetchImpl,timeoutMs:15e3}}get currentAppUserId(){return this.appUserId}async logIn(e){return this.appUserId=e,this.cached=void 0,this.getCustomerInfo({fetchPolicy:"network-only"})}logOut(){this.cached=void 0}async getCustomerInfo(e={}){if((e.fetchPolicy??"cache-first")==="cache-first"&&this.cached&&Date.now()-this.cached.at<this.cacheTtlMs)return this.cached.info;let r=await c(this.http,"GET",`/subscribers/${encodeURIComponent(this.appUserId)}`),i=new a(r);this.cached={info:i,at:Date.now()};for(let h of this.listeners)try{h(i)}catch{}return i}async isEntitled(e){return(await this.getCustomerInfo()).isActive(e)}async checkEntitlement(e,t={}){return c(this.http,"POST","/check",{app_user_id:this.appUserId,entitlement:e,sandbox:!!t.sandbox})}async getOfferings(){return c(this.http,"GET","/offerings")}addCustomerInfoUpdateListener(e){return this.listeners.add(e),()=>this.listeners.delete(e)}};var f=class{constructor(e){if(!e.apiKey)throw new s("apiKey is required.",0,"config");if(!e.apiKey.startsWith("sk_"))throw new s("EntitleHubServer needs a secret (sk_) key. For client reads, use the EntitleHub class with a pk_ key.",0,"config");this.http={baseUrl:e.baseUrl??"https://entitlehub.com/v1",apiKey:e.apiKey,fetchImpl:e.fetchImpl,timeoutMs:15e3}}async reportPurchase(e,t){let r=await c(this.http,"POST",`/subscribers/${encodeURIComponent(e)}/purchases`,{store:t.store,store_product_id:t.storeProductId,is_sandbox:!!t.isSandbox,transaction_id:t.transactionId??"",purchase_token:t.purchaseToken??"",is_subscription:!!t.isSubscription,signed_transaction:t.signedTransaction??""});return new a(r)}async grantEntitlement(e,t,r={}){let i=await c(this.http,"POST",`/subscribers/${encodeURIComponent(e)}/grant`,{entitlement:t,duration_days:r.durationDays??0,is_sandbox:!!r.isSandbox});return new a(i)}async getCustomerInfo(e){let t=await c(this.http,"GET",`/subscribers/${encodeURIComponent(e)}`);return new a(t)}async check(e,t,r={}){return c(this.http,"POST","/check",{app_user_id:e,entitlement:t,sandbox:!!r.sandbox})}};0&&(module.exports={CustomerInfo,EntitleHub,EntitleHubError,EntitleHubServer});
@@ -0,0 +1,195 @@
1
+ type Store = "app_store" | "play" | "stripe" | "web" | "amazon";
2
+ type EntitlementStatus = "active" | "trial" | "grace" | "expired" | "revoked";
3
+ /** One entitlement a subscriber currently holds (from GET /v1/subscribers/{id}). */
4
+ interface ActiveEntitlement {
5
+ entitlement: string;
6
+ status: EntitlementStatus | string;
7
+ store: Store | string;
8
+ product: string;
9
+ expires_at: string | null;
10
+ will_renew: boolean;
11
+ since: string | null;
12
+ }
13
+ /** Raw customer payload from the API. */
14
+ interface CustomerInfoResponse {
15
+ app_user_id: string;
16
+ active_entitlements: ActiveEntitlement[];
17
+ }
18
+ /** Result of POST /v1/check for a single entitlement. */
19
+ interface CheckResult {
20
+ app_user_id: string;
21
+ entitlement: string;
22
+ active: boolean;
23
+ status?: string;
24
+ store?: string;
25
+ product?: string;
26
+ expires_at?: string;
27
+ will_renew: boolean;
28
+ since?: string;
29
+ /** When inactive, why: unknown_entitlement | unknown_subscriber | no_active_grant. */
30
+ reason?: string;
31
+ }
32
+ interface OfferingEntitlement {
33
+ id: string;
34
+ key: string;
35
+ name: string;
36
+ description: string;
37
+ active_count: number;
38
+ product_count: number;
39
+ stores: Store[];
40
+ }
41
+ interface OfferingProduct {
42
+ id: string;
43
+ store: Store;
44
+ store_product_id: string;
45
+ type: "subscription" | "non_consumable" | "consumable" | string;
46
+ duration: string;
47
+ display_name: string;
48
+ price_micros: number;
49
+ currency: string;
50
+ entitlements: string[];
51
+ }
52
+ interface Offerings {
53
+ entitlements: OfferingEntitlement[];
54
+ products: OfferingProduct[];
55
+ }
56
+ interface PurchaseInput {
57
+ store: Store;
58
+ storeProductId: string;
59
+ isSandbox?: boolean;
60
+ transactionId?: string;
61
+ /**
62
+ * Google Play purchase token. When set, EntitleHub validates it against the project's
63
+ * Play service account before granting (the authenticated expiry wins). Pair with
64
+ * `isSubscription` for auto-renewable products.
65
+ */
66
+ purchaseToken?: string;
67
+ /** Google Play: true for an auto-renewable subscription, false/omit for a one-time product. */
68
+ isSubscription?: boolean;
69
+ /**
70
+ * Apple StoreKit 2 signed transaction (JWS). When set, EntitleHub verifies it against
71
+ * Apple's root CA; the authenticated product id / environment override `store` /
72
+ * `storeProductId`, so a client can't forge a purchase.
73
+ */
74
+ signedTransaction?: string;
75
+ }
76
+
77
+ /**
78
+ * Ergonomic wrapper around a subscriber's active entitlements — the object your UI reads.
79
+ *
80
+ * const info = await eh.getCustomerInfo();
81
+ * if (info.isActive("pro")) showProFeatures();
82
+ * const ent = info.active["pro"]; // full detail (expiry, store, willRenew)
83
+ */
84
+ declare class CustomerInfo {
85
+ readonly appUserId: string;
86
+ /** Active entitlements keyed by identifier. */
87
+ readonly active: Readonly<Record<string, ActiveEntitlement>>;
88
+ /** Raw list, in case you want to iterate. */
89
+ readonly entitlements: readonly ActiveEntitlement[];
90
+ constructor(raw: CustomerInfoResponse);
91
+ /** True if the subscriber currently holds this entitlement. */
92
+ isActive(entitlementId: string): boolean;
93
+ /** All active entitlement identifiers. */
94
+ get activeEntitlementIds(): string[];
95
+ /** Expiry for an entitlement (null = lifetime, undefined = not held). */
96
+ expirationDate(entitlementId: string): Date | null | undefined;
97
+ }
98
+
99
+ interface EntitleHubOptions {
100
+ /** Your publishable key (pk_live_… / pk_test_…). Client-safe. */
101
+ apiKey: string;
102
+ /** The user this device is acting as (your own stable user id). */
103
+ appUserId: string;
104
+ /** Override the API base (defaults to https://entitlehub.com/v1). */
105
+ baseUrl?: string;
106
+ /** Custom fetch (Node <18, tests). Defaults to global fetch. */
107
+ fetchImpl?: typeof fetch;
108
+ /** How long a cached CustomerInfo is considered fresh, ms (default 5 min). */
109
+ cacheTtlMs?: number;
110
+ }
111
+ type CustomerInfoListener = (info: CustomerInfo) => void;
112
+ type FetchPolicy = "cache-first" | "network-only";
113
+ /**
114
+ * The client-side EntitleHub SDK — configure once, then ask "what is this user entitled to?".
115
+ * Safe for browsers, React Native, and Expo (publishable key only; never ship a secret key).
116
+ *
117
+ * const eh = new EntitleHub({ apiKey: "pk_live_…", appUserId: user.id });
118
+ * const info = await eh.getCustomerInfo();
119
+ * if (info.isActive("pro")) unlockPro();
120
+ */
121
+ declare class EntitleHub {
122
+ private http;
123
+ private appUserId;
124
+ private cacheTtlMs;
125
+ private cached?;
126
+ private listeners;
127
+ constructor(opts: EntitleHubOptions);
128
+ /** The current app user id. */
129
+ get currentAppUserId(): string;
130
+ /** Switch the acting user (e.g. after your own login). Clears the cache. */
131
+ logIn(appUserId: string): Promise<CustomerInfo>;
132
+ /** Forget the cached user's info. Pair with your own logout. */
133
+ logOut(): void;
134
+ /** The subscriber's active entitlements. Cached for `cacheTtlMs`; pass network-only to force a refresh. */
135
+ getCustomerInfo(opts?: {
136
+ fetchPolicy?: FetchPolicy;
137
+ }): Promise<CustomerInfo>;
138
+ /** Convenience: is this user entitled right now? Uses cached CustomerInfo. */
139
+ isEntitled(entitlementId: string): Promise<boolean>;
140
+ /** Authoritative single-entitlement check (always hits the server; includes the reason when inactive). */
141
+ checkEntitlement(entitlementId: string, opts?: {
142
+ sandbox?: boolean;
143
+ }): Promise<CheckResult>;
144
+ /** The project's entitlements + products — for building a paywall. */
145
+ getOfferings(): Promise<Offerings>;
146
+ /** Subscribe to CustomerInfo refreshes. Returns an unsubscribe function. */
147
+ addCustomerInfoUpdateListener(listener: CustomerInfoListener): () => void;
148
+ }
149
+
150
+ interface EntitleHubServerOptions {
151
+ /** Your secret key (sk_live_… / sk_test_…). SERVER ONLY — never ship this to a client. */
152
+ apiKey: string;
153
+ baseUrl?: string;
154
+ fetchImpl?: typeof fetch;
155
+ }
156
+ /**
157
+ * The server-side EntitleHub SDK — report purchases and grant entitlements from your backend.
158
+ * Keep this on your server; it uses a secret key.
159
+ *
160
+ * const eh = new EntitleHubServer({ apiKey: process.env.ENTITLEHUB_SECRET_KEY! });
161
+ * await eh.reportPurchase(userId, { store: "app_store", storeProductId: "pro_monthly" });
162
+ */
163
+ declare class EntitleHubServer {
164
+ private http;
165
+ constructor(opts: EntitleHubServerOptions);
166
+ /**
167
+ * Report a completed store purchase → writes the mapped entitlement grant(s). Returns updated info.
168
+ *
169
+ * Three modes (pick one):
170
+ * • Google (validated): { store:"play", storeProductId, purchaseToken, isSubscription }
171
+ * • Apple (validated): { signedTransaction } — store/product are read from the JWS
172
+ * • Trusted server-report: { store, storeProductId } — no store validation; only when you've
173
+ * already validated the receipt elsewhere.
174
+ */
175
+ reportPurchase(appUserId: string, purchase: PurchaseInput): Promise<CustomerInfo>;
176
+ /** Grant an entitlement directly (promo / comp / support), no store purchase. */
177
+ grantEntitlement(appUserId: string, entitlementId: string, opts?: {
178
+ durationDays?: number;
179
+ isSandbox?: boolean;
180
+ }): Promise<CustomerInfo>;
181
+ /** Read a subscriber's active entitlements. */
182
+ getCustomerInfo(appUserId: string): Promise<CustomerInfo>;
183
+ /** Authoritative single-entitlement check. */
184
+ check(appUserId: string, entitlementId: string, opts?: {
185
+ sandbox?: boolean;
186
+ }): Promise<CheckResult>;
187
+ }
188
+
189
+ declare class EntitleHubError extends Error {
190
+ readonly status: number;
191
+ readonly code: string;
192
+ constructor(message: string, status: number, code?: string);
193
+ }
194
+
195
+ export { type ActiveEntitlement, type CheckResult, CustomerInfo, type CustomerInfoListener, type CustomerInfoResponse, EntitleHub, EntitleHubError, type EntitleHubOptions, EntitleHubServer, type EntitleHubServerOptions, type EntitlementStatus, type FetchPolicy, type OfferingEntitlement, type OfferingProduct, type Offerings, type PurchaseInput, type Store };
@@ -0,0 +1,195 @@
1
+ type Store = "app_store" | "play" | "stripe" | "web" | "amazon";
2
+ type EntitlementStatus = "active" | "trial" | "grace" | "expired" | "revoked";
3
+ /** One entitlement a subscriber currently holds (from GET /v1/subscribers/{id}). */
4
+ interface ActiveEntitlement {
5
+ entitlement: string;
6
+ status: EntitlementStatus | string;
7
+ store: Store | string;
8
+ product: string;
9
+ expires_at: string | null;
10
+ will_renew: boolean;
11
+ since: string | null;
12
+ }
13
+ /** Raw customer payload from the API. */
14
+ interface CustomerInfoResponse {
15
+ app_user_id: string;
16
+ active_entitlements: ActiveEntitlement[];
17
+ }
18
+ /** Result of POST /v1/check for a single entitlement. */
19
+ interface CheckResult {
20
+ app_user_id: string;
21
+ entitlement: string;
22
+ active: boolean;
23
+ status?: string;
24
+ store?: string;
25
+ product?: string;
26
+ expires_at?: string;
27
+ will_renew: boolean;
28
+ since?: string;
29
+ /** When inactive, why: unknown_entitlement | unknown_subscriber | no_active_grant. */
30
+ reason?: string;
31
+ }
32
+ interface OfferingEntitlement {
33
+ id: string;
34
+ key: string;
35
+ name: string;
36
+ description: string;
37
+ active_count: number;
38
+ product_count: number;
39
+ stores: Store[];
40
+ }
41
+ interface OfferingProduct {
42
+ id: string;
43
+ store: Store;
44
+ store_product_id: string;
45
+ type: "subscription" | "non_consumable" | "consumable" | string;
46
+ duration: string;
47
+ display_name: string;
48
+ price_micros: number;
49
+ currency: string;
50
+ entitlements: string[];
51
+ }
52
+ interface Offerings {
53
+ entitlements: OfferingEntitlement[];
54
+ products: OfferingProduct[];
55
+ }
56
+ interface PurchaseInput {
57
+ store: Store;
58
+ storeProductId: string;
59
+ isSandbox?: boolean;
60
+ transactionId?: string;
61
+ /**
62
+ * Google Play purchase token. When set, EntitleHub validates it against the project's
63
+ * Play service account before granting (the authenticated expiry wins). Pair with
64
+ * `isSubscription` for auto-renewable products.
65
+ */
66
+ purchaseToken?: string;
67
+ /** Google Play: true for an auto-renewable subscription, false/omit for a one-time product. */
68
+ isSubscription?: boolean;
69
+ /**
70
+ * Apple StoreKit 2 signed transaction (JWS). When set, EntitleHub verifies it against
71
+ * Apple's root CA; the authenticated product id / environment override `store` /
72
+ * `storeProductId`, so a client can't forge a purchase.
73
+ */
74
+ signedTransaction?: string;
75
+ }
76
+
77
+ /**
78
+ * Ergonomic wrapper around a subscriber's active entitlements — the object your UI reads.
79
+ *
80
+ * const info = await eh.getCustomerInfo();
81
+ * if (info.isActive("pro")) showProFeatures();
82
+ * const ent = info.active["pro"]; // full detail (expiry, store, willRenew)
83
+ */
84
+ declare class CustomerInfo {
85
+ readonly appUserId: string;
86
+ /** Active entitlements keyed by identifier. */
87
+ readonly active: Readonly<Record<string, ActiveEntitlement>>;
88
+ /** Raw list, in case you want to iterate. */
89
+ readonly entitlements: readonly ActiveEntitlement[];
90
+ constructor(raw: CustomerInfoResponse);
91
+ /** True if the subscriber currently holds this entitlement. */
92
+ isActive(entitlementId: string): boolean;
93
+ /** All active entitlement identifiers. */
94
+ get activeEntitlementIds(): string[];
95
+ /** Expiry for an entitlement (null = lifetime, undefined = not held). */
96
+ expirationDate(entitlementId: string): Date | null | undefined;
97
+ }
98
+
99
+ interface EntitleHubOptions {
100
+ /** Your publishable key (pk_live_… / pk_test_…). Client-safe. */
101
+ apiKey: string;
102
+ /** The user this device is acting as (your own stable user id). */
103
+ appUserId: string;
104
+ /** Override the API base (defaults to https://entitlehub.com/v1). */
105
+ baseUrl?: string;
106
+ /** Custom fetch (Node <18, tests). Defaults to global fetch. */
107
+ fetchImpl?: typeof fetch;
108
+ /** How long a cached CustomerInfo is considered fresh, ms (default 5 min). */
109
+ cacheTtlMs?: number;
110
+ }
111
+ type CustomerInfoListener = (info: CustomerInfo) => void;
112
+ type FetchPolicy = "cache-first" | "network-only";
113
+ /**
114
+ * The client-side EntitleHub SDK — configure once, then ask "what is this user entitled to?".
115
+ * Safe for browsers, React Native, and Expo (publishable key only; never ship a secret key).
116
+ *
117
+ * const eh = new EntitleHub({ apiKey: "pk_live_…", appUserId: user.id });
118
+ * const info = await eh.getCustomerInfo();
119
+ * if (info.isActive("pro")) unlockPro();
120
+ */
121
+ declare class EntitleHub {
122
+ private http;
123
+ private appUserId;
124
+ private cacheTtlMs;
125
+ private cached?;
126
+ private listeners;
127
+ constructor(opts: EntitleHubOptions);
128
+ /** The current app user id. */
129
+ get currentAppUserId(): string;
130
+ /** Switch the acting user (e.g. after your own login). Clears the cache. */
131
+ logIn(appUserId: string): Promise<CustomerInfo>;
132
+ /** Forget the cached user's info. Pair with your own logout. */
133
+ logOut(): void;
134
+ /** The subscriber's active entitlements. Cached for `cacheTtlMs`; pass network-only to force a refresh. */
135
+ getCustomerInfo(opts?: {
136
+ fetchPolicy?: FetchPolicy;
137
+ }): Promise<CustomerInfo>;
138
+ /** Convenience: is this user entitled right now? Uses cached CustomerInfo. */
139
+ isEntitled(entitlementId: string): Promise<boolean>;
140
+ /** Authoritative single-entitlement check (always hits the server; includes the reason when inactive). */
141
+ checkEntitlement(entitlementId: string, opts?: {
142
+ sandbox?: boolean;
143
+ }): Promise<CheckResult>;
144
+ /** The project's entitlements + products — for building a paywall. */
145
+ getOfferings(): Promise<Offerings>;
146
+ /** Subscribe to CustomerInfo refreshes. Returns an unsubscribe function. */
147
+ addCustomerInfoUpdateListener(listener: CustomerInfoListener): () => void;
148
+ }
149
+
150
+ interface EntitleHubServerOptions {
151
+ /** Your secret key (sk_live_… / sk_test_…). SERVER ONLY — never ship this to a client. */
152
+ apiKey: string;
153
+ baseUrl?: string;
154
+ fetchImpl?: typeof fetch;
155
+ }
156
+ /**
157
+ * The server-side EntitleHub SDK — report purchases and grant entitlements from your backend.
158
+ * Keep this on your server; it uses a secret key.
159
+ *
160
+ * const eh = new EntitleHubServer({ apiKey: process.env.ENTITLEHUB_SECRET_KEY! });
161
+ * await eh.reportPurchase(userId, { store: "app_store", storeProductId: "pro_monthly" });
162
+ */
163
+ declare class EntitleHubServer {
164
+ private http;
165
+ constructor(opts: EntitleHubServerOptions);
166
+ /**
167
+ * Report a completed store purchase → writes the mapped entitlement grant(s). Returns updated info.
168
+ *
169
+ * Three modes (pick one):
170
+ * • Google (validated): { store:"play", storeProductId, purchaseToken, isSubscription }
171
+ * • Apple (validated): { signedTransaction } — store/product are read from the JWS
172
+ * • Trusted server-report: { store, storeProductId } — no store validation; only when you've
173
+ * already validated the receipt elsewhere.
174
+ */
175
+ reportPurchase(appUserId: string, purchase: PurchaseInput): Promise<CustomerInfo>;
176
+ /** Grant an entitlement directly (promo / comp / support), no store purchase. */
177
+ grantEntitlement(appUserId: string, entitlementId: string, opts?: {
178
+ durationDays?: number;
179
+ isSandbox?: boolean;
180
+ }): Promise<CustomerInfo>;
181
+ /** Read a subscriber's active entitlements. */
182
+ getCustomerInfo(appUserId: string): Promise<CustomerInfo>;
183
+ /** Authoritative single-entitlement check. */
184
+ check(appUserId: string, entitlementId: string, opts?: {
185
+ sandbox?: boolean;
186
+ }): Promise<CheckResult>;
187
+ }
188
+
189
+ declare class EntitleHubError extends Error {
190
+ readonly status: number;
191
+ readonly code: string;
192
+ constructor(message: string, status: number, code?: string);
193
+ }
194
+
195
+ export { type ActiveEntitlement, type CheckResult, CustomerInfo, type CustomerInfoListener, type CustomerInfoResponse, EntitleHub, EntitleHubError, type EntitleHubOptions, EntitleHubServer, type EntitleHubServerOptions, type EntitlementStatus, type FetchPolicy, type OfferingEntitlement, type OfferingProduct, type Offerings, type PurchaseInput, type Store };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ var o=class{constructor(e){this.appUserId=e.app_user_id,this.entitlements=e.active_entitlements??[];let t={};for(let n of this.entitlements)t[n.entitlement]=n;this.active=t}isActive(e){return!!this.active[e]}get activeEntitlementIds(){return Object.keys(this.active)}expirationDate(e){let t=this.active[e];if(t)return t.expires_at?new Date(t.expires_at):null}};var s=class extends Error{constructor(e,t,n="error"){super(e),this.name="EntitleHubError",this.status=t,this.code=n}},w=r=>new Promise(e=>setTimeout(e,r));async function a(r,e,t,n){let c=r.fetchImpl??globalThis.fetch;if(!c)throw new s("No fetch implementation available. On Node <18, pass fetchImpl.",0,"no_fetch");let l=`${r.baseUrl.replace(/\/$/,"")}${t}`,d=e==="GET"?r.retries??2:0,f;for(let u=0;u<=d;u++){let y=new AbortController,b=r.timeoutMs?setTimeout(()=>y.abort(),r.timeoutMs):void 0;try{let i=await c(l,{method:e,headers:{Authorization:`Bearer ${r.apiKey}`,...n?{"Content-Type":"application/json"}:{}},body:n?JSON.stringify(n):void 0,signal:y.signal}),g=await i.text(),p=g?C(g):{};if(!i.ok){let I=`HTTP ${i.status}`;throw p&&typeof p=="object"&&"error"in p&&(I=String(p.error)),new s(I,i.status,i.status===401||i.status===403?"auth":"http")}return p}catch(i){if(f=i,i instanceof s)throw i;u<d&&await w(250*(u+1))}finally{b&&clearTimeout(b)}}throw new s(`Network request failed: ${String(f?.message??f)}`,0,"network")}function C(r){try{return JSON.parse(r)}catch{return{}}}var h=class{constructor(e){this.listeners=new Set;if(!e.apiKey)throw new s("apiKey is required.",0,"config");if(!e.appUserId)throw new s("appUserId is required.",0,"config");if(e.apiKey.startsWith("sk_"))throw new s("Never use a secret (sk_) key in the client SDK \u2014 use your publishable (pk_) key.",0,"config");this.appUserId=e.appUserId,this.cacheTtlMs=e.cacheTtlMs??5*6e4,this.http={baseUrl:e.baseUrl??"https://entitlehub.com/v1",apiKey:e.apiKey,fetchImpl:e.fetchImpl,timeoutMs:15e3}}get currentAppUserId(){return this.appUserId}async logIn(e){return this.appUserId=e,this.cached=void 0,this.getCustomerInfo({fetchPolicy:"network-only"})}logOut(){this.cached=void 0}async getCustomerInfo(e={}){if((e.fetchPolicy??"cache-first")==="cache-first"&&this.cached&&Date.now()-this.cached.at<this.cacheTtlMs)return this.cached.info;let n=await a(this.http,"GET",`/subscribers/${encodeURIComponent(this.appUserId)}`),c=new o(n);this.cached={info:c,at:Date.now()};for(let l of this.listeners)try{l(c)}catch{}return c}async isEntitled(e){return(await this.getCustomerInfo()).isActive(e)}async checkEntitlement(e,t={}){return a(this.http,"POST","/check",{app_user_id:this.appUserId,entitlement:e,sandbox:!!t.sandbox})}async getOfferings(){return a(this.http,"GET","/offerings")}addCustomerInfoUpdateListener(e){return this.listeners.add(e),()=>this.listeners.delete(e)}};var m=class{constructor(e){if(!e.apiKey)throw new s("apiKey is required.",0,"config");if(!e.apiKey.startsWith("sk_"))throw new s("EntitleHubServer needs a secret (sk_) key. For client reads, use the EntitleHub class with a pk_ key.",0,"config");this.http={baseUrl:e.baseUrl??"https://entitlehub.com/v1",apiKey:e.apiKey,fetchImpl:e.fetchImpl,timeoutMs:15e3}}async reportPurchase(e,t){let n=await a(this.http,"POST",`/subscribers/${encodeURIComponent(e)}/purchases`,{store:t.store,store_product_id:t.storeProductId,is_sandbox:!!t.isSandbox,transaction_id:t.transactionId??"",purchase_token:t.purchaseToken??"",is_subscription:!!t.isSubscription,signed_transaction:t.signedTransaction??""});return new o(n)}async grantEntitlement(e,t,n={}){let c=await a(this.http,"POST",`/subscribers/${encodeURIComponent(e)}/grant`,{entitlement:t,duration_days:n.durationDays??0,is_sandbox:!!n.isSandbox});return new o(c)}async getCustomerInfo(e){let t=await a(this.http,"GET",`/subscribers/${encodeURIComponent(e)}`);return new o(t)}async check(e,t,n={}){return a(this.http,"POST","/check",{app_user_id:e,entitlement:t,sandbox:!!n.sandbox})}};export{o as CustomerInfo,h as EntitleHub,s as EntitleHubError,m as EntitleHubServer};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@entitlehub/sdk",
3
+ "version": "0.1.0",
4
+ "description": "EntitleHub SDK — one entitlement API across App Store, Google Play, Stripe, and web. Works in browsers, React Native, Expo, and Node.",
5
+ "keywords": [
6
+ "entitlehub",
7
+ "entitlements",
8
+ "in-app-purchase",
9
+ "subscriptions",
10
+ "storekit",
11
+ "play-billing",
12
+ "stripe"
13
+ ],
14
+ "homepage": "https://entitlehub.com",
15
+ "repository": { "type": "git", "url": "git+https://github.com/DanCue44/EntitleHub.git", "directory": "sdk" },
16
+ "license": "MIT",
17
+ "publishConfig": { "access": "public" },
18
+ "type": "module",
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --minify",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "node --test",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "devDependencies": {
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.9.3"
46
+ }
47
+ }