@omnikit-ai/sdk 2.0.3
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/auth-utils.d.mts +68 -0
- package/dist/auth-utils.d.ts +68 -0
- package/dist/auth-utils.js +124 -0
- package/dist/auth-utils.js.map +1 -0
- package/dist/auth-utils.mjs +115 -0
- package/dist/auth-utils.mjs.map +1 -0
- package/dist/index.d.mts +1471 -0
- package/dist/index.d.ts +1471 -0
- package/dist/index.js +2096 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2085 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2085 @@
|
|
|
1
|
+
// src/auth-utils.ts
|
|
2
|
+
var ACCESS_TOKEN_KEY = "access_token";
|
|
3
|
+
var TOKEN_URL_PARAM = "token";
|
|
4
|
+
function setAccessTokenKey(appId) {
|
|
5
|
+
ACCESS_TOKEN_KEY = `omnikit_token_${appId}`;
|
|
6
|
+
}
|
|
7
|
+
function isBrowser() {
|
|
8
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
9
|
+
}
|
|
10
|
+
function getTokenFromUrl() {
|
|
11
|
+
if (!isBrowser()) return null;
|
|
12
|
+
try {
|
|
13
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
14
|
+
return urlParams.get(TOKEN_URL_PARAM);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.warn("Failed to get token from URL:", error);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function getTokenFromStorage() {
|
|
21
|
+
if (!isBrowser()) return null;
|
|
22
|
+
try {
|
|
23
|
+
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn("Failed to get token from localStorage:", error);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isTokenInUrl() {
|
|
30
|
+
return getTokenFromUrl() !== null;
|
|
31
|
+
}
|
|
32
|
+
function cleanTokenFromUrl() {
|
|
33
|
+
if (!isBrowser()) return;
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(window.location.href);
|
|
36
|
+
if (url.searchParams.has(TOKEN_URL_PARAM)) {
|
|
37
|
+
url.searchParams.delete(TOKEN_URL_PARAM);
|
|
38
|
+
window.history.replaceState({}, "", url.toString());
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn("Failed to clean token from URL:", error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getTokenFromHash() {
|
|
45
|
+
if (!isBrowser()) return null;
|
|
46
|
+
const hash = window.location.hash;
|
|
47
|
+
if (!hash) return null;
|
|
48
|
+
try {
|
|
49
|
+
const params = new URLSearchParams(hash.substring(1));
|
|
50
|
+
return params.get("_auth_token");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn("Failed to get token from hash:", error);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function cleanTokenFromHash() {
|
|
57
|
+
if (!isBrowser()) return;
|
|
58
|
+
try {
|
|
59
|
+
window.history.replaceState(
|
|
60
|
+
null,
|
|
61
|
+
"",
|
|
62
|
+
window.location.pathname + window.location.search
|
|
63
|
+
);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn("Failed to clean token from hash:", error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function saveAccessToken(token) {
|
|
69
|
+
if (!isBrowser()) {
|
|
70
|
+
console.warn("Cannot save token: not in browser environment");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("Failed to save token to localStorage:", error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function removeAccessToken() {
|
|
80
|
+
if (!isBrowser()) return;
|
|
81
|
+
try {
|
|
82
|
+
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn("Failed to remove token from localStorage:", error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function getAccessToken() {
|
|
88
|
+
const hashToken = getTokenFromHash();
|
|
89
|
+
if (hashToken) {
|
|
90
|
+
saveAccessToken(hashToken);
|
|
91
|
+
cleanTokenFromHash();
|
|
92
|
+
console.log("\u2705 OAuth token captured from URL hash");
|
|
93
|
+
if (isBrowser()) {
|
|
94
|
+
window.dispatchEvent(new CustomEvent("omnikit:auth-change"));
|
|
95
|
+
}
|
|
96
|
+
return hashToken;
|
|
97
|
+
}
|
|
98
|
+
const urlToken = getTokenFromUrl();
|
|
99
|
+
if (urlToken) {
|
|
100
|
+
saveAccessToken(urlToken);
|
|
101
|
+
cleanTokenFromUrl();
|
|
102
|
+
if (isBrowser()) {
|
|
103
|
+
window.dispatchEvent(new CustomEvent("omnikit:auth-change"));
|
|
104
|
+
}
|
|
105
|
+
return urlToken;
|
|
106
|
+
}
|
|
107
|
+
return getTokenFromStorage();
|
|
108
|
+
}
|
|
109
|
+
function setAccessToken(token) {
|
|
110
|
+
saveAccessToken(token);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/live-voice.ts
|
|
114
|
+
var INPUT_SAMPLE_RATE = 16e3;
|
|
115
|
+
var OUTPUT_SAMPLE_RATE = 24e3;
|
|
116
|
+
var CHUNK_SIZE = 4096;
|
|
117
|
+
var LiveVoiceSessionImpl = class {
|
|
118
|
+
constructor(baseUrl, appId, token, config) {
|
|
119
|
+
this.config = config;
|
|
120
|
+
this.ws = null;
|
|
121
|
+
this.audioContext = null;
|
|
122
|
+
this.mediaStream = null;
|
|
123
|
+
this.scriptProcessor = null;
|
|
124
|
+
this.sourceNode = null;
|
|
125
|
+
this.gainNode = null;
|
|
126
|
+
// Audio playback queue
|
|
127
|
+
this.playbackQueue = [];
|
|
128
|
+
this.isPlaying = false;
|
|
129
|
+
// Session state
|
|
130
|
+
this._isActive = false;
|
|
131
|
+
this._status = "idle";
|
|
132
|
+
this._sessionId = null;
|
|
133
|
+
this.baseUrl = baseUrl;
|
|
134
|
+
this.appId = appId;
|
|
135
|
+
this.token = token;
|
|
136
|
+
}
|
|
137
|
+
get isActive() {
|
|
138
|
+
return this._isActive;
|
|
139
|
+
}
|
|
140
|
+
get status() {
|
|
141
|
+
return this._status;
|
|
142
|
+
}
|
|
143
|
+
get sessionId() {
|
|
144
|
+
return this._sessionId;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Start the voice session
|
|
148
|
+
*/
|
|
149
|
+
async start() {
|
|
150
|
+
if (this._isActive) {
|
|
151
|
+
throw new OmnikitError("Session already active", 400, "SESSION_ACTIVE");
|
|
152
|
+
}
|
|
153
|
+
this.setStatus("connecting");
|
|
154
|
+
try {
|
|
155
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
156
|
+
audio: {
|
|
157
|
+
sampleRate: INPUT_SAMPLE_RATE,
|
|
158
|
+
channelCount: 1,
|
|
159
|
+
echoCancellation: true,
|
|
160
|
+
noiseSuppression: true,
|
|
161
|
+
autoGainControl: true
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
this.audioContext = new AudioContext({ sampleRate: OUTPUT_SAMPLE_RATE });
|
|
165
|
+
const wsUrl = this.buildWebSocketUrl();
|
|
166
|
+
await this.connectWebSocket(wsUrl);
|
|
167
|
+
await this.setupAudioCapture();
|
|
168
|
+
this._isActive = true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.setStatus("error");
|
|
171
|
+
this.config?.onError?.(error);
|
|
172
|
+
await this.cleanup();
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Stop the voice session
|
|
178
|
+
*/
|
|
179
|
+
async stop() {
|
|
180
|
+
if (!this._isActive) return;
|
|
181
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
182
|
+
this.ws.send(JSON.stringify({ type: "end" }));
|
|
183
|
+
}
|
|
184
|
+
await this.cleanup();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Interrupt the AI while speaking
|
|
188
|
+
*/
|
|
189
|
+
interrupt() {
|
|
190
|
+
if (!this._isActive || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.playbackQueue = [];
|
|
194
|
+
this.ws.send(JSON.stringify({ type: "interrupt" }));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Build WebSocket URL with auth and config
|
|
198
|
+
*/
|
|
199
|
+
buildWebSocketUrl() {
|
|
200
|
+
const wsProtocol = this.baseUrl.startsWith("https") ? "wss" : "ws";
|
|
201
|
+
const wsBase = this.baseUrl.replace(/^https?/, wsProtocol);
|
|
202
|
+
const params = new URLSearchParams();
|
|
203
|
+
if (this.token) {
|
|
204
|
+
params.set("token", this.token);
|
|
205
|
+
}
|
|
206
|
+
if (this.config?.systemInstruction) {
|
|
207
|
+
params.set("system_instruction", this.config.systemInstruction);
|
|
208
|
+
}
|
|
209
|
+
if (this.config?.voice) {
|
|
210
|
+
params.set("voice", this.config.voice);
|
|
211
|
+
}
|
|
212
|
+
return `${wsBase}/apps/${this.appId}/live-voice?${params.toString()}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Connect to WebSocket server
|
|
216
|
+
*/
|
|
217
|
+
connectWebSocket(url) {
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
this.ws = new WebSocket(url);
|
|
220
|
+
this.ws.binaryType = "arraybuffer";
|
|
221
|
+
const timeoutId = setTimeout(() => {
|
|
222
|
+
reject(new OmnikitError("WebSocket connection timeout", 408, "CONNECT_TIMEOUT"));
|
|
223
|
+
}, 1e4);
|
|
224
|
+
this.ws.onopen = () => {
|
|
225
|
+
clearTimeout(timeoutId);
|
|
226
|
+
};
|
|
227
|
+
this.ws.onmessage = (event) => {
|
|
228
|
+
this.handleMessage(event);
|
|
229
|
+
if (this._sessionId && this._status === "listening") {
|
|
230
|
+
resolve();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
this.ws.onerror = (event) => {
|
|
234
|
+
clearTimeout(timeoutId);
|
|
235
|
+
const error = new OmnikitError("WebSocket connection error", 500, "WS_ERROR");
|
|
236
|
+
reject(error);
|
|
237
|
+
};
|
|
238
|
+
this.ws.onclose = (event) => {
|
|
239
|
+
clearTimeout(timeoutId);
|
|
240
|
+
if (this._isActive) {
|
|
241
|
+
this.setStatus("disconnected");
|
|
242
|
+
this.config?.onError?.(new OmnikitError(
|
|
243
|
+
`Connection closed: ${event.reason || "Unknown reason"}`,
|
|
244
|
+
event.code,
|
|
245
|
+
"CONNECTION_CLOSED"
|
|
246
|
+
));
|
|
247
|
+
this.cleanup();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Handle incoming WebSocket messages
|
|
254
|
+
*/
|
|
255
|
+
handleMessage(event) {
|
|
256
|
+
if (event.data instanceof ArrayBuffer) {
|
|
257
|
+
this.handleAudioData(event.data);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const message = JSON.parse(event.data);
|
|
262
|
+
switch (message.type) {
|
|
263
|
+
case "session_started":
|
|
264
|
+
this._sessionId = message.session_id || null;
|
|
265
|
+
this.setStatus("listening");
|
|
266
|
+
this.config?.onSessionStarted?.(this._sessionId);
|
|
267
|
+
break;
|
|
268
|
+
case "session_ended":
|
|
269
|
+
this.config?.onSessionEnded?.(message.duration_seconds || 0);
|
|
270
|
+
this.cleanup();
|
|
271
|
+
break;
|
|
272
|
+
case "transcript":
|
|
273
|
+
if (message.text && message.role) {
|
|
274
|
+
this.config?.onTranscript?.(message.text, message.role);
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
case "status":
|
|
278
|
+
if (message.status) {
|
|
279
|
+
this.setStatus(message.status);
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
case "error":
|
|
283
|
+
const error = new OmnikitError(
|
|
284
|
+
message.message || "Unknown error",
|
|
285
|
+
500,
|
|
286
|
+
message.code
|
|
287
|
+
);
|
|
288
|
+
this.config?.onError?.(error);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.warn("[LiveVoice] Failed to parse message:", e);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Handle incoming audio data from Gemini
|
|
297
|
+
*/
|
|
298
|
+
handleAudioData(arrayBuffer) {
|
|
299
|
+
if (!this.audioContext) return;
|
|
300
|
+
if (this._status !== "speaking") {
|
|
301
|
+
this.setStatus("speaking");
|
|
302
|
+
}
|
|
303
|
+
const pcmData = new Int16Array(arrayBuffer);
|
|
304
|
+
const floatData = new Float32Array(pcmData.length);
|
|
305
|
+
for (let i = 0; i < pcmData.length; i++) {
|
|
306
|
+
floatData[i] = pcmData[i] / 32768;
|
|
307
|
+
}
|
|
308
|
+
this.playbackQueue.push(floatData);
|
|
309
|
+
if (!this.isPlaying) {
|
|
310
|
+
this.playNextChunk();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Play next audio chunk from queue
|
|
315
|
+
*/
|
|
316
|
+
playNextChunk() {
|
|
317
|
+
if (!this.audioContext || this.playbackQueue.length === 0) {
|
|
318
|
+
this.isPlaying = false;
|
|
319
|
+
if (this._status === "speaking") {
|
|
320
|
+
this.setStatus("listening");
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.isPlaying = true;
|
|
325
|
+
const floatData = this.playbackQueue.shift();
|
|
326
|
+
const audioBuffer = this.audioContext.createBuffer(
|
|
327
|
+
1,
|
|
328
|
+
// mono
|
|
329
|
+
floatData.length,
|
|
330
|
+
OUTPUT_SAMPLE_RATE
|
|
331
|
+
);
|
|
332
|
+
audioBuffer.getChannelData(0).set(floatData);
|
|
333
|
+
const source = this.audioContext.createBufferSource();
|
|
334
|
+
source.buffer = audioBuffer;
|
|
335
|
+
source.connect(this.audioContext.destination);
|
|
336
|
+
source.onended = () => {
|
|
337
|
+
this.playNextChunk();
|
|
338
|
+
};
|
|
339
|
+
source.start();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Set up audio capture from microphone
|
|
343
|
+
*/
|
|
344
|
+
async setupAudioCapture() {
|
|
345
|
+
if (!this.audioContext || !this.mediaStream) return;
|
|
346
|
+
this.sourceNode = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
347
|
+
this.scriptProcessor = this.audioContext.createScriptProcessor(CHUNK_SIZE, 1, 1);
|
|
348
|
+
const inputSampleRate = this.audioContext.sampleRate;
|
|
349
|
+
this.scriptProcessor.onaudioprocess = (event) => {
|
|
350
|
+
if (!this._isActive || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const inputData = event.inputBuffer.getChannelData(0);
|
|
354
|
+
const resampledData = this.resample(inputData, inputSampleRate, INPUT_SAMPLE_RATE);
|
|
355
|
+
const pcmData = new Int16Array(resampledData.length);
|
|
356
|
+
for (let i = 0; i < resampledData.length; i++) {
|
|
357
|
+
const sample = Math.max(-1, Math.min(1, resampledData[i]));
|
|
358
|
+
pcmData[i] = sample < 0 ? sample * 32768 : sample * 32767;
|
|
359
|
+
}
|
|
360
|
+
this.ws.send(pcmData.buffer);
|
|
361
|
+
};
|
|
362
|
+
this.sourceNode.connect(this.scriptProcessor);
|
|
363
|
+
this.scriptProcessor.connect(this.audioContext.destination);
|
|
364
|
+
this.gainNode = this.audioContext.createGain();
|
|
365
|
+
this.gainNode.gain.value = 0;
|
|
366
|
+
this.scriptProcessor.disconnect();
|
|
367
|
+
this.scriptProcessor.connect(this.gainNode);
|
|
368
|
+
this.gainNode.connect(this.audioContext.destination);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Simple linear resampling
|
|
372
|
+
*/
|
|
373
|
+
resample(inputData, inputRate, outputRate) {
|
|
374
|
+
if (inputRate === outputRate) {
|
|
375
|
+
return inputData;
|
|
376
|
+
}
|
|
377
|
+
const ratio = inputRate / outputRate;
|
|
378
|
+
const outputLength = Math.floor(inputData.length / ratio);
|
|
379
|
+
const output = new Float32Array(outputLength);
|
|
380
|
+
for (let i = 0; i < outputLength; i++) {
|
|
381
|
+
const srcIndex = i * ratio;
|
|
382
|
+
const srcIndexFloor = Math.floor(srcIndex);
|
|
383
|
+
const srcIndexCeil = Math.min(srcIndexFloor + 1, inputData.length - 1);
|
|
384
|
+
const fraction = srcIndex - srcIndexFloor;
|
|
385
|
+
output[i] = inputData[srcIndexFloor] * (1 - fraction) + inputData[srcIndexCeil] * fraction;
|
|
386
|
+
}
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Update status and notify callback
|
|
391
|
+
*/
|
|
392
|
+
setStatus(status) {
|
|
393
|
+
if (this._status !== status) {
|
|
394
|
+
this._status = status;
|
|
395
|
+
this.config?.onStatusChange?.(status);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Clean up all resources
|
|
400
|
+
*/
|
|
401
|
+
async cleanup() {
|
|
402
|
+
this._isActive = false;
|
|
403
|
+
this.playbackQueue = [];
|
|
404
|
+
this.isPlaying = false;
|
|
405
|
+
if (this.scriptProcessor) {
|
|
406
|
+
this.scriptProcessor.disconnect();
|
|
407
|
+
this.scriptProcessor = null;
|
|
408
|
+
}
|
|
409
|
+
if (this.sourceNode) {
|
|
410
|
+
this.sourceNode.disconnect();
|
|
411
|
+
this.sourceNode = null;
|
|
412
|
+
}
|
|
413
|
+
if (this.gainNode) {
|
|
414
|
+
this.gainNode.disconnect();
|
|
415
|
+
this.gainNode = null;
|
|
416
|
+
}
|
|
417
|
+
if (this.mediaStream) {
|
|
418
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
419
|
+
this.mediaStream = null;
|
|
420
|
+
}
|
|
421
|
+
if (this.audioContext && this.audioContext.state !== "closed") {
|
|
422
|
+
await this.audioContext.close();
|
|
423
|
+
this.audioContext = null;
|
|
424
|
+
}
|
|
425
|
+
if (this.ws) {
|
|
426
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
427
|
+
this.ws.close(1e3, "Session ended");
|
|
428
|
+
}
|
|
429
|
+
this.ws = null;
|
|
430
|
+
}
|
|
431
|
+
this._sessionId = null;
|
|
432
|
+
this.setStatus("idle");
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/client.ts
|
|
437
|
+
var LLM_MODEL_MAP = {
|
|
438
|
+
"gemini-flash": "vertex_ai/gemini-2.5-flash",
|
|
439
|
+
"gemini-pro": "vertex_ai/gemini-2.5-pro"
|
|
440
|
+
};
|
|
441
|
+
function mapLLMModel(model) {
|
|
442
|
+
if (!model) return void 0;
|
|
443
|
+
return LLM_MODEL_MAP[model] || model;
|
|
444
|
+
}
|
|
445
|
+
function makeArrayForgiving(arr) {
|
|
446
|
+
if (!Array.isArray(arr)) return arr;
|
|
447
|
+
Object.defineProperty(arr, "items", {
|
|
448
|
+
get() {
|
|
449
|
+
return this;
|
|
450
|
+
},
|
|
451
|
+
enumerable: false,
|
|
452
|
+
configurable: true
|
|
453
|
+
});
|
|
454
|
+
return arr;
|
|
455
|
+
}
|
|
456
|
+
var OmnikitError = class _OmnikitError extends Error {
|
|
457
|
+
constructor(message, status, code, data) {
|
|
458
|
+
super(message);
|
|
459
|
+
this.name = "OmnikitError";
|
|
460
|
+
this.status = status;
|
|
461
|
+
this.code = code;
|
|
462
|
+
this.data = data;
|
|
463
|
+
this.isAuthError = status === 401 || status === 403;
|
|
464
|
+
if (Error.captureStackTrace) {
|
|
465
|
+
Error.captureStackTrace(this, _OmnikitError);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
isNotFound() {
|
|
469
|
+
return this.status === 404;
|
|
470
|
+
}
|
|
471
|
+
isForbidden() {
|
|
472
|
+
return this.status === 403;
|
|
473
|
+
}
|
|
474
|
+
isUnauthorized() {
|
|
475
|
+
return this.status === 401;
|
|
476
|
+
}
|
|
477
|
+
isBadRequest() {
|
|
478
|
+
return this.status === 400;
|
|
479
|
+
}
|
|
480
|
+
isServerError() {
|
|
481
|
+
return this.status !== void 0 && this.status >= 500;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var getMetadataCacheKey = (appId) => `omnikit_metadata_${appId}`;
|
|
485
|
+
var APIClient = class {
|
|
486
|
+
constructor(config) {
|
|
487
|
+
this.userToken = null;
|
|
488
|
+
this._serviceToken = null;
|
|
489
|
+
this._apiKey = null;
|
|
490
|
+
this.initialized = false;
|
|
491
|
+
this.initPromise = null;
|
|
492
|
+
this._collections = {};
|
|
493
|
+
this._services = {};
|
|
494
|
+
// New flat services
|
|
495
|
+
this._integrations = {};
|
|
496
|
+
// Callbacks for metadata updates (for React re-render triggers)
|
|
497
|
+
this._metadataListeners = /* @__PURE__ */ new Set();
|
|
498
|
+
// Callbacks for user state changes (for React auth state sync)
|
|
499
|
+
this._userListeners = /* @__PURE__ */ new Set();
|
|
500
|
+
this.appId = config.appId;
|
|
501
|
+
this.baseUrl = config.serverUrl || config.baseUrl || "http://localhost:8001/api";
|
|
502
|
+
this._serviceToken = config.serviceToken || null;
|
|
503
|
+
this._apiKey = config.apiKey || null;
|
|
504
|
+
const isBrowser2 = typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
505
|
+
this._metadata = this.loadCachedMetadata(config.initialMetadata);
|
|
506
|
+
if (isBrowser2) {
|
|
507
|
+
setAccessTokenKey(this.appId);
|
|
508
|
+
}
|
|
509
|
+
const autoInitAuth = config.autoInitAuth !== false;
|
|
510
|
+
if (config.token) {
|
|
511
|
+
this.userToken = config.token;
|
|
512
|
+
if (isBrowser2) {
|
|
513
|
+
saveAccessToken(config.token);
|
|
514
|
+
}
|
|
515
|
+
} else if (autoInitAuth && isBrowser2) {
|
|
516
|
+
const detectedToken = getAccessToken();
|
|
517
|
+
if (detectedToken) {
|
|
518
|
+
this.userToken = detectedToken;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
this.ensureInitialized().catch((err) => {
|
|
522
|
+
console.warn("[Omnikit SDK] Background initialization failed:", err);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Load metadata from localStorage cache, falling back to initial config
|
|
527
|
+
* Guards localStorage access for Deno/Node compatibility
|
|
528
|
+
*/
|
|
529
|
+
loadCachedMetadata(initialMetadata) {
|
|
530
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
531
|
+
try {
|
|
532
|
+
const cached = localStorage.getItem(getMetadataCacheKey(this.appId));
|
|
533
|
+
if (cached) {
|
|
534
|
+
const parsed = JSON.parse(cached);
|
|
535
|
+
return {
|
|
536
|
+
id: this.appId,
|
|
537
|
+
name: parsed.name || "",
|
|
538
|
+
logoUrl: parsed.logoUrl || "",
|
|
539
|
+
thumbnailUrl: parsed.thumbnailUrl || "",
|
|
540
|
+
visibility: parsed.visibility || "private",
|
|
541
|
+
platformAuthProviders: parsed.platformAuthProviders || ["google"]
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
} catch (e) {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
id: this.appId,
|
|
549
|
+
name: initialMetadata?.name || "",
|
|
550
|
+
logoUrl: initialMetadata?.logoUrl || "",
|
|
551
|
+
thumbnailUrl: initialMetadata?.thumbnailUrl || "",
|
|
552
|
+
visibility: "private",
|
|
553
|
+
// Default to private, updated after fetch
|
|
554
|
+
platformAuthProviders: ["google"]
|
|
555
|
+
// Default to Google only
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Update metadata cache in localStorage
|
|
560
|
+
* IMPORTANT: Mutate in place to preserve object reference for exports
|
|
561
|
+
*/
|
|
562
|
+
updateMetadataCache(data) {
|
|
563
|
+
this._metadata.name = data.name || this._metadata.name;
|
|
564
|
+
this._metadata.logoUrl = data.logoUrl || this._metadata.logoUrl;
|
|
565
|
+
this._metadata.thumbnailUrl = data.thumbnailUrl || this._metadata.thumbnailUrl;
|
|
566
|
+
this._metadata.visibility = data.visibility || this._metadata.visibility || "private";
|
|
567
|
+
this._metadata.platformAuthProviders = data.platformAuthProviders || this._metadata.platformAuthProviders || ["google"];
|
|
568
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
569
|
+
try {
|
|
570
|
+
localStorage.setItem(getMetadataCacheKey(this.appId), JSON.stringify(this._metadata));
|
|
571
|
+
} catch (e) {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
this._metadataListeners.forEach((callback) => callback());
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* App metadata - available instantly without API call (no flicker!)
|
|
578
|
+
*/
|
|
579
|
+
get metadata() {
|
|
580
|
+
return this._metadata;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Subscribe to metadata updates (for React components to trigger re-render)
|
|
584
|
+
* @returns Unsubscribe function
|
|
585
|
+
*/
|
|
586
|
+
onMetadataChange(callback) {
|
|
587
|
+
this._metadataListeners.add(callback);
|
|
588
|
+
return () => this._metadataListeners.delete(callback);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Subscribe to user state changes (for React auth state sync)
|
|
592
|
+
* Called when user logs in, logs out, or updates profile
|
|
593
|
+
* @returns Unsubscribe function
|
|
594
|
+
*/
|
|
595
|
+
onUserChange(callback) {
|
|
596
|
+
this._userListeners.add(callback);
|
|
597
|
+
return () => this._userListeners.delete(callback);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Emit user change event to all listeners
|
|
601
|
+
*/
|
|
602
|
+
emitUserChange(user) {
|
|
603
|
+
this._userListeners.forEach((cb) => {
|
|
604
|
+
try {
|
|
605
|
+
cb(user);
|
|
606
|
+
} catch (e) {
|
|
607
|
+
console.error("[Omnikit SDK] User listener error:", e);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Lazy getter for collections - auto-initializes on first access
|
|
613
|
+
*/
|
|
614
|
+
get collections() {
|
|
615
|
+
return this.createCollectionsProxy(false);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* @deprecated Use collections instead. This alias exists for backward compatibility.
|
|
619
|
+
*/
|
|
620
|
+
get entities() {
|
|
621
|
+
return this.collections;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Lazy getter for services (new flat structure) - auto-initializes on first access
|
|
625
|
+
* Usage: omnikit.services.SendEmail({ to, subject, body })
|
|
626
|
+
*/
|
|
627
|
+
get services() {
|
|
628
|
+
return this.createServicesProxy(false);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* @deprecated Use services instead for flat access
|
|
632
|
+
* Lazy getter for integrations - auto-initializes on first access
|
|
633
|
+
*/
|
|
634
|
+
get integrations() {
|
|
635
|
+
return this.createIntegrationsProxy(false);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Lazy getter for auth - auto-initializes on first access
|
|
639
|
+
*/
|
|
640
|
+
get auth() {
|
|
641
|
+
return this.createAuthProxy();
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Lazy getter for service role operations
|
|
645
|
+
* Only available when serviceToken is provided
|
|
646
|
+
*/
|
|
647
|
+
get asServiceRole() {
|
|
648
|
+
if (!this._serviceToken) {
|
|
649
|
+
throw new OmnikitError(
|
|
650
|
+
"Service token is required to use asServiceRole. Provide serviceToken in config.",
|
|
651
|
+
403,
|
|
652
|
+
"SERVICE_TOKEN_REQUIRED"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (this._asServiceRole) {
|
|
656
|
+
return this._asServiceRole;
|
|
657
|
+
}
|
|
658
|
+
this._asServiceRole = {
|
|
659
|
+
collections: this.createCollectionsProxy(true),
|
|
660
|
+
// Service role collections
|
|
661
|
+
services: this.createServicesProxy(true),
|
|
662
|
+
// Service role services (new flat structure)
|
|
663
|
+
integrations: this.createIntegrationsProxy(true)
|
|
664
|
+
// Service role integrations (legacy)
|
|
665
|
+
// Note: auth not available in service role for security
|
|
666
|
+
};
|
|
667
|
+
return this._asServiceRole;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Create auth proxy that auto-initializes
|
|
671
|
+
*/
|
|
672
|
+
createAuthProxy() {
|
|
673
|
+
const client = this;
|
|
674
|
+
const authHandler = {
|
|
675
|
+
async isAuthenticated() {
|
|
676
|
+
await client.ensureInitialized();
|
|
677
|
+
try {
|
|
678
|
+
const response = await client.makeRequest(
|
|
679
|
+
`${client.baseUrl}/auth/is-authenticated?app_id=${client.appId}`,
|
|
680
|
+
"GET"
|
|
681
|
+
);
|
|
682
|
+
return response?.authenticated === true;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
async me() {
|
|
688
|
+
await client.ensureInitialized();
|
|
689
|
+
const response = await client.makeRequest(
|
|
690
|
+
`${client.baseUrl}/apps/${client.appId}/collections/user/me`,
|
|
691
|
+
"GET"
|
|
692
|
+
);
|
|
693
|
+
client.emitUserChange(response);
|
|
694
|
+
return response;
|
|
695
|
+
},
|
|
696
|
+
login(returnUrl) {
|
|
697
|
+
const fullReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
698
|
+
const encodedReturnUrl = encodeURIComponent(fullReturnUrl);
|
|
699
|
+
if (typeof window !== "undefined") {
|
|
700
|
+
const currentPath = window.location.pathname;
|
|
701
|
+
const apiSitesMatch = currentPath.match(/^\/api\/sites\/([^\/]+)/);
|
|
702
|
+
if (apiSitesMatch) {
|
|
703
|
+
window.location.href = `/api/sites/${client.appId}/login?return_url=${encodedReturnUrl}`;
|
|
704
|
+
} else {
|
|
705
|
+
window.location.href = `/login?return_url=${encodedReturnUrl}`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
/**
|
|
710
|
+
* Request a passwordless login code to email
|
|
711
|
+
*/
|
|
712
|
+
async requestLoginCode(email, returnUrl) {
|
|
713
|
+
return await client.makeRequest(
|
|
714
|
+
`${client.baseUrl}/auth/email/request-code`,
|
|
715
|
+
"POST",
|
|
716
|
+
{ email, return_url: returnUrl }
|
|
717
|
+
);
|
|
718
|
+
},
|
|
719
|
+
/**
|
|
720
|
+
* Verify the login code and set the session token
|
|
721
|
+
*/
|
|
722
|
+
async verifyLoginCode(email, code) {
|
|
723
|
+
const response = await client.makeRequest(
|
|
724
|
+
`${client.baseUrl}/auth/email/verify-code`,
|
|
725
|
+
"POST",
|
|
726
|
+
{ email, code }
|
|
727
|
+
);
|
|
728
|
+
if (response.access_token) {
|
|
729
|
+
client.setAuthToken(response.access_token);
|
|
730
|
+
try {
|
|
731
|
+
const user = await this.me();
|
|
732
|
+
client.emitUserChange(user);
|
|
733
|
+
} catch (e) {
|
|
734
|
+
client.emitUserChange(null);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return response;
|
|
738
|
+
},
|
|
739
|
+
/**
|
|
740
|
+
* Get available OAuth providers for this app
|
|
741
|
+
* Returns both platform providers (zero-config) and custom SSO providers
|
|
742
|
+
*/
|
|
743
|
+
async getAvailableProviders() {
|
|
744
|
+
await client.ensureInitialized();
|
|
745
|
+
try {
|
|
746
|
+
const response = await client.makeRequest(
|
|
747
|
+
`${client.baseUrl}/apps/${client.appId}/auth/providers`,
|
|
748
|
+
"GET"
|
|
749
|
+
);
|
|
750
|
+
return response;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
throw new OmnikitError(
|
|
753
|
+
"Failed to fetch available OAuth providers",
|
|
754
|
+
error.status || 500,
|
|
755
|
+
"PROVIDERS_FETCH_FAILED"
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
/**
|
|
760
|
+
* Initiate OAuth login with any provider
|
|
761
|
+
* Redirects to the backend OAuth endpoint for the specified provider
|
|
762
|
+
*/
|
|
763
|
+
loginWithProvider(providerId, returnUrl) {
|
|
764
|
+
const currentUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
765
|
+
const redirectUrl = `${client.baseUrl}/auth/${providerId}?redirect_url=${encodeURIComponent(currentUrl)}`;
|
|
766
|
+
if (typeof window !== "undefined") {
|
|
767
|
+
const inIframe = window.self !== window.top;
|
|
768
|
+
if (inIframe) {
|
|
769
|
+
const width = 600;
|
|
770
|
+
const height = 700;
|
|
771
|
+
const left = window.screen.width / 2 - width / 2;
|
|
772
|
+
const top = window.screen.height / 2 - height / 2;
|
|
773
|
+
const popup = window.open(
|
|
774
|
+
redirectUrl,
|
|
775
|
+
"oauth_popup",
|
|
776
|
+
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=yes`
|
|
777
|
+
);
|
|
778
|
+
if (popup) {
|
|
779
|
+
const checkTimer = setInterval(() => {
|
|
780
|
+
const token = getAccessToken();
|
|
781
|
+
if (token) {
|
|
782
|
+
clearInterval(checkTimer);
|
|
783
|
+
popup.close();
|
|
784
|
+
window.location.reload();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (popup.closed) {
|
|
788
|
+
clearInterval(checkTimer);
|
|
789
|
+
const finalToken = getAccessToken();
|
|
790
|
+
if (finalToken) {
|
|
791
|
+
window.location.reload();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}, 1e3);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
window.location.href = redirectUrl;
|
|
799
|
+
} else {
|
|
800
|
+
throw new OmnikitError("loginWithProvider() can only be called in browser environment", 400, "NOT_BROWSER");
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
/**
|
|
804
|
+
* Initiate Google OAuth Login
|
|
805
|
+
* @deprecated Use loginWithProvider("google", returnUrl) instead
|
|
806
|
+
* Redirects to the backend OAuth endpoint
|
|
807
|
+
*/
|
|
808
|
+
loginWithGoogle(returnUrl) {
|
|
809
|
+
console.warn('loginWithGoogle() is deprecated. Use loginWithProvider("google", returnUrl) instead.');
|
|
810
|
+
this.loginWithProvider("google", returnUrl);
|
|
811
|
+
},
|
|
812
|
+
async logout() {
|
|
813
|
+
await client.makeRequest(
|
|
814
|
+
`${client.baseUrl}/auth/logout`,
|
|
815
|
+
"POST"
|
|
816
|
+
);
|
|
817
|
+
client.clearAuthToken();
|
|
818
|
+
client.emitUserChange(null);
|
|
819
|
+
},
|
|
820
|
+
async refreshToken() {
|
|
821
|
+
const response = await client.makeRequest(
|
|
822
|
+
`${client.baseUrl}/auth/refresh`,
|
|
823
|
+
"POST"
|
|
824
|
+
);
|
|
825
|
+
if (response.access_token || response.token) {
|
|
826
|
+
const newToken = response.access_token || response.token;
|
|
827
|
+
client.setAuthToken(newToken);
|
|
828
|
+
}
|
|
829
|
+
return response;
|
|
830
|
+
},
|
|
831
|
+
async updateMe(data) {
|
|
832
|
+
await client.ensureInitialized();
|
|
833
|
+
const response = await client.makeRequest(
|
|
834
|
+
`${client.baseUrl}/apps/${client.appId}/collections/user/me`,
|
|
835
|
+
"PUT",
|
|
836
|
+
data
|
|
837
|
+
);
|
|
838
|
+
client.emitUserChange(response);
|
|
839
|
+
return response;
|
|
840
|
+
},
|
|
841
|
+
// Alias for updateMe with clearer naming
|
|
842
|
+
async updateProfile(data) {
|
|
843
|
+
return this.updateMe(data);
|
|
844
|
+
},
|
|
845
|
+
// Backward compatibility
|
|
846
|
+
async getCurrentUser() {
|
|
847
|
+
return this.me();
|
|
848
|
+
},
|
|
849
|
+
// Subscribe to user state changes (for React auth state sync)
|
|
850
|
+
onUserChange(callback) {
|
|
851
|
+
return client.onUserChange(callback);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
return authHandler;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Convert PascalCase to snake_case
|
|
858
|
+
* Example: PdfDocument -> pdf_document, UserProfile -> user_profile
|
|
859
|
+
*/
|
|
860
|
+
toSnakeCase(str) {
|
|
861
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Create collections proxy that auto-initializes
|
|
865
|
+
* @param useServiceToken - Whether to use service token for requests
|
|
866
|
+
*/
|
|
867
|
+
createCollectionsProxy(useServiceToken) {
|
|
868
|
+
const client = this;
|
|
869
|
+
return new Proxy({}, {
|
|
870
|
+
get(target, collectionName) {
|
|
871
|
+
if (typeof collectionName === "string" && !collectionName.startsWith("_")) {
|
|
872
|
+
return new Proxy({}, {
|
|
873
|
+
get(obj, method) {
|
|
874
|
+
if (typeof method === "string") {
|
|
875
|
+
return async function(...args) {
|
|
876
|
+
await client.ensureInitialized();
|
|
877
|
+
const normalizedCollectionName = client.toSnakeCase(collectionName);
|
|
878
|
+
const collection = client._collections[normalizedCollectionName];
|
|
879
|
+
if (!collection) {
|
|
880
|
+
throw new OmnikitError(
|
|
881
|
+
`Collection '${collectionName}' not found in app schema`,
|
|
882
|
+
404,
|
|
883
|
+
"COLLECTION_NOT_FOUND"
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (typeof collection[method] !== "function") {
|
|
887
|
+
const availableMethods = [
|
|
888
|
+
"get(id)",
|
|
889
|
+
"list(options?)",
|
|
890
|
+
"filter(filters?)",
|
|
891
|
+
"findOne(filters?)",
|
|
892
|
+
"create(data)",
|
|
893
|
+
"update(id, data)",
|
|
894
|
+
"delete(id)",
|
|
895
|
+
"count(filters?)",
|
|
896
|
+
"bulkCreate(items)",
|
|
897
|
+
"bulkDelete(ids)",
|
|
898
|
+
"import(file, format)",
|
|
899
|
+
"deleteAll(confirm)"
|
|
900
|
+
];
|
|
901
|
+
throw new OmnikitError(
|
|
902
|
+
`Method '${method}()' does not exist on collection '${collectionName}'.
|
|
903
|
+
|
|
904
|
+
Available methods:
|
|
905
|
+
${availableMethods.map((m) => ` - ${m}`).join("\n")}
|
|
906
|
+
|
|
907
|
+
Common mistake: There is NO 'read()' method. Use 'list()' instead.
|
|
908
|
+
Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
|
|
909
|
+
400,
|
|
910
|
+
"METHOD_NOT_FOUND"
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (useServiceToken) {
|
|
914
|
+
const originalGetToken = client.getAuthToken.bind(client);
|
|
915
|
+
client.getAuthToken = () => client._serviceToken;
|
|
916
|
+
try {
|
|
917
|
+
const result = await collection[method](...args);
|
|
918
|
+
client.getAuthToken = originalGetToken;
|
|
919
|
+
return result;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
client.getAuthToken = originalGetToken;
|
|
922
|
+
throw error;
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
return collection[method](...args);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return target[collectionName];
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Poll for job completion with progress callbacks
|
|
939
|
+
* @param jobId - Job ID to poll
|
|
940
|
+
* @param options - Async options (onStatusChange, pollInterval, timeout)
|
|
941
|
+
* @returns Final job result
|
|
942
|
+
*/
|
|
943
|
+
async pollJobUntilComplete(jobId, options) {
|
|
944
|
+
const pollInterval = options?.pollInterval || 2e3;
|
|
945
|
+
const timeout = options?.timeout || 3e5;
|
|
946
|
+
const startTime = Date.now();
|
|
947
|
+
while (true) {
|
|
948
|
+
const status = await this.makeRequest(
|
|
949
|
+
`${this.baseUrl}/apps/${this.appId}/jobs/${jobId}`,
|
|
950
|
+
"GET"
|
|
951
|
+
);
|
|
952
|
+
if (options?.onStatusChange) {
|
|
953
|
+
options.onStatusChange(status.status, status.status_message);
|
|
954
|
+
}
|
|
955
|
+
if (status.status === "completed") {
|
|
956
|
+
return status.result;
|
|
957
|
+
}
|
|
958
|
+
if (status.status === "failed") {
|
|
959
|
+
throw new OmnikitError(
|
|
960
|
+
status.error || "Job failed",
|
|
961
|
+
500,
|
|
962
|
+
"JOB_FAILED",
|
|
963
|
+
{ job_id: jobId, status }
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (status.status === "cancelled") {
|
|
967
|
+
throw new OmnikitError(
|
|
968
|
+
"Job was cancelled",
|
|
969
|
+
400,
|
|
970
|
+
"JOB_CANCELLED",
|
|
971
|
+
{ job_id: jobId }
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
if (Date.now() - startTime > timeout) {
|
|
975
|
+
throw new OmnikitError(
|
|
976
|
+
`Job timed out after ${timeout / 1e3} seconds`,
|
|
977
|
+
408,
|
|
978
|
+
"JOB_TIMEOUT",
|
|
979
|
+
{ job_id: jobId }
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Stream LLM response token by token via SSE
|
|
987
|
+
* @param params - LLM request parameters with streaming callbacks
|
|
988
|
+
* @returns Promise that resolves to the complete response string
|
|
989
|
+
*/
|
|
990
|
+
async streamLLMResponse(params) {
|
|
991
|
+
await this.ensureInitialized();
|
|
992
|
+
const headers = {
|
|
993
|
+
"Content-Type": "application/json"
|
|
994
|
+
};
|
|
995
|
+
const token = this.getAuthToken();
|
|
996
|
+
if (token) {
|
|
997
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
998
|
+
}
|
|
999
|
+
if (this._serviceToken) {
|
|
1000
|
+
headers["X-Service-Token"] = this._serviceToken;
|
|
1001
|
+
}
|
|
1002
|
+
if (this._apiKey) {
|
|
1003
|
+
headers["X-API-Key"] = this._apiKey;
|
|
1004
|
+
}
|
|
1005
|
+
const requestBody = {
|
|
1006
|
+
...params,
|
|
1007
|
+
stream: true,
|
|
1008
|
+
// Map friendly model names to full model names
|
|
1009
|
+
model: mapLLMModel(params.model),
|
|
1010
|
+
// Remove callback functions from request body
|
|
1011
|
+
onToken: void 0,
|
|
1012
|
+
onComplete: void 0,
|
|
1013
|
+
onError: void 0
|
|
1014
|
+
};
|
|
1015
|
+
Object.keys(requestBody).forEach((key) => {
|
|
1016
|
+
if (requestBody[key] === void 0) {
|
|
1017
|
+
delete requestBody[key];
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
const response = await fetch(
|
|
1021
|
+
`${this.baseUrl}/apps/${this.appId}/services/llm`,
|
|
1022
|
+
{
|
|
1023
|
+
method: "POST",
|
|
1024
|
+
headers,
|
|
1025
|
+
body: JSON.stringify(requestBody)
|
|
1026
|
+
}
|
|
1027
|
+
);
|
|
1028
|
+
if (!response.ok) {
|
|
1029
|
+
const errorText = await response.text();
|
|
1030
|
+
let errorMessage = `LLM streaming failed: ${response.statusText}`;
|
|
1031
|
+
try {
|
|
1032
|
+
const errorJson = JSON.parse(errorText);
|
|
1033
|
+
errorMessage = errorJson.detail || errorJson.message || errorMessage;
|
|
1034
|
+
} catch {
|
|
1035
|
+
errorMessage = errorText || errorMessage;
|
|
1036
|
+
}
|
|
1037
|
+
const err = new OmnikitError(errorMessage, response.status, "STREAM_ERROR");
|
|
1038
|
+
params.onError?.(err);
|
|
1039
|
+
throw err;
|
|
1040
|
+
}
|
|
1041
|
+
const reader = response.body?.getReader();
|
|
1042
|
+
if (!reader) {
|
|
1043
|
+
const err = new OmnikitError("No response body", 500, "NO_BODY");
|
|
1044
|
+
params.onError?.(err);
|
|
1045
|
+
throw err;
|
|
1046
|
+
}
|
|
1047
|
+
const decoder = new TextDecoder();
|
|
1048
|
+
let fullResponse = "";
|
|
1049
|
+
let buffer = "";
|
|
1050
|
+
try {
|
|
1051
|
+
while (true) {
|
|
1052
|
+
const { done, value } = await reader.read();
|
|
1053
|
+
if (done) break;
|
|
1054
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1055
|
+
const lines = buffer.split("\n");
|
|
1056
|
+
buffer = lines.pop() || "";
|
|
1057
|
+
for (const line of lines) {
|
|
1058
|
+
if (line.startsWith("data: ")) {
|
|
1059
|
+
try {
|
|
1060
|
+
const event = JSON.parse(line.slice(6));
|
|
1061
|
+
if (event.type === "token" && event.content) {
|
|
1062
|
+
fullResponse += event.content;
|
|
1063
|
+
params.onToken?.(event.content);
|
|
1064
|
+
} else if (event.type === "done") {
|
|
1065
|
+
params.onComplete?.({
|
|
1066
|
+
result: event.result || fullResponse,
|
|
1067
|
+
model_used: event.model_used || "",
|
|
1068
|
+
usage: event.usage
|
|
1069
|
+
});
|
|
1070
|
+
} else if (event.type === "error") {
|
|
1071
|
+
const err = new OmnikitError(
|
|
1072
|
+
event.message || "Stream error",
|
|
1073
|
+
500,
|
|
1074
|
+
"STREAM_ERROR"
|
|
1075
|
+
);
|
|
1076
|
+
params.onError?.(err);
|
|
1077
|
+
throw err;
|
|
1078
|
+
}
|
|
1079
|
+
} catch (parseError) {
|
|
1080
|
+
if (parseError instanceof OmnikitError) {
|
|
1081
|
+
throw parseError;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
} finally {
|
|
1088
|
+
reader.releaseLock();
|
|
1089
|
+
}
|
|
1090
|
+
return fullResponse;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Create services proxy that auto-initializes (new flat structure)
|
|
1094
|
+
* @param useServiceToken - Whether to use service token for requests
|
|
1095
|
+
*/
|
|
1096
|
+
createServicesProxy(useServiceToken) {
|
|
1097
|
+
const client = this;
|
|
1098
|
+
return new Proxy({}, {
|
|
1099
|
+
get(target, serviceName) {
|
|
1100
|
+
if (typeof serviceName === "string" && !serviceName.startsWith("_")) {
|
|
1101
|
+
if (serviceName === "CheckJobStatus") {
|
|
1102
|
+
return async function(params) {
|
|
1103
|
+
await client.ensureInitialized();
|
|
1104
|
+
if (!params?.job_id) {
|
|
1105
|
+
throw new OmnikitError(
|
|
1106
|
+
"job_id is required for CheckJobStatus",
|
|
1107
|
+
400,
|
|
1108
|
+
"MISSING_JOB_ID"
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
return client.makeRequest(
|
|
1112
|
+
`${client.baseUrl}/apps/${client.appId}/jobs/${params.job_id}`,
|
|
1113
|
+
"GET"
|
|
1114
|
+
);
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
if (serviceName === "DownloadPrivateFile") {
|
|
1118
|
+
return async function(params) {
|
|
1119
|
+
await client.ensureInitialized();
|
|
1120
|
+
if (!params?.file_uri) {
|
|
1121
|
+
throw new OmnikitError(
|
|
1122
|
+
"file_uri is required for DownloadPrivateFile",
|
|
1123
|
+
400,
|
|
1124
|
+
"MISSING_FILE_URI"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
const downloadUrl = new URL(`${client.baseUrl}/apps/${client.appId}/services/files/download`);
|
|
1128
|
+
downloadUrl.searchParams.set("file_uri", params.file_uri);
|
|
1129
|
+
if (params.filename) {
|
|
1130
|
+
downloadUrl.searchParams.set("filename", params.filename);
|
|
1131
|
+
}
|
|
1132
|
+
const token = client.getAuthToken();
|
|
1133
|
+
if (token) {
|
|
1134
|
+
downloadUrl.searchParams.set("token", token);
|
|
1135
|
+
}
|
|
1136
|
+
window.open(downloadUrl.toString(), "_blank");
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
return async function(params, asyncOptions) {
|
|
1140
|
+
await client.ensureInitialized();
|
|
1141
|
+
if (serviceName === "InvokeLLM") {
|
|
1142
|
+
if (params?.model) {
|
|
1143
|
+
params = { ...params, model: mapLLMModel(params.model) };
|
|
1144
|
+
}
|
|
1145
|
+
if (params?.stream) {
|
|
1146
|
+
return client.streamLLMResponse(params);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const method = client._services[serviceName];
|
|
1150
|
+
if (!method) {
|
|
1151
|
+
throw new OmnikitError(
|
|
1152
|
+
`Service '${serviceName}' not found. Available services: ${Object.keys(client._services).join(", ")}`,
|
|
1153
|
+
404,
|
|
1154
|
+
"SERVICE_NOT_FOUND"
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const response = await method(params, useServiceToken);
|
|
1158
|
+
if (response && response.async && response.job_id) {
|
|
1159
|
+
if (asyncOptions?.returnJobId) {
|
|
1160
|
+
return response;
|
|
1161
|
+
}
|
|
1162
|
+
return client.pollJobUntilComplete(response.job_id, asyncOptions);
|
|
1163
|
+
}
|
|
1164
|
+
return response;
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return target[serviceName];
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* @deprecated Use createServicesProxy instead
|
|
1173
|
+
* Create integrations proxy that auto-initializes
|
|
1174
|
+
* @param useServiceToken - Whether to use service token for requests
|
|
1175
|
+
*/
|
|
1176
|
+
createIntegrationsProxy(useServiceToken) {
|
|
1177
|
+
const client = this;
|
|
1178
|
+
return new Proxy({}, {
|
|
1179
|
+
get(target, packageName) {
|
|
1180
|
+
if (typeof packageName === "string" && !packageName.startsWith("_")) {
|
|
1181
|
+
return new Proxy({}, {
|
|
1182
|
+
get(obj, methodName) {
|
|
1183
|
+
if (typeof methodName === "string") {
|
|
1184
|
+
if (methodName === "CheckJobStatus") {
|
|
1185
|
+
return async function(params) {
|
|
1186
|
+
await client.ensureInitialized();
|
|
1187
|
+
if (!params?.job_id) {
|
|
1188
|
+
throw new OmnikitError(
|
|
1189
|
+
"job_id is required for CheckJobStatus",
|
|
1190
|
+
400,
|
|
1191
|
+
"MISSING_JOB_ID"
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return client.makeRequest(
|
|
1195
|
+
`${client.baseUrl}/apps/${client.appId}/jobs/${params.job_id}`,
|
|
1196
|
+
"GET"
|
|
1197
|
+
);
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
return async function(params, asyncOptions) {
|
|
1201
|
+
await client.ensureInitialized();
|
|
1202
|
+
if (methodName === "InvokeLLM") {
|
|
1203
|
+
if (params?.model) {
|
|
1204
|
+
params = { ...params, model: mapLLMModel(params.model) };
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const integrationPackage = client._integrations[packageName];
|
|
1208
|
+
if (!integrationPackage) {
|
|
1209
|
+
throw new OmnikitError(
|
|
1210
|
+
`Integration package '${packageName}' not found`,
|
|
1211
|
+
404,
|
|
1212
|
+
"INTEGRATION_NOT_FOUND"
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
const method = integrationPackage[methodName];
|
|
1216
|
+
if (!method) {
|
|
1217
|
+
throw new OmnikitError(
|
|
1218
|
+
`Integration method '${methodName}' not found in package '${packageName}'`,
|
|
1219
|
+
404,
|
|
1220
|
+
"INTEGRATION_METHOD_NOT_FOUND"
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
const response = await method(params, useServiceToken);
|
|
1224
|
+
if (response && response.async && response.job_id) {
|
|
1225
|
+
if (asyncOptions?.returnJobId) {
|
|
1226
|
+
return response;
|
|
1227
|
+
}
|
|
1228
|
+
return client.pollJobUntilComplete(response.job_id, asyncOptions);
|
|
1229
|
+
}
|
|
1230
|
+
return response;
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
return void 0;
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
return target[packageName];
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Ensure SDK is initialized (auto-initializes once on first call)
|
|
1243
|
+
*/
|
|
1244
|
+
async ensureInitialized() {
|
|
1245
|
+
if (this.initialized) return;
|
|
1246
|
+
if (this.initPromise) {
|
|
1247
|
+
return this.initPromise;
|
|
1248
|
+
}
|
|
1249
|
+
this.initPromise = this.initialize();
|
|
1250
|
+
await this.initPromise;
|
|
1251
|
+
this.initPromise = null;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Initialize SDK by fetching app schema and integration endpoints
|
|
1255
|
+
* This happens automatically on first use - no need to call explicitly
|
|
1256
|
+
*/
|
|
1257
|
+
async initialize() {
|
|
1258
|
+
if (this.initialized) return;
|
|
1259
|
+
try {
|
|
1260
|
+
const [appSchema, integrationSchema] = await Promise.all([
|
|
1261
|
+
this.fetchAppSchema(),
|
|
1262
|
+
this.fetchIntegrationSchema()
|
|
1263
|
+
]);
|
|
1264
|
+
if (appSchema.app) {
|
|
1265
|
+
this.updateMetadataCache({
|
|
1266
|
+
name: appSchema.app.name,
|
|
1267
|
+
logoUrl: appSchema.app.logo_url,
|
|
1268
|
+
thumbnailUrl: appSchema.app.thumbnail_url,
|
|
1269
|
+
visibility: appSchema.app.visibility,
|
|
1270
|
+
platformAuthProviders: appSchema.app.platform_auth_providers
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
if (appSchema.entities) {
|
|
1274
|
+
this.createCollections(appSchema.entities);
|
|
1275
|
+
}
|
|
1276
|
+
if (integrationSchema && "services" in integrationSchema) {
|
|
1277
|
+
this.createServicesFromSchema(integrationSchema);
|
|
1278
|
+
} else if (integrationSchema && "installed_packages" in integrationSchema) {
|
|
1279
|
+
this.createIntegrationsFromSchema(integrationSchema);
|
|
1280
|
+
}
|
|
1281
|
+
this.initialized = true;
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
console.error("Failed to initialize Omnikit SDK:", error);
|
|
1284
|
+
throw new OmnikitError(
|
|
1285
|
+
`SDK initialization failed: ${error.message}`,
|
|
1286
|
+
500,
|
|
1287
|
+
"INIT_FAILED",
|
|
1288
|
+
error
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Fetch app schema from backend
|
|
1294
|
+
*/
|
|
1295
|
+
async fetchAppSchema() {
|
|
1296
|
+
const response = await fetch(`${this.baseUrl}/apps/${this.appId}`);
|
|
1297
|
+
if (!response.ok) {
|
|
1298
|
+
throw new Error(`Failed to fetch app schema: ${response.statusText}`);
|
|
1299
|
+
}
|
|
1300
|
+
return await response.json();
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Fetch integration schema from backend
|
|
1304
|
+
* Returns ServicesSchema (new flat format) or IntegrationSchema (legacy)
|
|
1305
|
+
*/
|
|
1306
|
+
async fetchIntegrationSchema() {
|
|
1307
|
+
try {
|
|
1308
|
+
const response = await fetch(
|
|
1309
|
+
`${this.baseUrl}/apps/${this.appId}/services`
|
|
1310
|
+
);
|
|
1311
|
+
if (!response.ok) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
return await response.json();
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Create collection classes from schema
|
|
1321
|
+
*/
|
|
1322
|
+
createCollections(collectionsData) {
|
|
1323
|
+
const collections = Array.isArray(collectionsData) ? collectionsData.reduce((acc, collection) => ({ ...acc, [collection.name]: collection }), {}) : collectionsData;
|
|
1324
|
+
Object.entries(collections).forEach(([collectionName, collectionDef]) => {
|
|
1325
|
+
this._collections[collectionName.toLowerCase()] = this.createCollectionClass(collectionName, collectionDef);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Create a single collection class with all CRUD operations
|
|
1330
|
+
* Automatically enhances User collection with convenience methods
|
|
1331
|
+
*/
|
|
1332
|
+
createCollectionClass(collectionName, collectionDef) {
|
|
1333
|
+
const client = this;
|
|
1334
|
+
const isUserCollection = collectionName.toLowerCase() === "user";
|
|
1335
|
+
const baseCollection = {
|
|
1336
|
+
// Get single record by ID
|
|
1337
|
+
async get(id) {
|
|
1338
|
+
const response = await client.makeRequest(
|
|
1339
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/${id}`,
|
|
1340
|
+
"GET"
|
|
1341
|
+
);
|
|
1342
|
+
return response;
|
|
1343
|
+
},
|
|
1344
|
+
// List all records with MongoDB/Mongoose-style filtering
|
|
1345
|
+
async list(...args) {
|
|
1346
|
+
const queryParams = new URLSearchParams();
|
|
1347
|
+
let filter;
|
|
1348
|
+
let options;
|
|
1349
|
+
if (args.length === 0) {
|
|
1350
|
+
filter = void 0;
|
|
1351
|
+
options = void 0;
|
|
1352
|
+
} else if (args.length === 1) {
|
|
1353
|
+
const arg = args[0];
|
|
1354
|
+
if (arg && (arg.q !== void 0 || arg.sort !== void 0 || arg.limit !== void 0 || arg.offset !== void 0 || arg._count !== void 0)) {
|
|
1355
|
+
Object.entries(arg).forEach(([key, value]) => {
|
|
1356
|
+
if (value !== void 0 && value !== null) {
|
|
1357
|
+
queryParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
} else {
|
|
1361
|
+
filter = arg;
|
|
1362
|
+
options = void 0;
|
|
1363
|
+
}
|
|
1364
|
+
} else if (args.length === 2) {
|
|
1365
|
+
[filter, options] = args;
|
|
1366
|
+
}
|
|
1367
|
+
if (filter !== void 0 || options !== void 0) {
|
|
1368
|
+
if (filter && Object.keys(filter).length > 0) {
|
|
1369
|
+
queryParams.append("q", JSON.stringify(filter));
|
|
1370
|
+
}
|
|
1371
|
+
if (options) {
|
|
1372
|
+
if (options.sort !== void 0) {
|
|
1373
|
+
let sortString;
|
|
1374
|
+
if (typeof options.sort === "object") {
|
|
1375
|
+
const sortField = Object.keys(options.sort)[0];
|
|
1376
|
+
const sortOrder = options.sort[sortField];
|
|
1377
|
+
sortString = sortOrder === -1 ? `-${sortField}` : sortField;
|
|
1378
|
+
} else {
|
|
1379
|
+
sortString = options.sort;
|
|
1380
|
+
}
|
|
1381
|
+
queryParams.append("sort", sortString);
|
|
1382
|
+
}
|
|
1383
|
+
if (options.limit !== void 0) {
|
|
1384
|
+
queryParams.append("limit", String(options.limit));
|
|
1385
|
+
}
|
|
1386
|
+
if (options.offset !== void 0) {
|
|
1387
|
+
queryParams.append("offset", String(options.offset));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const url = queryParams.toString() ? `${client.baseUrl}/apps/${client.appId}/collections/${collectionName}?${queryParams}` : `${client.baseUrl}/apps/${client.appId}/collections/${collectionName}`;
|
|
1392
|
+
const response = await client.makeRequest(url, "GET");
|
|
1393
|
+
return makeArrayForgiving(Array.isArray(response) ? response : []);
|
|
1394
|
+
},
|
|
1395
|
+
// Filter records by query (alias for list)
|
|
1396
|
+
async filter(...args) {
|
|
1397
|
+
return this.list(...args);
|
|
1398
|
+
},
|
|
1399
|
+
// Find single record matching query
|
|
1400
|
+
async findOne(...args) {
|
|
1401
|
+
if (args.length === 0) {
|
|
1402
|
+
const results = await this.list({}, { limit: 1 });
|
|
1403
|
+
return results[0] || null;
|
|
1404
|
+
} else if (args.length === 1) {
|
|
1405
|
+
const arg = args[0];
|
|
1406
|
+
if (arg && (arg.q !== void 0 || arg._count !== void 0)) {
|
|
1407
|
+
const results = await this.list({ ...arg, limit: 1 });
|
|
1408
|
+
return results[0] || null;
|
|
1409
|
+
} else {
|
|
1410
|
+
const results = await this.list(arg, { limit: 1 });
|
|
1411
|
+
return results[0] || null;
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
const [filter, options] = args;
|
|
1415
|
+
const results = await this.list(filter, { ...options, limit: 1 });
|
|
1416
|
+
return results[0] || null;
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
// Create new record
|
|
1420
|
+
async create(data) {
|
|
1421
|
+
const response = await client.makeRequest(
|
|
1422
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}`,
|
|
1423
|
+
"POST",
|
|
1424
|
+
data
|
|
1425
|
+
);
|
|
1426
|
+
return response;
|
|
1427
|
+
},
|
|
1428
|
+
// Update existing record (overridden for User collection)
|
|
1429
|
+
async update(...args) {
|
|
1430
|
+
if (isUserCollection) {
|
|
1431
|
+
if (args.length === 1) {
|
|
1432
|
+
const response = await client.makeRequest(
|
|
1433
|
+
`${client.baseUrl}/apps/${client.appId}/collections/user/me`,
|
|
1434
|
+
"PUT",
|
|
1435
|
+
args[0]
|
|
1436
|
+
);
|
|
1437
|
+
return response;
|
|
1438
|
+
} else if (args.length === 2) {
|
|
1439
|
+
const [id, data] = args;
|
|
1440
|
+
const response = await client.makeRequest(
|
|
1441
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/${id}`,
|
|
1442
|
+
"PATCH",
|
|
1443
|
+
data
|
|
1444
|
+
);
|
|
1445
|
+
return response;
|
|
1446
|
+
} else {
|
|
1447
|
+
throw new OmnikitError(
|
|
1448
|
+
"User.update() expects 1 argument (current user data) or 2 arguments (id, data)",
|
|
1449
|
+
400,
|
|
1450
|
+
"INVALID_ARGUMENTS"
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
} else {
|
|
1454
|
+
const [id, data] = args;
|
|
1455
|
+
const response = await client.makeRequest(
|
|
1456
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/${id}`,
|
|
1457
|
+
"PATCH",
|
|
1458
|
+
data
|
|
1459
|
+
);
|
|
1460
|
+
return response;
|
|
1461
|
+
}
|
|
1462
|
+
},
|
|
1463
|
+
// Delete record by ID
|
|
1464
|
+
async delete(id) {
|
|
1465
|
+
await client.makeRequest(
|
|
1466
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/${id}`,
|
|
1467
|
+
"DELETE"
|
|
1468
|
+
);
|
|
1469
|
+
return true;
|
|
1470
|
+
},
|
|
1471
|
+
// Count records matching query
|
|
1472
|
+
async count(...args) {
|
|
1473
|
+
const queryParams = new URLSearchParams();
|
|
1474
|
+
if (args.length === 0) {
|
|
1475
|
+
queryParams.append("_count", "true");
|
|
1476
|
+
} else if (args.length === 1) {
|
|
1477
|
+
const arg = args[0];
|
|
1478
|
+
if (arg && (arg.q !== void 0 || arg.sort !== void 0)) {
|
|
1479
|
+
Object.entries(arg).forEach(([key, value]) => {
|
|
1480
|
+
if (value !== void 0 && value !== null) {
|
|
1481
|
+
queryParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
if (arg && Object.keys(arg).length > 0) {
|
|
1486
|
+
queryParams.append("q", JSON.stringify(arg));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
queryParams.append("_count", "true");
|
|
1490
|
+
} else if (args.length === 2) {
|
|
1491
|
+
const [filter] = args;
|
|
1492
|
+
if (filter && Object.keys(filter).length > 0) {
|
|
1493
|
+
queryParams.append("q", JSON.stringify(filter));
|
|
1494
|
+
}
|
|
1495
|
+
queryParams.append("_count", "true");
|
|
1496
|
+
}
|
|
1497
|
+
const response = await client.makeRequest(
|
|
1498
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}?${queryParams}`,
|
|
1499
|
+
"GET"
|
|
1500
|
+
);
|
|
1501
|
+
return response.total || response.count || 0;
|
|
1502
|
+
},
|
|
1503
|
+
// Bulk create multiple records
|
|
1504
|
+
async bulkCreate(items) {
|
|
1505
|
+
const response = await client.makeRequest(
|
|
1506
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/bulk`,
|
|
1507
|
+
"POST",
|
|
1508
|
+
{ items }
|
|
1509
|
+
);
|
|
1510
|
+
return response;
|
|
1511
|
+
},
|
|
1512
|
+
// Bulk delete multiple records by ID
|
|
1513
|
+
async bulkDelete(ids) {
|
|
1514
|
+
const response = await client.makeRequest(
|
|
1515
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/bulk-delete`,
|
|
1516
|
+
"POST",
|
|
1517
|
+
ids
|
|
1518
|
+
);
|
|
1519
|
+
return response;
|
|
1520
|
+
},
|
|
1521
|
+
// Import records from CSV or JSON file
|
|
1522
|
+
async import(file, format) {
|
|
1523
|
+
const formData = new FormData();
|
|
1524
|
+
formData.append("file", file);
|
|
1525
|
+
formData.append("format", format);
|
|
1526
|
+
const response = await client.makeRequestWithFormData(
|
|
1527
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}/import`,
|
|
1528
|
+
formData
|
|
1529
|
+
);
|
|
1530
|
+
return response;
|
|
1531
|
+
},
|
|
1532
|
+
// Delete all records (requires explicit confirmation)
|
|
1533
|
+
async deleteAll(confirm) {
|
|
1534
|
+
if (!confirm) {
|
|
1535
|
+
throw new OmnikitError(
|
|
1536
|
+
"deleteAll requires explicit confirmation. Pass true to confirm.",
|
|
1537
|
+
400,
|
|
1538
|
+
"CONFIRMATION_REQUIRED"
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
const response = await client.makeRequest(
|
|
1542
|
+
`${client.baseUrl}/apps/${client.appId}/collections/${collectionName}?confirm=true`,
|
|
1543
|
+
"DELETE"
|
|
1544
|
+
);
|
|
1545
|
+
return response;
|
|
1546
|
+
},
|
|
1547
|
+
// Backward compatibility: findById → get
|
|
1548
|
+
async findById(id) {
|
|
1549
|
+
console.warn("findById() is deprecated. Use get() instead.");
|
|
1550
|
+
return this.get(id);
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
if (isUserCollection) {
|
|
1554
|
+
baseCollection.me = async () => {
|
|
1555
|
+
const response = await client.makeRequest(
|
|
1556
|
+
`${client.baseUrl}/apps/${client.appId}/collections/user/me`,
|
|
1557
|
+
"GET"
|
|
1558
|
+
);
|
|
1559
|
+
return response;
|
|
1560
|
+
};
|
|
1561
|
+
baseCollection.updateMe = async (data) => {
|
|
1562
|
+
const response = await client.makeRequest(
|
|
1563
|
+
`${client.baseUrl}/apps/${client.appId}/collections/user/me`,
|
|
1564
|
+
"PUT",
|
|
1565
|
+
data
|
|
1566
|
+
);
|
|
1567
|
+
return response;
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
return baseCollection;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Create services from new flat schema (recommended)
|
|
1574
|
+
* Services are exposed as omnikit.services.ServiceName
|
|
1575
|
+
*/
|
|
1576
|
+
createServicesFromSchema(schema) {
|
|
1577
|
+
schema.services.forEach((service) => {
|
|
1578
|
+
this._services[service.name] = this.createServiceMethod(service);
|
|
1579
|
+
});
|
|
1580
|
+
this._integrations["BuiltIn"] = {};
|
|
1581
|
+
schema.services.forEach((service) => {
|
|
1582
|
+
this._integrations["BuiltIn"][service.name] = this._services[service.name];
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Create a service method from ServiceDefinition
|
|
1587
|
+
*/
|
|
1588
|
+
createServiceMethod(service) {
|
|
1589
|
+
const client = this;
|
|
1590
|
+
const { path, method = "POST", path_params } = service;
|
|
1591
|
+
return async (params, useServiceToken) => {
|
|
1592
|
+
let normalizedParams = params;
|
|
1593
|
+
if (params instanceof File) {
|
|
1594
|
+
console.warn("[Omnikit SDK] UploadFile called with File directly. Auto-wrapping to { file }. Please use UploadFile({ file }) for best practice.");
|
|
1595
|
+
normalizedParams = { file: params };
|
|
1596
|
+
}
|
|
1597
|
+
let finalPath = path;
|
|
1598
|
+
const remainingParams = { ...normalizedParams };
|
|
1599
|
+
if (path_params && path_params.length > 0 && normalizedParams) {
|
|
1600
|
+
path_params.forEach((paramName) => {
|
|
1601
|
+
if (normalizedParams[paramName] !== void 0) {
|
|
1602
|
+
finalPath = finalPath.replace(`{${paramName}}`, String(normalizedParams[paramName]));
|
|
1603
|
+
delete remainingParams[paramName];
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
const fullUrl = `${client.baseUrl}/apps/${client.appId}${finalPath}`;
|
|
1608
|
+
if (remainingParams && remainingParams.file instanceof File) {
|
|
1609
|
+
return client.handleFileUpload(remainingParams.file, path, useServiceToken);
|
|
1610
|
+
}
|
|
1611
|
+
const response = await client.makeRequest(
|
|
1612
|
+
fullUrl,
|
|
1613
|
+
method,
|
|
1614
|
+
method === "GET" ? void 0 : remainingParams,
|
|
1615
|
+
{
|
|
1616
|
+
useServiceToken,
|
|
1617
|
+
queryParams: method === "GET" ? remainingParams : void 0
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
return client.normalizeIntegrationResponse(response, path);
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Handle file upload via presigned URL flow
|
|
1625
|
+
* Supports both public and private file uploads based on path
|
|
1626
|
+
*/
|
|
1627
|
+
async handleFileUpload(file, path, useServiceToken) {
|
|
1628
|
+
const isPrivate = path.includes("/files/private");
|
|
1629
|
+
const initEndpoint = isPrivate ? "/services/files/private/init" : "/services/files/init";
|
|
1630
|
+
const completeEndpoint = isPrivate ? "/services/files/private/complete" : "/services/files/complete";
|
|
1631
|
+
const initResponse = await this.makeRequest(
|
|
1632
|
+
`${this.baseUrl}/apps/${this.appId}${initEndpoint}`,
|
|
1633
|
+
"POST",
|
|
1634
|
+
{
|
|
1635
|
+
filename: file.name,
|
|
1636
|
+
content_type: file.type || "application/octet-stream",
|
|
1637
|
+
size: file.size
|
|
1638
|
+
},
|
|
1639
|
+
{ useServiceToken }
|
|
1640
|
+
);
|
|
1641
|
+
const { file_id, upload_url } = initResponse;
|
|
1642
|
+
const uploadResponse = await fetch(upload_url, {
|
|
1643
|
+
method: "PUT",
|
|
1644
|
+
body: file,
|
|
1645
|
+
headers: {
|
|
1646
|
+
"Content-Type": file.type || "application/octet-stream"
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
if (!uploadResponse.ok) {
|
|
1650
|
+
throw new OmnikitError(
|
|
1651
|
+
`Failed to upload file to storage: ${uploadResponse.statusText}`,
|
|
1652
|
+
uploadResponse.status,
|
|
1653
|
+
"UPLOAD_FAILED"
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
const completeResponse = await this.makeRequest(
|
|
1657
|
+
`${this.baseUrl}/apps/${this.appId}${completeEndpoint}/${file_id}`,
|
|
1658
|
+
"POST",
|
|
1659
|
+
null,
|
|
1660
|
+
{ useServiceToken }
|
|
1661
|
+
);
|
|
1662
|
+
if (isPrivate) {
|
|
1663
|
+
return completeResponse;
|
|
1664
|
+
}
|
|
1665
|
+
return this.normalizeIntegrationResponse(completeResponse, path);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* @deprecated Use createServicesFromSchema instead
|
|
1669
|
+
* Create integration methods from legacy backend schema
|
|
1670
|
+
*/
|
|
1671
|
+
createIntegrationsFromSchema(schema) {
|
|
1672
|
+
schema.installed_packages.forEach((pkg) => {
|
|
1673
|
+
const packageName = pkg.package_name;
|
|
1674
|
+
this._integrations[packageName] = {};
|
|
1675
|
+
pkg.endpoints.forEach((endpoint) => {
|
|
1676
|
+
const methodName = this.formatMethodName(endpoint.name);
|
|
1677
|
+
this._integrations[packageName][methodName] = this.createIntegrationMethod(endpoint);
|
|
1678
|
+
this._services[methodName] = this._integrations[packageName][methodName];
|
|
1679
|
+
});
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* @deprecated Backend now returns final method names
|
|
1684
|
+
* Format endpoint name to PascalCase method name (legacy)
|
|
1685
|
+
*/
|
|
1686
|
+
formatMethodName(name) {
|
|
1687
|
+
const methodMap = {
|
|
1688
|
+
"email": "SendEmail",
|
|
1689
|
+
"llm": "InvokeLLM",
|
|
1690
|
+
"files": "UploadFile",
|
|
1691
|
+
"files/private": "UploadPrivateFile",
|
|
1692
|
+
"files/signed-url": "CreateFileSignedUrl",
|
|
1693
|
+
"images": "GenerateImage",
|
|
1694
|
+
"speech": "GenerateSpeech",
|
|
1695
|
+
"video": "GenerateVideo",
|
|
1696
|
+
"video_status": "CheckVideoStatus",
|
|
1697
|
+
"extract-text": "ExtractData",
|
|
1698
|
+
"extract-text_status": "CheckExtractStatus",
|
|
1699
|
+
"sms": "SendSMS"
|
|
1700
|
+
};
|
|
1701
|
+
return methodMap[name] || name.charAt(0).toUpperCase() + name.slice(1);
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Create integration method that calls backend endpoint
|
|
1705
|
+
*/
|
|
1706
|
+
createIntegrationMethod(endpoint) {
|
|
1707
|
+
const client = this;
|
|
1708
|
+
const { path, method = "POST", path_params = [] } = endpoint;
|
|
1709
|
+
return async (params, useServiceToken) => {
|
|
1710
|
+
let normalizedParams = params;
|
|
1711
|
+
if (params instanceof File) {
|
|
1712
|
+
console.warn("[Omnikit SDK] UploadFile called with File directly. Auto-wrapping to { file }. Please use UploadFile({ file }) for best practice.");
|
|
1713
|
+
normalizedParams = { file: params };
|
|
1714
|
+
}
|
|
1715
|
+
let finalPath = path;
|
|
1716
|
+
const remainingParams = { ...normalizedParams };
|
|
1717
|
+
if (path_params && path_params.length > 0 && normalizedParams) {
|
|
1718
|
+
path_params.forEach((paramName) => {
|
|
1719
|
+
if (normalizedParams[paramName] !== void 0) {
|
|
1720
|
+
finalPath = finalPath.replace(`{${paramName}}`, String(normalizedParams[paramName]));
|
|
1721
|
+
delete remainingParams[paramName];
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
const fullUrl = `${client.baseUrl}/apps/${client.appId}${finalPath}`;
|
|
1726
|
+
if (path.includes("/services/files/private") && remainingParams && remainingParams.file instanceof File) {
|
|
1727
|
+
const file = remainingParams.file;
|
|
1728
|
+
try {
|
|
1729
|
+
const initResponse = await client.makeRequest(
|
|
1730
|
+
`${client.baseUrl}/apps/${client.appId}/services/files/private/init`,
|
|
1731
|
+
"POST",
|
|
1732
|
+
{
|
|
1733
|
+
filename: file.name,
|
|
1734
|
+
content_type: file.type || "application/octet-stream",
|
|
1735
|
+
size: file.size
|
|
1736
|
+
},
|
|
1737
|
+
{ useServiceToken }
|
|
1738
|
+
);
|
|
1739
|
+
const { file_id, upload_url, file_uri } = initResponse;
|
|
1740
|
+
const uploadResponse = await fetch(upload_url, {
|
|
1741
|
+
method: "PUT",
|
|
1742
|
+
body: file,
|
|
1743
|
+
headers: {
|
|
1744
|
+
"Content-Type": file.type || "application/octet-stream"
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
if (!uploadResponse.ok) {
|
|
1748
|
+
throw new OmnikitError(
|
|
1749
|
+
`Failed to upload private file to storage: ${uploadResponse.statusText}`,
|
|
1750
|
+
uploadResponse.status,
|
|
1751
|
+
"UPLOAD_FAILED"
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const completeResponse = await client.makeRequest(
|
|
1755
|
+
`${client.baseUrl}/apps/${client.appId}/services/files/private/complete/${file_id}`,
|
|
1756
|
+
"POST",
|
|
1757
|
+
null,
|
|
1758
|
+
{ useServiceToken }
|
|
1759
|
+
);
|
|
1760
|
+
return completeResponse;
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
if (error instanceof OmnikitError) {
|
|
1763
|
+
throw error;
|
|
1764
|
+
}
|
|
1765
|
+
throw new OmnikitError(
|
|
1766
|
+
`Private file upload failed: ${error.message}`,
|
|
1767
|
+
500,
|
|
1768
|
+
"UPLOAD_ERROR",
|
|
1769
|
+
error
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (remainingParams && remainingParams.file instanceof File) {
|
|
1774
|
+
const file = remainingParams.file;
|
|
1775
|
+
try {
|
|
1776
|
+
const initResponse = await client.makeRequest(
|
|
1777
|
+
`${client.baseUrl}/apps/${client.appId}/services/files/init`,
|
|
1778
|
+
"POST",
|
|
1779
|
+
{
|
|
1780
|
+
filename: file.name,
|
|
1781
|
+
content_type: file.type || "application/octet-stream",
|
|
1782
|
+
size: file.size
|
|
1783
|
+
},
|
|
1784
|
+
{ useServiceToken }
|
|
1785
|
+
);
|
|
1786
|
+
const { file_id, upload_url, public_url } = initResponse;
|
|
1787
|
+
const uploadResponse = await fetch(upload_url, {
|
|
1788
|
+
method: "PUT",
|
|
1789
|
+
body: file,
|
|
1790
|
+
headers: {
|
|
1791
|
+
"Content-Type": file.type || "application/octet-stream"
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
if (!uploadResponse.ok) {
|
|
1795
|
+
throw new OmnikitError(
|
|
1796
|
+
`Failed to upload file to storage: ${uploadResponse.statusText}`,
|
|
1797
|
+
uploadResponse.status,
|
|
1798
|
+
"UPLOAD_FAILED"
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
const completeResponse = await client.makeRequest(
|
|
1802
|
+
`${client.baseUrl}/apps/${client.appId}/services/files/complete/${file_id}`,
|
|
1803
|
+
"POST",
|
|
1804
|
+
null,
|
|
1805
|
+
{ useServiceToken }
|
|
1806
|
+
);
|
|
1807
|
+
return client.normalizeIntegrationResponse(completeResponse, path);
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
if (error instanceof OmnikitError) {
|
|
1810
|
+
throw error;
|
|
1811
|
+
}
|
|
1812
|
+
throw new OmnikitError(
|
|
1813
|
+
`File upload failed: ${error.message}`,
|
|
1814
|
+
500,
|
|
1815
|
+
"UPLOAD_ERROR",
|
|
1816
|
+
error
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
const response = await client.makeRequest(
|
|
1821
|
+
fullUrl,
|
|
1822
|
+
method,
|
|
1823
|
+
method === "GET" ? null : remainingParams,
|
|
1824
|
+
{ useServiceToken }
|
|
1825
|
+
);
|
|
1826
|
+
return client.normalizeIntegrationResponse(response, path);
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Normalize integration response to add common aliases for bulletproof access
|
|
1831
|
+
* Prevents runtime errors from AI-generated code using wrong property names
|
|
1832
|
+
*/
|
|
1833
|
+
normalizeIntegrationResponse(response, path) {
|
|
1834
|
+
if (!response || typeof response !== "object") {
|
|
1835
|
+
return response;
|
|
1836
|
+
}
|
|
1837
|
+
const normalized = { ...response };
|
|
1838
|
+
if (path.includes("/services/llm") && "result" in normalized && !("content" in normalized)) {
|
|
1839
|
+
Object.defineProperty(normalized, "content", {
|
|
1840
|
+
get() {
|
|
1841
|
+
return this.result;
|
|
1842
|
+
},
|
|
1843
|
+
enumerable: false
|
|
1844
|
+
// Don't show in JSON.stringify or Object.keys
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
if (path.includes("/services/files") && "file_url" in normalized && !("url" in normalized)) {
|
|
1848
|
+
Object.defineProperty(normalized, "url", {
|
|
1849
|
+
get() {
|
|
1850
|
+
return this.file_url;
|
|
1851
|
+
},
|
|
1852
|
+
enumerable: false
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
if (path.includes("/services/speech") && "url" in normalized && !("audio_url" in normalized)) {
|
|
1856
|
+
Object.defineProperty(normalized, "audio_url", {
|
|
1857
|
+
get() {
|
|
1858
|
+
return this.url;
|
|
1859
|
+
},
|
|
1860
|
+
enumerable: false
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
if (path.includes("/services/extract") && "results" in normalized && Array.isArray(normalized.results)) {
|
|
1864
|
+
if (!("text" in normalized)) {
|
|
1865
|
+
Object.defineProperty(normalized, "text", {
|
|
1866
|
+
get() {
|
|
1867
|
+
const firstSuccess = this.results?.find((r) => r.success);
|
|
1868
|
+
return firstSuccess?.text || "";
|
|
1869
|
+
},
|
|
1870
|
+
enumerable: false
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return normalized;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* HTTP request helper with JSON
|
|
1878
|
+
*/
|
|
1879
|
+
async makeRequest(url, method = "GET", data = null, options) {
|
|
1880
|
+
let finalUrl = url;
|
|
1881
|
+
if (options?.queryParams && Object.keys(options.queryParams).length > 0) {
|
|
1882
|
+
const searchParams = new URLSearchParams();
|
|
1883
|
+
Object.entries(options.queryParams).forEach(([key, value]) => {
|
|
1884
|
+
if (value !== void 0 && value !== null) {
|
|
1885
|
+
searchParams.append(key, String(value));
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
const queryString = searchParams.toString();
|
|
1889
|
+
if (queryString) {
|
|
1890
|
+
finalUrl = url.includes("?") ? `${url}&${queryString}` : `${url}?${queryString}`;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const fetchOptions = {
|
|
1894
|
+
method,
|
|
1895
|
+
headers: {
|
|
1896
|
+
"Content-Type": "application/json"
|
|
1897
|
+
}
|
|
1898
|
+
// Removed credentials: 'include' to prevent sending cookies (switching to pure Bearer token)
|
|
1899
|
+
};
|
|
1900
|
+
const userToken = this.getAuthToken();
|
|
1901
|
+
if (userToken) {
|
|
1902
|
+
fetchOptions.headers.Authorization = `Bearer ${userToken}`;
|
|
1903
|
+
}
|
|
1904
|
+
if (this._serviceToken) {
|
|
1905
|
+
fetchOptions.headers["X-Service-Token"] = this._serviceToken;
|
|
1906
|
+
}
|
|
1907
|
+
if (this._apiKey) {
|
|
1908
|
+
fetchOptions.headers["X-API-Key"] = this._apiKey;
|
|
1909
|
+
}
|
|
1910
|
+
if (options?.headers) {
|
|
1911
|
+
Object.assign(fetchOptions.headers, options.headers);
|
|
1912
|
+
}
|
|
1913
|
+
if (data && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
1914
|
+
fetchOptions.body = JSON.stringify(data);
|
|
1915
|
+
}
|
|
1916
|
+
const response = await fetch(finalUrl, fetchOptions);
|
|
1917
|
+
if (response.status === 401) {
|
|
1918
|
+
this.clearAuthToken();
|
|
1919
|
+
throw new OmnikitError(
|
|
1920
|
+
"Authentication required or token expired",
|
|
1921
|
+
401,
|
|
1922
|
+
"UNAUTHORIZED"
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
if (!response.ok) {
|
|
1926
|
+
const error = await response.json().catch(() => ({}));
|
|
1927
|
+
throw new OmnikitError(
|
|
1928
|
+
error.detail || error.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
1929
|
+
response.status,
|
|
1930
|
+
error.code || "HTTP_ERROR",
|
|
1931
|
+
error
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
if (response.status === 204 || method === "DELETE") {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
return await response.json();
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* HTTP request helper with FormData (for file uploads)
|
|
1941
|
+
*/
|
|
1942
|
+
async makeRequestWithFormData(url, formData, useServiceToken) {
|
|
1943
|
+
const fetchOptions = {
|
|
1944
|
+
method: "POST",
|
|
1945
|
+
body: formData,
|
|
1946
|
+
headers: {}
|
|
1947
|
+
// Removed credentials: 'include'
|
|
1948
|
+
};
|
|
1949
|
+
const userToken = this.getAuthToken();
|
|
1950
|
+
if (userToken) {
|
|
1951
|
+
fetchOptions.headers.Authorization = `Bearer ${userToken}`;
|
|
1952
|
+
}
|
|
1953
|
+
if (this._serviceToken) {
|
|
1954
|
+
fetchOptions.headers["X-Service-Token"] = this._serviceToken;
|
|
1955
|
+
}
|
|
1956
|
+
if (this._apiKey) {
|
|
1957
|
+
fetchOptions.headers["X-API-Key"] = this._apiKey;
|
|
1958
|
+
}
|
|
1959
|
+
const response = await fetch(url, fetchOptions);
|
|
1960
|
+
if (!response.ok) {
|
|
1961
|
+
const error = await response.json().catch(() => ({}));
|
|
1962
|
+
throw new OmnikitError(
|
|
1963
|
+
error.detail || error.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
1964
|
+
response.status,
|
|
1965
|
+
error.code || "HTTP_ERROR",
|
|
1966
|
+
error
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
return await response.json();
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Get app metadata (for internal use)
|
|
1973
|
+
*/
|
|
1974
|
+
async getAppMetadata() {
|
|
1975
|
+
const response = await this.makeRequest(
|
|
1976
|
+
`${this.baseUrl}/apps/${this.appId}`,
|
|
1977
|
+
"GET"
|
|
1978
|
+
);
|
|
1979
|
+
if (response?.app || response?.name) {
|
|
1980
|
+
const app = response.app || response;
|
|
1981
|
+
this.updateMetadataCache({
|
|
1982
|
+
name: app.name,
|
|
1983
|
+
logoUrl: app.logo_url,
|
|
1984
|
+
thumbnailUrl: app.thumbnail_url
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
return response;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Set auth token
|
|
1991
|
+
*/
|
|
1992
|
+
setAuthToken(token) {
|
|
1993
|
+
this.userToken = token;
|
|
1994
|
+
saveAccessToken(token);
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Get current auth token
|
|
1998
|
+
*/
|
|
1999
|
+
getAuthToken() {
|
|
2000
|
+
return this.userToken;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Clear auth token
|
|
2004
|
+
*/
|
|
2005
|
+
clearAuthToken() {
|
|
2006
|
+
this.userToken = null;
|
|
2007
|
+
removeAccessToken();
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Create a live voice session for real-time voice conversation with AI.
|
|
2011
|
+
*
|
|
2012
|
+
* The session manages WebSocket communication, microphone capture, and audio playback.
|
|
2013
|
+
*
|
|
2014
|
+
* @example
|
|
2015
|
+
* ```typescript
|
|
2016
|
+
* const session = omnikit.createLiveVoiceSession({
|
|
2017
|
+
* systemInstruction: 'You are a helpful assistant.',
|
|
2018
|
+
* voice: 'Puck',
|
|
2019
|
+
* onTranscript: (text, role) => console.log(`${role}: ${text}`),
|
|
2020
|
+
* onStatusChange: (status) => console.log(`Status: ${status}`),
|
|
2021
|
+
* onError: (error) => console.error(error),
|
|
2022
|
+
* });
|
|
2023
|
+
*
|
|
2024
|
+
* await session.start();
|
|
2025
|
+
* // ... user speaks, AI responds ...
|
|
2026
|
+
* await session.stop();
|
|
2027
|
+
* ```
|
|
2028
|
+
*
|
|
2029
|
+
* @param config - Optional configuration for the voice session
|
|
2030
|
+
* @returns A LiveVoiceSession object to control the session
|
|
2031
|
+
*/
|
|
2032
|
+
createLiveVoiceSession(config) {
|
|
2033
|
+
return new LiveVoiceSessionImpl(
|
|
2034
|
+
this.baseUrl,
|
|
2035
|
+
this.appId,
|
|
2036
|
+
this.getAuthToken(),
|
|
2037
|
+
config
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Invoke a backend function by name.
|
|
2042
|
+
*
|
|
2043
|
+
* Backend functions are deployed to Supabase Edge Functions and can be invoked
|
|
2044
|
+
* from the frontend using this method.
|
|
2045
|
+
*
|
|
2046
|
+
* @example
|
|
2047
|
+
* ```typescript
|
|
2048
|
+
* // Invoke a function
|
|
2049
|
+
* const result = await omnikit.invokeFunction('processPayment', {
|
|
2050
|
+
* amount: 100,
|
|
2051
|
+
* userId: 'abc123'
|
|
2052
|
+
* });
|
|
2053
|
+
* console.log(result.transactionId); // Direct access to function response
|
|
2054
|
+
* ```
|
|
2055
|
+
*
|
|
2056
|
+
* @param functionName - Name of the function to invoke (matches filename without .js)
|
|
2057
|
+
* @param body - Optional request body to send to the function
|
|
2058
|
+
* @returns Response data from the function (unwrapped)
|
|
2059
|
+
* @throws Error if the function invocation fails
|
|
2060
|
+
*/
|
|
2061
|
+
async invokeFunction(functionName, body) {
|
|
2062
|
+
await this.ensureInitialized();
|
|
2063
|
+
const response = await this.makeRequest(
|
|
2064
|
+
`${this.baseUrl}/apps/${this.appId}/functions/invoke/${functionName}`,
|
|
2065
|
+
"POST",
|
|
2066
|
+
{ body }
|
|
2067
|
+
);
|
|
2068
|
+
if (response && typeof response === "object") {
|
|
2069
|
+
if (response.success === false && response.error) {
|
|
2070
|
+
throw new Error(response.error);
|
|
2071
|
+
}
|
|
2072
|
+
if ("data" in response) {
|
|
2073
|
+
return response.data;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return response;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
function createClient(config) {
|
|
2080
|
+
return new APIClient(config);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
export { APIClient, LiveVoiceSessionImpl, OmnikitError, cleanTokenFromUrl, createClient, getAccessToken, isTokenInUrl, removeAccessToken, saveAccessToken, setAccessToken };
|
|
2084
|
+
//# sourceMappingURL=index.mjs.map
|
|
2085
|
+
//# sourceMappingURL=index.mjs.map
|