opencloud-platform-sdk 1.3.0 → 2.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/dist/index.mjs CHANGED
@@ -1,64 +1,30 @@
1
1
  // src/index.ts
2
2
  var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
3
- var STORAGE_KEYS = {
4
- OPENROUTER_KEY: "openrouter_api_key",
5
- ANTHROPIC_KEY: "anthropic_api_key",
6
- OPENAI_KEY: "openai_api_key",
7
- PREVIEW_PROVIDER: "opencloud_preview_provider"
8
- };
9
- function getCookie(name) {
10
- if (!isBrowser) return null;
11
- const value = `; ${document.cookie}`;
12
- const parts = value.split(`; ${name}=`);
13
- if (parts.length === 2) {
14
- const cookieValue = parts.pop()?.split(";").shift();
15
- if (cookieValue) {
16
- try {
17
- return decodeURIComponent(cookieValue);
18
- } catch {
19
- return cookieValue;
20
- }
21
- }
22
- }
23
- return null;
24
- }
25
- function getBoltApiKeys() {
26
- const apiKeysCookie = getCookie("apiKeys");
27
- if (!apiKeysCookie) return null;
28
- try {
29
- return JSON.parse(apiKeysCookie);
30
- } catch {
31
- return null;
32
- }
33
- }
34
3
  var OpenCloudSDK = class {
35
4
  constructor() {
36
5
  this._isPreviewMode = false;
6
+ this._cachedSession = null;
37
7
  this.config = {
38
8
  appId: "",
39
- apiUrl: isBrowser ? window.location.origin : "http://localhost:3000",
40
- previewApiKey: void 0
9
+ apiUrl: isBrowser ? window.location.origin : "https://opencloud.app"
41
10
  };
42
11
  if (isBrowser) {
43
12
  this._isPreviewMode = this.detectPreviewMode();
44
13
  }
45
14
  }
46
- /**
47
- * Detect if running in preview/development mode
48
- */
49
15
  detectPreviewMode() {
50
16
  if (!isBrowser) return false;
51
17
  const hostname = window.location.hostname;
52
18
  return hostname.includes("webcontainer") || hostname.includes("localhost") || hostname.includes("127.0.0.1") || hostname.includes(".local") || hostname.includes("stackblitz") || hostname.includes("codesandbox");
53
19
  }
54
20
  /**
55
- * Check if currently in preview mode
21
+ * Check if running in preview/development mode
56
22
  */
57
23
  isPreview() {
58
24
  return this._isPreviewMode;
59
25
  }
60
26
  /**
61
- * Initialize the SDK
27
+ * Initialize the SDK with your app ID
62
28
  */
63
29
  init(options) {
64
30
  if (!options.appId) {
@@ -66,8 +32,7 @@ var OpenCloudSDK = class {
66
32
  }
67
33
  this.config = {
68
34
  appId: options.appId,
69
- apiUrl: options.apiUrl || this.config.apiUrl,
70
- previewApiKey: options.previewApiKey
35
+ apiUrl: options.apiUrl || this.config.apiUrl
71
36
  };
72
37
  if (!this._isPreviewMode) {
73
38
  this.startHeartbeat();
@@ -76,235 +41,105 @@ var OpenCloudSDK = class {
76
41
  console.log(`[OpenCloudSDK] Initialized for app: ${options.appId} (${mode} mode)`);
77
42
  }
78
43
  /**
79
- * Set API key for preview mode
80
- * Safe to call during build - will be a no-op if not in browser
44
+ * Track a usage event and charge the user
45
+ * Call this BEFORE or AFTER your AI/API call
46
+ *
47
+ * Example:
48
+ * ```
49
+ * // Check if user can afford it first
50
+ * const canUse = await opencloud.canAfford();
51
+ * if (!canUse) {
52
+ * opencloud.requestTopUp();
53
+ * return;
54
+ * }
55
+ *
56
+ * // Do your AI call
57
+ * const result = await myAICall();
58
+ *
59
+ * // Track the usage (charges user)
60
+ * await opencloud.trackUsage({ action: 'chat_message' });
61
+ * ```
81
62
  */
82
- setPreviewApiKey(key, provider = "openrouter") {
83
- if (!isBrowser) return;
84
- const storageKey = provider === "openrouter" ? STORAGE_KEYS.OPENROUTER_KEY : provider === "anthropic" ? STORAGE_KEYS.ANTHROPIC_KEY : STORAGE_KEYS.OPENAI_KEY;
85
- localStorage.setItem(storageKey, key);
86
- localStorage.setItem(STORAGE_KEYS.PREVIEW_PROVIDER, provider);
87
- this.config.previewApiKey = key;
88
- }
89
- /**
90
- * Get API key for preview mode
91
- * Checks in order: config, localStorage, bolt.diy cookies
92
- */
93
- getPreviewApiKey() {
94
- if (!isBrowser) return null;
95
- if (this.config.previewApiKey) {
96
- return { key: this.config.previewApiKey, provider: "openrouter" };
97
- }
98
- const provider = localStorage.getItem(STORAGE_KEYS.PREVIEW_PROVIDER) || "openrouter";
99
- const storageKey = provider === "openrouter" ? STORAGE_KEYS.OPENROUTER_KEY : provider === "anthropic" ? STORAGE_KEYS.ANTHROPIC_KEY : STORAGE_KEYS.OPENAI_KEY;
100
- const key = localStorage.getItem(storageKey);
101
- if (key) {
102
- return { key, provider };
103
- }
104
- for (const [name, storageKey2] of Object.entries(STORAGE_KEYS)) {
105
- if (name === "PREVIEW_PROVIDER") continue;
106
- const key2 = localStorage.getItem(storageKey2);
107
- if (key2) {
108
- return { key: key2, provider: name.replace("_KEY", "").toLowerCase() };
109
- }
110
- }
111
- const boltApiKeys = getBoltApiKeys();
112
- if (boltApiKeys) {
113
- const providerMappings = [
114
- { boltName: "OpenRouter", provider: "openrouter" },
115
- { boltName: "Anthropic", provider: "anthropic" },
116
- { boltName: "OpenAI", provider: "openai" },
117
- // Also check with _API_KEY suffix (older format)
118
- { boltName: "OpenRouter_API_KEY", provider: "openrouter" },
119
- { boltName: "Anthropic_API_KEY", provider: "anthropic" },
120
- { boltName: "OpenAI_API_KEY", provider: "openai" }
121
- ];
122
- for (const { boltName, provider: provider2 } of providerMappings) {
123
- const apiKey = boltApiKeys[boltName];
124
- if (apiKey && apiKey.length > 10 && !apiKey.includes("your_")) {
125
- console.log(`[OpenCloudSDK] Using API key from bolt.diy (${provider2})`);
126
- return { key: apiKey, provider: provider2 };
127
- }
128
- }
129
- }
130
- return null;
131
- }
132
- /**
133
- * Call an AI model - automatically uses preview or production mode
134
- */
135
- async callAI(params) {
63
+ async trackUsage(params = {}) {
136
64
  if (!this.config.appId) {
137
- throw new Error("OpenCloudSDK: Must call init() before using callAI()");
138
- }
139
- const { model, prompt, options = {} } = params;
140
- if (!model || !prompt) {
141
- throw new Error("OpenCloudSDK: model and prompt are required");
65
+ throw new Error("OpenCloudSDK: Must call init() before tracking usage");
142
66
  }
143
67
  if (this._isPreviewMode) {
144
- return this.callAIPreview(params);
145
- } else {
146
- return this.callAIProduction(params);
147
- }
148
- }
149
- /**
150
- * Call AI in preview mode (direct to OpenRouter/provider)
151
- */
152
- async callAIPreview(params) {
153
- const { model, prompt, options = {} } = params;
154
- const apiKeyInfo = this.getPreviewApiKey();
155
- if (!apiKeyInfo) {
156
- throw new Error(
157
- '[OpenCloudSDK Preview] No API key found. Please set your API key using opencloud.setPreviewApiKey("your-key") or configure it in the builder settings.'
158
- );
68
+ console.log("[OpenCloudSDK Preview] Usage tracked (no charge in preview mode)");
69
+ return {
70
+ success: true,
71
+ charged: 0,
72
+ userBalance: 999.99,
73
+ transactionId: "preview-" + Date.now()
74
+ };
159
75
  }
160
- const { key, provider } = apiKeyInfo;
161
76
  try {
162
- let response;
163
- let responseData;
164
- if (provider === "openrouter" || provider === "openrouter_key") {
165
- response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
166
- method: "POST",
167
- headers: {
168
- "Content-Type": "application/json",
169
- "Authorization": `Bearer ${key}`,
170
- "HTTP-Referer": window.location.origin,
171
- "X-Title": this.config.appId
172
- },
173
- body: JSON.stringify({
174
- model,
175
- messages: [{ role: "user", content: prompt }],
176
- ...options
177
- })
178
- });
179
- responseData = await response.json();
180
- if (!response.ok) {
181
- throw new Error(responseData.error?.message || `OpenRouter API error: ${response.status}`);
182
- }
183
- return {
184
- success: true,
185
- text: responseData.choices?.[0]?.message?.content || "",
186
- usage: {
187
- promptTokens: responseData.usage?.prompt_tokens || 0,
188
- completionTokens: responseData.usage?.completion_tokens || 0,
189
- totalTokens: responseData.usage?.total_tokens || 0
190
- },
191
- cost: 0,
192
- // Preview mode - no cost tracking
193
- model,
194
- mode: "preview"
195
- };
196
- } else if (provider === "anthropic" || provider === "anthropic_key") {
197
- response = await fetch("https://api.anthropic.com/v1/messages", {
198
- method: "POST",
199
- headers: {
200
- "Content-Type": "application/json",
201
- "x-api-key": key,
202
- "anthropic-version": "2023-06-01",
203
- "anthropic-dangerous-direct-browser-access": "true"
204
- },
205
- body: JSON.stringify({
206
- model: model.replace("anthropic/", ""),
207
- max_tokens: options.max_tokens || 1024,
208
- messages: [{ role: "user", content: prompt }]
209
- })
210
- });
211
- responseData = await response.json();
212
- if (!response.ok) {
213
- throw new Error(responseData.error?.message || `Anthropic API error: ${response.status}`);
214
- }
215
- return {
216
- success: true,
217
- text: responseData.content?.[0]?.text || "",
218
- usage: {
219
- promptTokens: responseData.usage?.input_tokens || 0,
220
- completionTokens: responseData.usage?.output_tokens || 0,
221
- totalTokens: (responseData.usage?.input_tokens || 0) + (responseData.usage?.output_tokens || 0)
222
- },
223
- cost: 0,
224
- model,
225
- mode: "preview"
226
- };
227
- } else if (provider === "openai" || provider === "openai_key") {
228
- response = await fetch("https://api.openai.com/v1/chat/completions", {
229
- method: "POST",
230
- headers: {
231
- "Content-Type": "application/json",
232
- "Authorization": `Bearer ${key}`
233
- },
234
- body: JSON.stringify({
235
- model: model.replace("openai/", ""),
236
- messages: [{ role: "user", content: prompt }],
237
- ...options
238
- })
239
- });
240
- responseData = await response.json();
241
- if (!response.ok) {
242
- throw new Error(responseData.error?.message || `OpenAI API error: ${response.status}`);
77
+ const session = await this.getUserSession();
78
+ const response = await fetch(`${this.config.apiUrl}/api/sdk/track-usage`, {
79
+ method: "POST",
80
+ headers: {
81
+ "Content-Type": "application/json",
82
+ "Authorization": `Bearer ${session.token}`
83
+ },
84
+ body: JSON.stringify({
85
+ appId: this.config.appId,
86
+ action: params.action,
87
+ metadata: params.metadata
88
+ })
89
+ });
90
+ if (!response.ok) {
91
+ const error = await response.json();
92
+ if (response.status === 402) {
93
+ throw new Error("INSUFFICIENT_BALANCE");
243
94
  }
244
- return {
245
- success: true,
246
- text: responseData.choices?.[0]?.message?.content || "",
247
- usage: {
248
- promptTokens: responseData.usage?.prompt_tokens || 0,
249
- completionTokens: responseData.usage?.completion_tokens || 0,
250
- totalTokens: responseData.usage?.total_tokens || 0
251
- },
252
- cost: 0,
253
- model,
254
- mode: "preview"
255
- };
95
+ throw new Error(error.error || `Failed to track usage: ${response.status}`);
256
96
  }
257
- throw new Error(`Unsupported provider: ${provider}`);
97
+ return await response.json();
258
98
  } catch (error) {
259
- console.error("[OpenCloudSDK Preview] AI call failed:", error);
99
+ console.error("[OpenCloudSDK] Track usage failed:", error);
260
100
  throw error;
261
101
  }
262
102
  }
263
103
  /**
264
- * Call AI in production mode (through OpenCloud API)
104
+ * Check if the user can afford to use the app
105
+ * Returns true if user has enough balance
265
106
  */
266
- async callAIProduction(params) {
267
- const { model, prompt, options = {} } = params;
107
+ async canAfford() {
108
+ if (this._isPreviewMode) {
109
+ return true;
110
+ }
268
111
  try {
269
112
  const session = await this.getUserSession();
270
- const response = await fetch(`${this.config.apiUrl}/api/ai/call`, {
113
+ const response = await fetch(`${this.config.apiUrl}/api/sdk/can-afford`, {
271
114
  method: "POST",
272
115
  headers: {
273
116
  "Content-Type": "application/json",
274
117
  "Authorization": `Bearer ${session.token}`
275
118
  },
276
119
  body: JSON.stringify({
277
- appId: this.config.appId,
278
- model,
279
- prompt,
280
- options
120
+ appId: this.config.appId
281
121
  })
282
122
  });
283
123
  if (!response.ok) {
284
- const error = await response.json();
285
- throw new Error(error.error || `API call failed: ${response.status}`);
124
+ return false;
286
125
  }
287
126
  const data = await response.json();
288
- return {
289
- ...data,
290
- mode: "production"
291
- };
127
+ return data.canAfford;
292
128
  } catch (error) {
293
- console.error("[OpenCloudSDK Production] AI call failed:", error);
294
- throw error;
129
+ console.error("[OpenCloudSDK] canAfford check failed:", error);
130
+ return false;
295
131
  }
296
132
  }
297
133
  /**
298
- * Get current user's balance (production only)
134
+ * Get current user's balance
299
135
  */
300
136
  async getBalance() {
301
137
  if (this._isPreviewMode) {
302
- console.warn("[OpenCloudSDK] getBalance() not available in preview mode");
303
- return 0;
138
+ return 999.99;
304
139
  }
305
140
  try {
306
141
  const session = await this.getUserSession();
307
- const response = await fetch(`${this.config.apiUrl}/api/user/balance`, {
142
+ const response = await fetch(`${this.config.apiUrl}/api/sdk/balance`, {
308
143
  headers: {
309
144
  "Authorization": `Bearer ${session.token}`
310
145
  }
@@ -320,9 +155,54 @@ var OpenCloudSDK = class {
320
155
  }
321
156
  }
322
157
  /**
323
- * Request user to purchase more tokens
158
+ * Get current user info
159
+ */
160
+ async getUserInfo() {
161
+ if (this._isPreviewMode) {
162
+ return {
163
+ id: "preview-user",
164
+ email: "preview@example.com",
165
+ balance: 999.99,
166
+ username: "preview_user"
167
+ };
168
+ }
169
+ try {
170
+ const session = await this.getUserSession();
171
+ const response = await fetch(`${this.config.apiUrl}/api/sdk/user`, {
172
+ headers: {
173
+ "Authorization": `Bearer ${session.token}`
174
+ }
175
+ });
176
+ if (!response.ok) {
177
+ throw new Error("Failed to get user info");
178
+ }
179
+ return await response.json();
180
+ } catch (error) {
181
+ console.error("[OpenCloudSDK] Get user info failed:", error);
182
+ throw error;
183
+ }
184
+ }
185
+ /**
186
+ * Get the price per use for this app
187
+ */
188
+ async getAppPrice() {
189
+ try {
190
+ const response = await fetch(`${this.config.apiUrl}/api/sdk/app-price?appId=${this.config.appId}`);
191
+ if (!response.ok) {
192
+ throw new Error("Failed to get app price");
193
+ }
194
+ const data = await response.json();
195
+ return data.pricePerUse;
196
+ } catch (error) {
197
+ console.error("[OpenCloudSDK] Get app price failed:", error);
198
+ throw error;
199
+ }
200
+ }
201
+ /**
202
+ * Request user to add more credits
203
+ * Opens the top-up modal or redirects to payment page
324
204
  */
325
- async requestTopUp() {
205
+ requestTopUp() {
326
206
  if (!isBrowser) return;
327
207
  if (this._isPreviewMode) {
328
208
  console.warn("[OpenCloudSDK] Top-up not available in preview mode");
@@ -334,45 +214,72 @@ var OpenCloudSDK = class {
334
214
  appId: this.config.appId
335
215
  }, "*");
336
216
  } else {
337
- window.location.href = `${this.config.apiUrl}/topup`;
217
+ window.location.href = `${this.config.apiUrl}/settings/credits`;
338
218
  }
339
219
  }
340
220
  /**
341
- * Start sending heartbeats
221
+ * Check if user is authenticated
342
222
  */
223
+ async isAuthenticated() {
224
+ if (this._isPreviewMode) {
225
+ return true;
226
+ }
227
+ try {
228
+ await this.getUserSession();
229
+ return true;
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+ /**
235
+ * Request user to log in
236
+ */
237
+ requestLogin() {
238
+ if (!isBrowser) return;
239
+ if (this._isPreviewMode) {
240
+ console.warn("[OpenCloudSDK] Login not required in preview mode");
241
+ return;
242
+ }
243
+ if (window.parent !== window) {
244
+ window.parent.postMessage({
245
+ type: "OPENCLOUD_REQUEST_LOGIN",
246
+ appId: this.config.appId
247
+ }, "*");
248
+ } else {
249
+ window.location.href = `${this.config.apiUrl}/auth/login?redirect=${encodeURIComponent(window.location.href)}`;
250
+ }
251
+ }
343
252
  startHeartbeat() {
344
253
  if (!isBrowser) return;
345
254
  this.sendHeartbeat();
346
255
  this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 3e4);
347
256
  }
348
- /**
349
- * Send heartbeat to platform
350
- */
351
257
  sendHeartbeat() {
352
258
  if (!this.config.appId || !isBrowser) return;
353
259
  const data = JSON.stringify({
354
260
  appId: this.config.appId,
355
261
  timestamp: Date.now(),
356
- version: "1.1.0",
262
+ version: "2.0.0",
357
263
  url: window.location.href
358
264
  });
359
265
  if (navigator.sendBeacon) {
360
266
  navigator.sendBeacon(`${this.config.apiUrl}/api/sdk/heartbeat`, data);
361
267
  }
362
268
  }
363
- /**
364
- * Get user session token
365
- */
366
269
  async getUserSession() {
367
270
  if (!isBrowser) {
368
271
  throw new Error("getUserSession can only be called in browser");
369
272
  }
273
+ if (this._cachedSession) {
274
+ return this._cachedSession;
275
+ }
370
276
  return new Promise((resolve, reject) => {
371
277
  if (window.parent !== window) {
372
278
  const messageHandler = (event) => {
373
279
  if (event.data.type === "USER_SESSION_RESPONSE") {
374
280
  window.removeEventListener("message", messageHandler);
375
281
  if (event.data.session) {
282
+ this._cachedSession = event.data.session;
376
283
  resolve(event.data.session);
377
284
  } else {
378
285
  reject(new Error("User not authenticated"));
@@ -391,6 +298,7 @@ var OpenCloudSDK = class {
391
298
  } else {
392
299
  const token = localStorage.getItem("opencloud_session_token");
393
300
  if (token) {
301
+ this._cachedSession = { token };
394
302
  resolve({ token });
395
303
  } else {
396
304
  reject(new Error("User not authenticated. Please log in."));
@@ -399,18 +307,51 @@ var OpenCloudSDK = class {
399
307
  });
400
308
  }
401
309
  /**
402
- * Cleanup
310
+ * Clear cached session (call on logout)
311
+ */
312
+ clearSession() {
313
+ this._cachedSession = null;
314
+ }
315
+ /**
316
+ * Cleanup resources
403
317
  */
404
318
  destroy() {
405
319
  if (this.heartbeatInterval) {
406
320
  clearInterval(this.heartbeatInterval);
407
321
  }
322
+ this._cachedSession = null;
323
+ }
324
+ /**
325
+ * Execute a function and automatically charge the user
326
+ * This is a convenience method that combines canAfford + your code + trackUsage
327
+ *
328
+ * @param fn - The async function to execute (your AI call, etc.)
329
+ * @param options - Optional tracking options
330
+ * @returns The result of your function
331
+ *
332
+ * Example:
333
+ * ```typescript
334
+ * const result = await opencloud.withUsage(async () => {
335
+ * const response = await fetch('https://api.openai.com/...', { ... });
336
+ * return await response.json();
337
+ * }, { action: 'chat_message' });
338
+ * ```
339
+ */
340
+ async withUsage(fn, options = {}) {
341
+ const canUse = await this.canAfford();
342
+ if (!canUse) {
343
+ this.requestTopUp();
344
+ throw new Error("INSUFFICIENT_BALANCE");
345
+ }
346
+ const result = await fn();
347
+ await this.trackUsage(options);
348
+ return result;
408
349
  }
409
350
  };
410
351
  var opencloud = new OpenCloudSDK();
411
352
  if (isBrowser) {
412
353
  window.OpenCloudSDK = opencloud;
413
- window.PlatformSDK = opencloud;
354
+ window.opencloud = opencloud;
414
355
  }
415
356
  export {
416
357
  OpenCloudSDK,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencloud-platform-sdk",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Official SDK for OpenCloud - AI App Marketplace",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",