🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@kolbo/app-sdk

Package Overview
Dependencies
Maintainers
1
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@kolbo/app-sdk - npm Package Compare versions

Comparing version
2.3.1
to
2.4.0
+1
-1
package.json
{
"name": "@kolbo/app-sdk",
"version": "2.3.1",
"version": "2.4.0",
"description": "Kolbo App Builder SDK — auth, data, storage and AI for generated apps",

@@ -5,0 +5,0 @@ "type": "module",

@@ -502,36 +502,39 @@ // Kolbo-native adapter.

_buildAi() {
const env = (typeof import.meta !== 'undefined' && import.meta.env) ? import.meta.env : {};
const apiKey = env.VITE_KOLBO_API_KEY || '';
// AI endpoints live at `{apiUrl}/api/v1/*`. Default to the adapter's apiUrl
// (which comes from VITE_KOLBO_API_URL — the same host where auth / data /
// storage talk). Falling back to api.kolbo.ai hard-codes production and
// breaks dev/staging previews whose API key was issued in a different DB.
const fallbackBase = `${String(this._apiUrl || '').replace(/\/$/, '')}/api` || 'https://api.kolbo.ai/api';
const apiBase = (env.VITE_KOLBO_BASE || fallbackBase).replace(/\/$/, '');
const h = () => ({ 'X-API-Key': apiKey, 'Content-Type': 'application/json' });
const self = this;
// AI now proxies through the App Builder AI proxy at
// `{apiUrl}/api/apps/{appId}/ai/*`, authenticated with the SAME per-app
// end-user JWT that auth/data/storage already hold (via _getAuthHeader).
// No full-access API key is ever shipped in the app bundle — the proxy
// resolves the app OWNER server-side and bills the owner's credits.
const aiBase = `${String(this._apiUrl || '').replace(/\/$/, '')}/api/apps/${this._appId}/ai`;
// Pre-flight guard. If the sandbox booted without the API key env var
// (race between .env write and Vite startup — most common when the
// session was created before ensureKolboApiKey was wired), every
// AI call would 401 with an opaque "401" string. Surface a specific
// error the user can actually act on.
const assertKey = () => {
if (!apiKey || apiKey.length < 10) {
throw new Error(
'Kolbo AI key missing — this preview was built before the API key was injected. ' +
'Click "Reload preview" in the top bar (or regenerate) to rebuild with the key.'
);
// Map a proxy HTTP error to an actionable message.
async function throwForStatus(r, label) {
if (r.status === 401) {
throw new Error('Kolbo AI requires the app user to be signed in (401). Ask the user to log in and retry.');
}
};
// Translate 401 into the same actionable message, in case the key was
// revoked / rotated between sandbox boot and this call.
const assertAuthed = (res) => {
if (res.status === 401) {
throw new Error(
'Kolbo AI rejected this preview\'s API key (401). The key may have been revoked ' +
'or the sandbox env is stale. Reload the preview or regenerate.'
);
if (r.status === 402) {
throw new Error('Out of Kolbo AI credits — the app owner needs to top up at kolbo.ai/pricing.');
}
};
if (r.status === 429) {
throw new Error('Too many AI requests right now — please wait a moment and try again.');
}
let msg;
try { msg = (await r.json())?.error; } catch (_) { /* non-JSON body */ }
throw new Error(msg || `${label} failed: ${r.status}`);
}
// POST helper — attaches the app-user JWT, parses JSON, throws on !ok.
async function post(path, body, signal) {
const authHeader = await self._getAuthHeader();
const r = await fetch(`${aiBase}/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'ngrok-skip-browser-warning': 'true', ...authHeader },
body: JSON.stringify(body),
signal,
});
if (!r.ok) await throwForStatus(r, path);
return r.json();
}
async function poll(pollUrl, optsOrTimeout) {

@@ -545,6 +548,11 @@ // Back-compat: existing callers pass a raw number for timeoutMs.

const signal = opts.signal;
// The proxy returns a relative pollUrl (`/api/apps/.../status/:id`);
// resolve it against the adapter's apiUrl. Absolute URLs pass through.
const base = String(self._apiUrl || '').replace(/\/$/, '');
const url = /^https?:/i.test(pollUrl) ? pollUrl : `${base}${pollUrl}`;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (signal && signal.aborted) throw new Error('Generation cancelled');
const r = await fetch(pollUrl, { headers: { 'X-API-Key': apiKey }, signal });
const authHeader = await self._getAuthHeader();
const r = await fetch(url, { headers: { 'ngrok-skip-browser-warning': 'true', ...authHeader }, signal });
if (!r.ok) throw new Error(`Poll ${r.status}`);

@@ -567,18 +575,10 @@ const b = await r.json();

// Every method now forwards `model` and any other params the caller
// supplies. Before this, `model` was stripped and the backend's
// Smart Select routed every call to a generic default — fine for
// "just make an image" but wrong for specialized cases (storybook
// character consistency, talking-head lipsync, cloned voice TTS).
// The coder agent picks `model` explicitly via the AI Integrator plan.
// Public method names, signatures and return shapes are UNCHANGED so
// generated app code keeps working — only the transport (URL + auth)
// changed under the hood. Each method forwards `model` and any extra
// params so the coder agent can pick a specific model.
return {
async chat({ message, messages, session_id, model, signal, ...rest } = {}) {
assertKey();
const text = message ?? (messages ? messages[messages.length - 1]?.content : '') ?? '';
const body = { message: text, session_id, ...(model ? { model } : {}), ...rest };
const r = await fetch(`${apiBase}/v1/chat`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(r);
if (r.status === 402) throw new Error('Out of Kolbo AI credits — top up at kolbo.ai/pricing');
if (!r.ok) throw new Error(`Chat failed: ${r.status}`);
const d = await r.json();
const d = await post('chat', { message: text, session_id, ...(model ? { model } : {}), ...rest }, signal);
return { content: d.reply, session_id: d.session_id };

@@ -588,8 +588,3 @@ },

const body = { prompt, aspect_ratio, ...(model ? { model } : {}), ...(visual_dna_id ? { visual_dna_id } : {}), ...(reference_image ? { reference_image } : {}), ...rest };
assertKey();
const init = await fetch(`${apiBase}/v1/generate/image`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (init.status === 402) throw new Error('Out of Kolbo AI credits');
if (!init.ok) throw new Error(`Image init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/image', body, signal);
const result = await poll(pollUrl, { signal });

@@ -599,8 +594,4 @@ return { url: result.images?.[0]?.url, images: result.images };

async generateImageEdit({ prompt, image, mask, model, signal, ...rest } = {}) {
assertKey();
const body = { prompt, image, ...(mask ? { mask } : {}), ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/image-edit`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Image edit failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/image-edit', body, signal);
const result = await poll(pollUrl, { signal });

@@ -610,8 +601,4 @@ return { url: result.images?.[0]?.url, images: result.images };

async generateVideo({ prompt, duration = 5, aspect_ratio = '16:9', model, signal, ...rest } = {}) {
assertKey();
const body = { prompt, duration, aspect_ratio, ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/video`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Video init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/video', body, signal);
const result = await poll(pollUrl, { timeoutMs: 10 * 60_000, signal });

@@ -621,8 +608,4 @@ return { url: result.video?.url };

async generateVideoFromImage({ prompt, image, duration = 5, model, signal, ...rest } = {}) {
assertKey();
const body = { prompt, image, duration, ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/video/from-image`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Video-from-image init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/video/from-image', body, signal);
const result = await poll(pollUrl, { timeoutMs: 10 * 60_000, signal });

@@ -632,8 +615,4 @@ return { url: result.video?.url };

async generateLipsync({ image, video, audio, model, signal, ...rest } = {}) {
assertKey();
const body = { audio, ...(image ? { image } : {}), ...(video ? { video } : {}), ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/lipsync`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Lipsync init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/lipsync', body, signal);
const result = await poll(pollUrl, { timeoutMs: 10 * 60_000, signal });

@@ -643,8 +622,4 @@ return { url: result.video?.url };

async generateMusic({ prompt, duration, model, signal, ...rest } = {}) {
assertKey();
const body = { prompt, ...(duration ? { duration } : {}), ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/music`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Music init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/music', body, signal);
const result = await poll(pollUrl, { signal });

@@ -654,8 +629,4 @@ return { url: result.audio_url };

async generateSpeech({ text, voice, model, signal, ...rest } = {}) {
assertKey();
const body = { text, ...(voice ? { voice } : {}), ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/speech`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Speech init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/speech', body, signal);
const result = await poll(pollUrl, { signal });

@@ -665,8 +636,4 @@ return { url: result.audio_url };

async generateSound({ prompt, duration, model, signal, ...rest } = {}) {
assertKey();
const body = { prompt, ...(duration ? { duration } : {}), ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/generate/sound`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Sound init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('generate/sound', body, signal);
const result = await poll(pollUrl, { signal });

@@ -676,8 +643,4 @@ return { url: result.audio_url };

async transcribe({ audio, model, signal, ...rest } = {}) {
assertKey();
const body = { audio, ...(model ? { model } : {}), ...rest };
const init = await fetch(`${apiBase}/v1/transcribe`, { method: 'POST', headers: h(), body: JSON.stringify(body), signal });
assertAuthed(init);
if (!init.ok) throw new Error(`Transcribe init failed: ${init.status}`);
const { pollUrl } = await init.json();
const { pollUrl } = await post('transcribe', body, signal);
const result = await poll(pollUrl, { signal });

@@ -769,4 +732,11 @@ return { text: result.text, srt: result.srt, segments: result.segments };

}
// Default the popup's return target to the CURRENT page, not a bare
// `/auth/callback`. The SDK consumes hash tokens on ANY page load
// (see init hash handler), so returning to the exact page that opened
// the popup is correct for every surface — and critically, on a dev
// preview snapshot served at /snap/<id>/index.html, `/auth/callback`
// resolves to the BUILDER/host root where this SDK never runs, so the
// popup can't hand the tokens back and sign-in stalls.
const redirectTo = options?.redirectTo
|| (window.location.origin + '/auth/callback');
|| (window.location.origin + window.location.pathname);

@@ -773,0 +743,0 @@ // Always use popup — both for iframed sandbox previews and top-level

@@ -35,3 +35,13 @@ import { KolboAdapter } from './adapters/kolbo.js';

const apiUrl = config.apiUrl ?? env.VITE_KOLBO_API_URL ?? '';
if (!apiUrl) console.warn('[kolbo-sdk] VITE_KOLBO_API_URL is not set — backend unreachable');
if (!apiUrl) {
// Fail loud, not silent. With an empty apiUrl, every adapter call becomes
// a relative-path fetch resolved against the iframe's host (e.g.
// http://localhost:8080/api/apps/<id>/storage → 404 → blank screen).
// Throwing here surfaces the misconfiguration in the error overlay instead.
throw new Error(
'kolbo-sdk: VITE_KOLBO_API_URL is required but was not set when this app was built. ' +
'The app cannot reach the backend. Rebuild after setting APP_BUILDER_PUBLIC_API_URL ' +
'(in kolbo-api .env) to a publicly-reachable HTTPS URL pointing at the API.'
);
}

@@ -38,0 +48,0 @@ const sandboxAccessToken = env.VITE_KOLBO_SANDBOX_ACCESS_TOKEN;