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