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.
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/publish.yml +34 -0
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/__generated/revenuecat-api-v2.d.ts +5110 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/rateLimitMiddleware.d.ts +3 -0
- package/dist/rateLimitMiddleware.d.ts.map +1 -0
- package/dist/rateLimitMiddleware.js +160 -0
- package/eslint.config.mjs +11 -0
- package/package.json +43 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/generate_openapi_client.sh +9 -0
- package/test/rateLimitMiddleware.test.ts +609 -0
- package/tsconfig.json +114 -0
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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,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
|