revenuecat-api 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import createClient, { type ClientOptions } from "openapi-fetch";
2
+ import type { paths } from "./__generated/revenuecat-api-v2";
3
+ export * from "./__generated/revenuecat-api-v2";
4
+ export type CreateRevenueCatClientOptions = ClientOptions & {
5
+ automaticRateLimit?: boolean;
6
+ };
7
+ export declare function createRevenueCatClient(accessToken: string, options?: CreateRevenueCatClientOptions): createClient.PathBasedClient<paths, `${string}/${string}`>;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,EAAE,EACnB,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAC;AAG7D,cAAc,iCAAiC,CAAC;AAEhD,MAAM,MAAM,6BAA6B,GAAG,aAAa,GAAG;IAC1D,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAOF,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,6BAA6B,8DA2BxC"}
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createRevenueCatClient = createRevenueCatClient;
40
+ const openapi_fetch_1 = __importStar(require("openapi-fetch"));
41
+ const rateLimitMiddleware_1 = require("./rateLimitMiddleware");
42
+ __exportStar(require("./__generated/revenuecat-api-v2"), exports);
43
+ const defaultOptions = {
44
+ baseUrl: "https://api.revenuecat.com/v2",
45
+ automaticRateLimit: false,
46
+ };
47
+ function createRevenueCatClient(accessToken, options) {
48
+ if (!accessToken) {
49
+ // For non-TS users, we'll throw an error if the accessToken is not provided
50
+ throw new Error("accessToken is required");
51
+ }
52
+ const { automaticRateLimit, headers, ...otherOptions } = {
53
+ ...defaultOptions,
54
+ ...options,
55
+ };
56
+ const client = (0, openapi_fetch_1.default)({
57
+ ...otherOptions,
58
+ headers: {
59
+ Authorization: `Bearer ${accessToken}`,
60
+ ...headers,
61
+ },
62
+ });
63
+ const pathBasedClient = (0, openapi_fetch_1.wrapAsPathBasedClient)(client);
64
+ if (automaticRateLimit) {
65
+ client.use((0, rateLimitMiddleware_1.createRateLimitMiddleware)());
66
+ }
67
+ return pathBasedClient;
68
+ }
@@ -0,0 +1,3 @@
1
+ import { Middleware } from "openapi-fetch";
2
+ export declare const createRateLimitMiddleware: () => Middleware;
3
+ //# sourceMappingURL=rateLimitMiddleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimitMiddleware.d.ts","sourceRoot":"","sources":["../src/rateLimitMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAoM3C,eAAO,MAAM,yBAAyB,QAAO,UAuB5C,CAAC"}
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ // https://www.revenuecat.com/docs/api-v2#tag/Rate-Limit
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.createRateLimitMiddleware = void 0;
5
+ class RateLimitManager {
6
+ async delay(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ endpointStates = new Map();
10
+ maxRetries = 3;
11
+ maxQueueSize = 100;
12
+ getEndpointKey(request) {
13
+ const url = new URL(request.url);
14
+ return `${request.method}:${url.pathname}`;
15
+ }
16
+ getRetryAfterTime(response) {
17
+ const retryAfter = response.headers.get("Retry-After");
18
+ if (retryAfter) {
19
+ const seconds = parseInt(retryAfter, 10);
20
+ if (!isNaN(seconds)) {
21
+ return seconds;
22
+ }
23
+ }
24
+ // Fallback to 1 second if no valid Retry-After header
25
+ return 1;
26
+ }
27
+ async isRetryable(response) {
28
+ try {
29
+ // Clone the response to avoid consuming the body
30
+ const clonedResponse = response.clone();
31
+ const body = await clonedResponse.json();
32
+ // Check if retryable field exists and is false
33
+ if (typeof body === "object" && body !== null && "retryable" in body) {
34
+ return body.retryable !== false;
35
+ }
36
+ // Default to retryable if no retryable field is present
37
+ return true;
38
+ }
39
+ catch {
40
+ // If we can't parse the JSON, default to retryable
41
+ return true;
42
+ }
43
+ }
44
+ async waitForThrottle(request) {
45
+ const endpointKey = this.getEndpointKey(request);
46
+ // Initialize endpoint state if it doesn't exist
47
+ if (!this.endpointStates.has(endpointKey)) {
48
+ this.endpointStates.set(endpointKey, {
49
+ isThrottled: false,
50
+ retryAfter: 0,
51
+ queue: [],
52
+ lastRetryTime: 0,
53
+ processing: false,
54
+ });
55
+ }
56
+ const state = this.endpointStates.get(endpointKey);
57
+ // If throttled, wait for the retry-after time
58
+ if (state.isThrottled) {
59
+ const now = Date.now();
60
+ const waitTime = state.lastRetryTime + state.retryAfter * 1000 - now;
61
+ if (waitTime > 0) {
62
+ await this.delay(waitTime);
63
+ }
64
+ state.isThrottled = false;
65
+ }
66
+ }
67
+ async handleResponse(request, response) {
68
+ if (response.status !== 429) {
69
+ return response;
70
+ }
71
+ // Check if the response indicates it's not retryable
72
+ const shouldRetry = await this.isRetryable(response);
73
+ if (!shouldRetry) {
74
+ // If not retryable, return the original response immediately
75
+ return response;
76
+ }
77
+ const endpointKey = this.getEndpointKey(request);
78
+ // Initialize endpoint state if it doesn't exist
79
+ if (!this.endpointStates.has(endpointKey)) {
80
+ this.endpointStates.set(endpointKey, {
81
+ isThrottled: false,
82
+ retryAfter: 0,
83
+ queue: [],
84
+ lastRetryTime: 0,
85
+ processing: false,
86
+ });
87
+ }
88
+ const state = this.endpointStates.get(endpointKey);
89
+ let retryAfter = this.getRetryAfterTime(response);
90
+ state.isThrottled = true;
91
+ state.retryAfter = retryAfter;
92
+ state.lastRetryTime = Date.now();
93
+ let lastResponse = response;
94
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
95
+ // Wait for the retry-after time
96
+ await this.delay(retryAfter * 1000);
97
+ try {
98
+ const retryResponse = await fetch(request, {});
99
+ if (retryResponse.status !== 429) {
100
+ // Success, clear throttled state and return
101
+ state.isThrottled = false;
102
+ return retryResponse;
103
+ }
104
+ // Check if the retry response is also not retryable
105
+ const shouldRetryAgain = await this.isRetryable(retryResponse);
106
+ if (!shouldRetryAgain) {
107
+ // If not retryable, return the response immediately
108
+ state.isThrottled = false;
109
+ return retryResponse;
110
+ }
111
+ // If still 429, update retryAfter for next attempt
112
+ retryAfter = this.getRetryAfterTime(retryResponse);
113
+ state.retryAfter = retryAfter;
114
+ state.lastRetryTime = Date.now();
115
+ lastResponse = retryResponse;
116
+ }
117
+ catch (error) {
118
+ // If fetch fails, throw the error
119
+ state.isThrottled = false;
120
+ throw error;
121
+ }
122
+ }
123
+ // All retries exhausted, return the last 429 response
124
+ state.isThrottled = false;
125
+ return lastResponse;
126
+ }
127
+ getQueueSize(request) {
128
+ const endpointKey = this.getEndpointKey(request);
129
+ const state = this.endpointStates.get(endpointKey);
130
+ return state?.queue.length || 0;
131
+ }
132
+ warnIfQueueTooLarge(request) {
133
+ const queueSize = this.getQueueSize(request);
134
+ if (queueSize >= this.maxQueueSize) {
135
+ const endpointKey = this.getEndpointKey(request);
136
+ console.warn(`[RevenueCat API] Rate limit queue for ${endpointKey} has reached maximum size of ${this.maxQueueSize}. Consider implementing additional throttling.`);
137
+ }
138
+ }
139
+ }
140
+ const createRateLimitMiddleware = () => {
141
+ const rateLimitManager = new RateLimitManager();
142
+ return {
143
+ async onRequest({ request }) {
144
+ // Wait if the endpoint is currently throttled
145
+ await rateLimitManager.waitForThrottle(request);
146
+ // Warn if queue is getting too large
147
+ rateLimitManager.warnIfQueueTooLarge(request);
148
+ // Don't return anything - let the request proceed normally
149
+ return undefined;
150
+ },
151
+ async onResponse({ request, response }) {
152
+ // Handle 429 responses with retry logic
153
+ if (response.status === 429) {
154
+ return await rateLimitManager.handleResponse(request, response);
155
+ }
156
+ return undefined;
157
+ },
158
+ };
159
+ };
160
+ exports.createRateLimitMiddleware = createRateLimitMiddleware;
@@ -0,0 +1,11 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+ import { defineConfig } from "eslint/config";
5
+
6
+
7
+ export default defineConfig([
8
+ { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"] },
9
+ { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.node } },
10
+ tseslint.configs.recommended,
11
+ ]);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "revenuecat-api",
3
+ "version": "1.0.2",
4
+ "description": "Type-safe RevenueCat API client using fetch with automatic rate limiting",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [
8
+ "revenuecat",
9
+ "revenue-cat",
10
+ "api",
11
+ "client",
12
+ "fetch",
13
+ "rate-limiting"
14
+ ],
15
+ "author": "David Idol",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/idolize/revenuecat-api.git"
20
+ },
21
+ "dependencies": {
22
+ "openapi-fetch": "^0.14.0"
23
+ },
24
+ "devDependencies": {
25
+ "@eslint/js": "^9.30.1",
26
+ "@types/node": "^24.0.13",
27
+ "eslint": "^9.30.1",
28
+ "globals": "^16.3.0",
29
+ "openapi-typescript": "^7.8.0",
30
+ "typescript": "^5.8.3",
31
+ "typescript-eslint": "^8.36.0",
32
+ "vitest": "^3.2.4"
33
+ },
34
+ "scripts": {
35
+ "test": "vitest --no-watch",
36
+ "test:watch": "vitest",
37
+ "types:check": "tsc --noEmit",
38
+ "lint": "eslint ./src/**",
39
+ "lint:fix": "eslint ./src/** --fix",
40
+ "update:openapi": "rm downloaded/openapi.yaml && pnpm generate:openapi",
41
+ "generate:openapi": "scripts/generate_openapi_client.sh"
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ onlyBuiltDependencies:
2
+ - msw
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+
3
+ OPENAPI_FILE="./downloaded/openapi.yaml"
4
+
5
+ if [ ! -f "$OPENAPI_FILE" ]; then
6
+ curl -o "$OPENAPI_FILE" https://www.revenuecat.com/docs/redocusaurus/plugin-redoc-0.yaml
7
+ fi
8
+
9
+ npx openapi-typescript "$OPENAPI_FILE" -o ./src/__generated/revenuecat-api-v2.d.ts