@shuffleio/shuffle-mcps 0.0.15 → 0.0.17

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.
@@ -2,7 +2,7 @@ import {
2
2
  fetchApps,
3
3
  fetchAppsViaApiConfig,
4
4
  invalidateAppsCache
5
- } from "./chunk-FOMFBSUO.mjs";
5
+ } from "./chunk-R3YLER6P.mjs";
6
6
  export {
7
7
  fetchApps,
8
8
  fetchAppsViaApiConfig,
@@ -0,0 +1,325 @@
1
+ // fetchBreaker.ts
2
+ var FAIL_THRESHOLD = 12;
3
+ var ROLLING_WINDOW_MS = 5e3;
4
+ var COOLDOWN_MS = 3e4;
5
+ var HARD_BURST_THRESHOLD = 30;
6
+ var HARD_BURST_WINDOW_MS = 1e3;
7
+ var state = /* @__PURE__ */ new Map();
8
+ var installed = false;
9
+ var allowedOrigins = /* @__PURE__ */ new Set();
10
+ var getEntry = (key) => {
11
+ let entry = state.get(key);
12
+ if (!entry) {
13
+ entry = { failures: [], attempts: [], blockedUntil: 0, warned: false };
14
+ state.set(key, entry);
15
+ }
16
+ return entry;
17
+ };
18
+ var prune = (arr, now, windowMs) => {
19
+ const cutoff = now - windowMs;
20
+ while (arr.length && arr[0] < cutoff) arr.shift();
21
+ };
22
+ var keyFor = (input, init) => {
23
+ let urlStr;
24
+ let method = (init?.method || (typeof input !== "string" && "method" in input ? input.method : "GET") || "GET").toUpperCase();
25
+ if (typeof input === "string") urlStr = input;
26
+ else if (input instanceof URL) urlStr = input.toString();
27
+ else if (input && typeof input.url === "string") urlStr = input.url;
28
+ else return null;
29
+ try {
30
+ const u = new URL(urlStr, typeof window !== "undefined" ? window.location.href : "http://localhost");
31
+ if (allowedOrigins.size && !allowedOrigins.has(u.origin)) return null;
32
+ if (!u.pathname.startsWith("/api/")) return null;
33
+ return `${method} ${u.origin}${u.pathname}`;
34
+ } catch {
35
+ return null;
36
+ }
37
+ };
38
+ var synthetic503 = (key, retryInMs) => new Response(
39
+ JSON.stringify({
40
+ error: "circuit_breaker_open",
41
+ message: `Too many failures for ${key}. Cooling down for ${Math.ceil(retryInMs / 1e3)}s.`
42
+ }),
43
+ {
44
+ status: 503,
45
+ statusText: "Service Unavailable (circuit breaker)",
46
+ headers: { "Content-Type": "application/json", "Retry-After": String(Math.ceil(retryInMs / 1e3)) }
47
+ }
48
+ );
49
+ var registerProtectedOrigin = (url) => {
50
+ if (!url) return;
51
+ try {
52
+ const origin = new URL(url, typeof window !== "undefined" ? window.location.href : "http://localhost").origin;
53
+ allowedOrigins.add(origin);
54
+ } catch {
55
+ }
56
+ };
57
+ var installFetchBreaker = () => {
58
+ if (installed) return;
59
+ if (typeof window === "undefined" || typeof window.fetch !== "function") return;
60
+ installed = true;
61
+ const original = window.fetch.bind(window);
62
+ window.fetch = (async (input, init) => {
63
+ const key = keyFor(input, init);
64
+ if (!key) return original(input, init);
65
+ const entry = getEntry(key);
66
+ const now = Date.now();
67
+ if (entry.blockedUntil > now) {
68
+ return synthetic503(key, entry.blockedUntil - now);
69
+ }
70
+ entry.attempts.push(now);
71
+ prune(entry.attempts, now, HARD_BURST_WINDOW_MS);
72
+ if (entry.attempts.length > HARD_BURST_THRESHOLD) {
73
+ entry.blockedUntil = now + COOLDOWN_MS;
74
+ if (!entry.warned) {
75
+ entry.warned = true;
76
+ console.error(
77
+ `[fetchBreaker] Burst detected on ${key} (${entry.attempts.length} calls / ${HARD_BURST_WINDOW_MS}ms). Blocking for ${COOLDOWN_MS / 1e3}s. Fix the caller \u2014 this is almost always a missing useEffect dep or a render loop.`
78
+ );
79
+ }
80
+ return synthetic503(key, COOLDOWN_MS);
81
+ }
82
+ let response;
83
+ try {
84
+ response = await original(input, init);
85
+ } catch (err) {
86
+ const ts = Date.now();
87
+ entry.failures.push(ts);
88
+ prune(entry.failures, ts, ROLLING_WINDOW_MS);
89
+ if (entry.failures.length >= FAIL_THRESHOLD) {
90
+ entry.blockedUntil = ts + COOLDOWN_MS;
91
+ if (!entry.warned) {
92
+ entry.warned = true;
93
+ console.error(
94
+ `[fetchBreaker] ${entry.failures.length} network errors on ${key} in ${ROLLING_WINDOW_MS / 1e3}s. Blocking for ${COOLDOWN_MS / 1e3}s.`
95
+ );
96
+ }
97
+ }
98
+ throw err;
99
+ }
100
+ if (response.status >= 500 || response.status === 0) {
101
+ const ts = Date.now();
102
+ entry.failures.push(ts);
103
+ prune(entry.failures, ts, ROLLING_WINDOW_MS);
104
+ if (entry.failures.length >= FAIL_THRESHOLD) {
105
+ entry.blockedUntil = ts + COOLDOWN_MS;
106
+ if (!entry.warned) {
107
+ entry.warned = true;
108
+ console.error(
109
+ `[fetchBreaker] ${entry.failures.length} ${response.status}s on ${key} in ${ROLLING_WINDOW_MS / 1e3}s. Blocking for ${COOLDOWN_MS / 1e3}s.`
110
+ );
111
+ }
112
+ }
113
+ } else if (response.status < 400) {
114
+ entry.failures.length = 0;
115
+ entry.warned = false;
116
+ }
117
+ return response;
118
+ });
119
+ };
120
+
121
+ // api.ts
122
+ installFetchBreaker();
123
+ var DEV_BACKEND = "https://tunnel.schemaless.org";
124
+ var PROD_BACKEND = "https://shuffler.io";
125
+ var CLOUD_DOMAINS = ["security.shuffler.io", "shutdown.no"];
126
+ var getEnvVar = (key) => {
127
+ try {
128
+ const meta = new Function("try { return import.meta } catch { return undefined }")();
129
+ return meta?.env?.[key];
130
+ } catch {
131
+ return void 0;
132
+ }
133
+ };
134
+ var isDevEnvironment = () => {
135
+ if (getEnvVar("VITE_SHUFFLE_API_URL")) return false;
136
+ const hostname = typeof window !== "undefined" ? window.location.hostname : "";
137
+ return hostname.includes("lovableproject.com") || hostname.includes("id-preview--");
138
+ };
139
+ var isCloudDomain = () => {
140
+ const hostname = typeof window !== "undefined" ? window.location.hostname : "";
141
+ return CLOUD_DOMAINS.includes(hostname);
142
+ };
143
+ var isCloud = () => isCloudDomain();
144
+ var isOnprem = () => !isDevEnvironment() && !isCloudDomain();
145
+ var getDefaultBaseUrl = () => {
146
+ const envUrl = getEnvVar("VITE_SHUFFLE_API_URL");
147
+ if (envUrl) {
148
+ return envUrl;
149
+ }
150
+ if (isDevEnvironment()) return DEV_BACKEND;
151
+ if (isCloudDomain()) return PROD_BACKEND;
152
+ if (typeof window !== "undefined") {
153
+ return window.location.origin;
154
+ }
155
+ return PROD_BACKEND;
156
+ };
157
+ var _regionUrl = null;
158
+ var _trackedOrgId = null;
159
+ var _hostBaseUrl = null;
160
+ var isShufflerSubdomain = (url) => {
161
+ try {
162
+ const parsed = new URL(url);
163
+ return parsed.hostname.endsWith(".shuffler.io") || parsed.hostname === "shuffler.io";
164
+ } catch {
165
+ return false;
166
+ }
167
+ };
168
+ var setRegionUrl = (regionUrl, orgId) => {
169
+ if (isDevEnvironment()) {
170
+ _trackedOrgId = orgId || null;
171
+ return;
172
+ }
173
+ _trackedOrgId = orgId || null;
174
+ if (regionUrl && isShufflerSubdomain(regionUrl)) {
175
+ const normalized = regionUrl.replace(/\/+$/, "");
176
+ if (normalized !== PROD_BACKEND) {
177
+ _regionUrl = normalized;
178
+ console.log(`[API] Region URL set to: ${_regionUrl}`);
179
+ return;
180
+ }
181
+ }
182
+ _regionUrl = null;
183
+ };
184
+ var resetRegionUrl = () => {
185
+ if (_regionUrl) {
186
+ console.log(`[API] Region URL reset to default (org changed)`);
187
+ }
188
+ _regionUrl = null;
189
+ };
190
+ var setHostBaseUrl = (url) => {
191
+ const next = url ? url.replace(/\/+$/, "") : null;
192
+ if (next === _hostBaseUrl) return;
193
+ _hostBaseUrl = next;
194
+ if (next) {
195
+ try {
196
+ registerProtectedOrigin(next);
197
+ } catch {
198
+ }
199
+ }
200
+ };
201
+ var getHostBaseUrl = () => _hostBaseUrl;
202
+ var getTrackedOrgId = () => _trackedOrgId;
203
+ var API_CONFIG = {
204
+ // Shuffle backend URL — host override beats region URL beats default.
205
+ get baseUrl() {
206
+ const url = _hostBaseUrl || _regionUrl || getDefaultBaseUrl();
207
+ try {
208
+ registerProtectedOrigin(url);
209
+ } catch {
210
+ }
211
+ return url;
212
+ },
213
+ // API version
214
+ version: "v1",
215
+ // Get API key from localStorage (for local development)
216
+ get apiKey() {
217
+ return localStorage.getItem("shuffle_api_key");
218
+ },
219
+ // Set API key in localStorage
220
+ setApiKey(key) {
221
+ if (key) {
222
+ localStorage.setItem("shuffle_api_key", key);
223
+ } else {
224
+ localStorage.removeItem("shuffle_api_key");
225
+ }
226
+ }
227
+ };
228
+ var getApiUrl = (endpoint) => {
229
+ return `${API_CONFIG.baseUrl}${endpoint}`;
230
+ };
231
+ var shuffleFetch = (url, init) => {
232
+ const { headers: extraHeaders, ...rest } = init || {};
233
+ return fetch(url, {
234
+ credentials: "include",
235
+ ...rest,
236
+ headers: {
237
+ ...getAuthHeader(),
238
+ ...extraHeaders
239
+ }
240
+ });
241
+ };
242
+ var getAuthHeader = (overrideOrgId) => {
243
+ const headers = {};
244
+ const apiKey = API_CONFIG.apiKey;
245
+ if (apiKey) {
246
+ headers["Authorization"] = `Bearer ${apiKey}`;
247
+ }
248
+ const orgId = overrideOrgId ?? _trackedOrgId;
249
+ if (orgId) {
250
+ headers["Org-Id"] = orgId;
251
+ }
252
+ return headers;
253
+ };
254
+
255
+ // appsCache.ts
256
+ var TTL_MS = 3e4;
257
+ var ERROR_TTL_MS = 3e4;
258
+ var cache = /* @__PURE__ */ new Map();
259
+ var cacheKey = (o) => `${o.baseUrl}|${o.path || "/api/v1/apps"}|${o.apiKey || ""}|${o.orgId || ""}`;
260
+ var doFetch = async (o) => {
261
+ const headers = {};
262
+ if (o.apiKey) headers["Authorization"] = `Bearer ${o.apiKey}`;
263
+ if (o.orgId) headers["Org-Id"] = o.orgId;
264
+ const response = await fetch(`${o.baseUrl}${o.path || "/api/v1/apps"}`, {
265
+ credentials: "include",
266
+ headers
267
+ });
268
+ if (!response.ok) return [];
269
+ const data = await response.json();
270
+ if (Array.isArray(data)) return data;
271
+ return Array.isArray(data?.data) ? data.data : [];
272
+ };
273
+ var fetchApps = (opts) => {
274
+ const key = cacheKey(opts);
275
+ const entry = cache.get(key) || {};
276
+ if (entry.data && entry.fetchedAt && Date.now() - entry.fetchedAt < TTL_MS) {
277
+ return Promise.resolve(entry.data);
278
+ }
279
+ if (entry.promise) return entry.promise;
280
+ const promise = doFetch(opts).then((data) => {
281
+ cache.set(key, { data, fetchedAt: Date.now() });
282
+ return data;
283
+ }).catch(() => {
284
+ cache.set(key, { data: [], fetchedAt: Date.now() - (TTL_MS - ERROR_TTL_MS) });
285
+ return [];
286
+ });
287
+ cache.set(key, { ...entry, promise });
288
+ return promise;
289
+ };
290
+ var invalidateAppsCache = (opts) => {
291
+ if (!opts) {
292
+ cache.clear();
293
+ return;
294
+ }
295
+ cache.delete(cacheKey(opts));
296
+ };
297
+ var fetchAppsViaApiConfig = () => {
298
+ const headers = getAuthHeader();
299
+ const orgId = headers["Org-Id"] || null;
300
+ return fetchApps({
301
+ baseUrl: API_CONFIG.baseUrl,
302
+ apiKey: API_CONFIG.apiKey,
303
+ orgId
304
+ });
305
+ };
306
+
307
+ export {
308
+ registerProtectedOrigin,
309
+ installFetchBreaker,
310
+ isCloudDomain,
311
+ isCloud,
312
+ isOnprem,
313
+ setRegionUrl,
314
+ resetRegionUrl,
315
+ setHostBaseUrl,
316
+ getHostBaseUrl,
317
+ getTrackedOrgId,
318
+ API_CONFIG,
319
+ getApiUrl,
320
+ shuffleFetch,
321
+ getAuthHeader,
322
+ fetchApps,
323
+ invalidateAppsCache,
324
+ fetchAppsViaApiConfig
325
+ };