@blinkdotnew/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/index.d.mts +563 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +2230 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2224 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ../core/src/types.ts
|
|
4
|
+
var BlinkError = class extends Error {
|
|
5
|
+
constructor(message, code, status, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.details = details;
|
|
10
|
+
this.name = "BlinkError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var BlinkAuthError = class extends BlinkError {
|
|
14
|
+
constructor(message, details) {
|
|
15
|
+
super(message, "AUTH_ERROR", 401, details);
|
|
16
|
+
this.name = "BlinkAuthError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var BlinkNetworkError = class extends BlinkError {
|
|
20
|
+
constructor(message, status, details) {
|
|
21
|
+
super(message, "NETWORK_ERROR", status, details);
|
|
22
|
+
this.name = "BlinkNetworkError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var BlinkValidationError = class extends BlinkError {
|
|
26
|
+
constructor(message, details) {
|
|
27
|
+
super(message, "VALIDATION_ERROR", 400, details);
|
|
28
|
+
this.name = "BlinkValidationError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var BlinkStorageError = class extends BlinkError {
|
|
32
|
+
constructor(message, status, details) {
|
|
33
|
+
super(message, "STORAGE_ERROR", status, details);
|
|
34
|
+
this.name = "BlinkStorageError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var BlinkAIError = class extends BlinkError {
|
|
38
|
+
constructor(message, status, details) {
|
|
39
|
+
super(message, "AI_ERROR", status, details);
|
|
40
|
+
this.name = "BlinkAIError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ../core/src/query-builder.ts
|
|
45
|
+
function buildFilterQuery(condition) {
|
|
46
|
+
if (!condition) return "";
|
|
47
|
+
if ("AND" in condition) {
|
|
48
|
+
const andConditions = condition.AND?.map(buildFilterQuery).filter(Boolean) || [];
|
|
49
|
+
return andConditions.length > 0 ? `and=(${andConditions.join(",")})` : "";
|
|
50
|
+
}
|
|
51
|
+
if ("OR" in condition) {
|
|
52
|
+
const orConditions = condition.OR?.map(buildFilterQuery).filter(Boolean) || [];
|
|
53
|
+
return orConditions.length > 0 ? `or=(${orConditions.join(",")})` : "";
|
|
54
|
+
}
|
|
55
|
+
const params = [];
|
|
56
|
+
for (const [field, value] of Object.entries(condition)) {
|
|
57
|
+
if (value === void 0 || value === null) continue;
|
|
58
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
59
|
+
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
60
|
+
const param = buildOperatorQuery(field, operator, operatorValue);
|
|
61
|
+
if (param) params.push(param);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
params.push(`${field}=eq.${encodeQueryValue(value)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return params.join("&");
|
|
68
|
+
}
|
|
69
|
+
function buildOperatorQuery(field, operator, value) {
|
|
70
|
+
switch (operator) {
|
|
71
|
+
case "eq":
|
|
72
|
+
return `${field}=eq.${encodeQueryValue(value)}`;
|
|
73
|
+
case "neq":
|
|
74
|
+
return `${field}=neq.${encodeQueryValue(value)}`;
|
|
75
|
+
case "gt":
|
|
76
|
+
return `${field}=gt.${encodeQueryValue(value)}`;
|
|
77
|
+
case "gte":
|
|
78
|
+
return `${field}=gte.${encodeQueryValue(value)}`;
|
|
79
|
+
case "lt":
|
|
80
|
+
return `${field}=lt.${encodeQueryValue(value)}`;
|
|
81
|
+
case "lte":
|
|
82
|
+
return `${field}=lte.${encodeQueryValue(value)}`;
|
|
83
|
+
case "like":
|
|
84
|
+
return `${field}=like.${encodeQueryValue(value)}`;
|
|
85
|
+
case "ilike":
|
|
86
|
+
return `${field}=ilike.${encodeQueryValue(value)}`;
|
|
87
|
+
case "is":
|
|
88
|
+
return `${field}=is.${value === null ? "null" : encodeQueryValue(value)}`;
|
|
89
|
+
case "not":
|
|
90
|
+
return `${field}=not.${encodeQueryValue(value)}`;
|
|
91
|
+
case "in":
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
const values = value.map(encodeQueryValue).join(",");
|
|
94
|
+
return `${field}=in.(${values})`;
|
|
95
|
+
}
|
|
96
|
+
return "";
|
|
97
|
+
case "not_in":
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
const values = value.map(encodeQueryValue).join(",");
|
|
100
|
+
return `${field}=not.in.(${values})`;
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
103
|
+
default:
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function encodeQueryValue(value) {
|
|
108
|
+
if (value === null) return "null";
|
|
109
|
+
if (typeof value === "boolean") {
|
|
110
|
+
return value ? "1" : "0";
|
|
111
|
+
}
|
|
112
|
+
if (typeof value === "number") return value.toString();
|
|
113
|
+
return encodeURIComponent(String(value));
|
|
114
|
+
}
|
|
115
|
+
function buildQuery(options = {}) {
|
|
116
|
+
const params = {};
|
|
117
|
+
if (options.select && options.select.length > 0) {
|
|
118
|
+
params.select = options.select.join(",");
|
|
119
|
+
} else {
|
|
120
|
+
params.select = "*";
|
|
121
|
+
}
|
|
122
|
+
if (options.where) {
|
|
123
|
+
const filterQuery = buildFilterQuery(options.where);
|
|
124
|
+
if (filterQuery) {
|
|
125
|
+
const filterParams = filterQuery.split("&");
|
|
126
|
+
for (const param of filterParams) {
|
|
127
|
+
const [key, value] = param.split("=", 2);
|
|
128
|
+
if (key && value) {
|
|
129
|
+
params[key] = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (options.orderBy) {
|
|
135
|
+
if (typeof options.orderBy === "string") {
|
|
136
|
+
params.order = options.orderBy;
|
|
137
|
+
} else {
|
|
138
|
+
const orderClauses = Object.entries(options.orderBy).map(([field, direction]) => `${field}.${direction}`);
|
|
139
|
+
params.order = orderClauses.join(",");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (options.limit !== void 0) {
|
|
143
|
+
params.limit = options.limit.toString();
|
|
144
|
+
}
|
|
145
|
+
if (options.offset !== void 0) {
|
|
146
|
+
params.offset = options.offset.toString();
|
|
147
|
+
}
|
|
148
|
+
if (options.cursor) {
|
|
149
|
+
params.cursor = options.cursor;
|
|
150
|
+
}
|
|
151
|
+
return params;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ../core/src/http-client.ts
|
|
155
|
+
var HttpClient = class {
|
|
156
|
+
authUrl = "https://blink.new";
|
|
157
|
+
coreUrl = "https://core.blink.new";
|
|
158
|
+
projectId;
|
|
159
|
+
getToken;
|
|
160
|
+
getValidToken;
|
|
161
|
+
constructor(config, getToken, getValidToken) {
|
|
162
|
+
this.projectId = config.projectId;
|
|
163
|
+
this.getToken = getToken;
|
|
164
|
+
this.getValidToken = getValidToken;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Make an authenticated request to the Blink API
|
|
168
|
+
*/
|
|
169
|
+
async request(path, options = {}) {
|
|
170
|
+
const url = this.buildUrl(path, options.searchParams);
|
|
171
|
+
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
|
|
172
|
+
const headers = {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
...options.headers
|
|
175
|
+
};
|
|
176
|
+
if (token) {
|
|
177
|
+
headers.Authorization = `Bearer ${token}`;
|
|
178
|
+
}
|
|
179
|
+
const requestInit = {
|
|
180
|
+
method: options.method || "GET",
|
|
181
|
+
headers,
|
|
182
|
+
signal: options.signal
|
|
183
|
+
};
|
|
184
|
+
if (options.body && options.method !== "GET") {
|
|
185
|
+
requestInit.body = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(url, requestInit);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
await this.handleErrorResponse(response);
|
|
191
|
+
}
|
|
192
|
+
const data = await this.parseResponse(response);
|
|
193
|
+
return {
|
|
194
|
+
data,
|
|
195
|
+
status: response.status,
|
|
196
|
+
headers: response.headers
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error instanceof BlinkError) {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
throw new BlinkNetworkError(
|
|
203
|
+
`Network request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
204
|
+
0,
|
|
205
|
+
{ originalError: error }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* GET request
|
|
211
|
+
*/
|
|
212
|
+
async get(path, searchParams) {
|
|
213
|
+
return this.request(path, { method: "GET", searchParams });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* POST request
|
|
217
|
+
*/
|
|
218
|
+
async post(path, body, headers) {
|
|
219
|
+
return this.request(path, { method: "POST", body, headers });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* PATCH request
|
|
223
|
+
*/
|
|
224
|
+
async patch(path, body, headers) {
|
|
225
|
+
return this.request(path, { method: "PATCH", body, headers });
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* DELETE request
|
|
229
|
+
*/
|
|
230
|
+
async delete(path, searchParams) {
|
|
231
|
+
return this.request(path, { method: "DELETE", searchParams });
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Database-specific requests
|
|
235
|
+
*/
|
|
236
|
+
// Table operations (PostgREST-compatible)
|
|
237
|
+
async dbGet(table, searchParams) {
|
|
238
|
+
return this.get(`/api/db/${this.projectId}/rest/v1/${table}`, searchParams);
|
|
239
|
+
}
|
|
240
|
+
async dbPost(table, body, options = {}) {
|
|
241
|
+
const headers = {};
|
|
242
|
+
if (options.returning) {
|
|
243
|
+
headers.Prefer = "return=representation";
|
|
244
|
+
}
|
|
245
|
+
return this.post(`/api/db/${this.projectId}/rest/v1/${table}`, body, headers);
|
|
246
|
+
}
|
|
247
|
+
async dbPatch(table, body, searchParams, options = {}) {
|
|
248
|
+
const headers = {};
|
|
249
|
+
if (options.returning) {
|
|
250
|
+
headers.Prefer = "return=representation";
|
|
251
|
+
}
|
|
252
|
+
return this.request(`/api/db/${this.projectId}/rest/v1/${table}`, {
|
|
253
|
+
method: "PATCH",
|
|
254
|
+
body,
|
|
255
|
+
headers,
|
|
256
|
+
searchParams
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async dbDelete(table, searchParams, options = {}) {
|
|
260
|
+
const headers = {};
|
|
261
|
+
if (options.returning) {
|
|
262
|
+
headers.Prefer = "return=representation";
|
|
263
|
+
}
|
|
264
|
+
return this.request(`/api/db/${this.projectId}/rest/v1/${table}`, {
|
|
265
|
+
method: "DELETE",
|
|
266
|
+
headers,
|
|
267
|
+
searchParams
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Raw SQL operations
|
|
271
|
+
async dbSql(query, params) {
|
|
272
|
+
return this.post(`/api/db/${this.projectId}/sql`, { query, params });
|
|
273
|
+
}
|
|
274
|
+
// Batch SQL operations
|
|
275
|
+
async dbBatch(statements, mode = "write") {
|
|
276
|
+
return this.post(`/api/db/${this.projectId}/batch`, { statements, mode });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Upload file with progress tracking
|
|
280
|
+
*/
|
|
281
|
+
async uploadFile(path, file, filePath, options = {}) {
|
|
282
|
+
const url = this.buildUrl(path);
|
|
283
|
+
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
|
|
284
|
+
const formData = new FormData();
|
|
285
|
+
if (file instanceof File) {
|
|
286
|
+
formData.append("file", file);
|
|
287
|
+
} else if (file instanceof Blob) {
|
|
288
|
+
formData.append("file", file);
|
|
289
|
+
} else if (typeof Buffer !== "undefined" && file instanceof Buffer) {
|
|
290
|
+
const blob = new Blob([file]);
|
|
291
|
+
formData.append("file", blob);
|
|
292
|
+
} else {
|
|
293
|
+
throw new BlinkValidationError("Unsupported file type");
|
|
294
|
+
}
|
|
295
|
+
formData.append("path", filePath);
|
|
296
|
+
if (options.upsert !== void 0) {
|
|
297
|
+
formData.append("options", JSON.stringify({ upsert: options.upsert }));
|
|
298
|
+
}
|
|
299
|
+
const headers = {};
|
|
300
|
+
if (token) {
|
|
301
|
+
headers.Authorization = `Bearer ${token}`;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
if (typeof XMLHttpRequest !== "undefined" && options.onProgress) {
|
|
305
|
+
return this.uploadWithProgress(url, formData, headers, options.onProgress);
|
|
306
|
+
}
|
|
307
|
+
const response = await fetch(url, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers,
|
|
310
|
+
body: formData
|
|
311
|
+
});
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
await this.handleErrorResponse(response);
|
|
314
|
+
}
|
|
315
|
+
const data = await this.parseResponse(response);
|
|
316
|
+
return {
|
|
317
|
+
data,
|
|
318
|
+
status: response.status,
|
|
319
|
+
headers: response.headers
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof BlinkError) {
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
throw new BlinkNetworkError(
|
|
326
|
+
`File upload failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
327
|
+
0,
|
|
328
|
+
{ originalError: error }
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Upload with progress tracking using XMLHttpRequest
|
|
334
|
+
*/
|
|
335
|
+
uploadWithProgress(url, formData, headers, onProgress) {
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
const xhr = new XMLHttpRequest();
|
|
338
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
339
|
+
if (event.lengthComputable) {
|
|
340
|
+
const percent = Math.round(event.loaded / event.total * 100);
|
|
341
|
+
onProgress(percent);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
xhr.addEventListener("load", async () => {
|
|
345
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
346
|
+
try {
|
|
347
|
+
const data = JSON.parse(xhr.responseText);
|
|
348
|
+
resolve({
|
|
349
|
+
data,
|
|
350
|
+
status: xhr.status,
|
|
351
|
+
headers: new Headers()
|
|
352
|
+
// XMLHttpRequest doesn't provide easy access to response headers
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
reject(new BlinkNetworkError("Failed to parse response", xhr.status));
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
try {
|
|
359
|
+
const errorData = JSON.parse(xhr.responseText);
|
|
360
|
+
const message = errorData.error?.message || errorData.message || `HTTP ${xhr.status}`;
|
|
361
|
+
switch (xhr.status) {
|
|
362
|
+
case 401:
|
|
363
|
+
reject(new BlinkAuthError(message, errorData));
|
|
364
|
+
break;
|
|
365
|
+
case 400:
|
|
366
|
+
reject(new BlinkValidationError(message, errorData));
|
|
367
|
+
break;
|
|
368
|
+
default:
|
|
369
|
+
reject(new BlinkNetworkError(message, xhr.status, errorData));
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
reject(new BlinkNetworkError(`HTTP ${xhr.status}`, xhr.status));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
xhr.addEventListener("error", () => {
|
|
377
|
+
reject(new BlinkNetworkError("Network error during file upload"));
|
|
378
|
+
});
|
|
379
|
+
xhr.open("POST", url);
|
|
380
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
381
|
+
xhr.setRequestHeader(key, value);
|
|
382
|
+
});
|
|
383
|
+
xhr.send(formData);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* AI-specific requests
|
|
388
|
+
*/
|
|
389
|
+
async aiText(prompt, options = {}) {
|
|
390
|
+
const { signal, ...body } = options;
|
|
391
|
+
const requestBody = { ...body };
|
|
392
|
+
if (prompt) {
|
|
393
|
+
requestBody.prompt = prompt;
|
|
394
|
+
}
|
|
395
|
+
return this.request(`/api/ai/${this.projectId}/text`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
body: requestBody,
|
|
398
|
+
signal
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Stream AI text generation with Vercel AI SDK data stream format
|
|
403
|
+
*/
|
|
404
|
+
async streamAiText(prompt, options = {}, onChunk) {
|
|
405
|
+
const url = this.buildUrl(`/api/ai/${this.projectId}/text`);
|
|
406
|
+
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
|
|
407
|
+
const headers = {
|
|
408
|
+
"Content-Type": "application/json"
|
|
409
|
+
};
|
|
410
|
+
if (token) {
|
|
411
|
+
headers.Authorization = `Bearer ${token}`;
|
|
412
|
+
}
|
|
413
|
+
const body = {
|
|
414
|
+
prompt,
|
|
415
|
+
stream: true,
|
|
416
|
+
...options
|
|
417
|
+
};
|
|
418
|
+
const { signal: _signal, ...jsonBody } = body;
|
|
419
|
+
try {
|
|
420
|
+
const response = await fetch(url, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers,
|
|
423
|
+
body: JSON.stringify(jsonBody),
|
|
424
|
+
signal: options.signal
|
|
425
|
+
});
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
await this.handleErrorResponse(response);
|
|
428
|
+
}
|
|
429
|
+
if (!response.body) {
|
|
430
|
+
throw new BlinkNetworkError("No response body for streaming");
|
|
431
|
+
}
|
|
432
|
+
return this.parseDataStream(response.body, onChunk);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof BlinkError) {
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
throw new BlinkNetworkError(
|
|
438
|
+
`Streaming request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
439
|
+
0,
|
|
440
|
+
{ originalError: error }
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async aiObject(prompt, options = {}) {
|
|
445
|
+
const { signal, ...body } = options;
|
|
446
|
+
const requestBody = { ...body };
|
|
447
|
+
if (prompt) {
|
|
448
|
+
requestBody.prompt = prompt;
|
|
449
|
+
}
|
|
450
|
+
return this.request(`/api/ai/${this.projectId}/object`, {
|
|
451
|
+
method: "POST",
|
|
452
|
+
body: requestBody,
|
|
453
|
+
signal
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Stream AI object generation with Vercel AI SDK data stream format
|
|
458
|
+
*/
|
|
459
|
+
async streamAiObject(prompt, options = {}, onPartial) {
|
|
460
|
+
const url = this.buildUrl(`/api/ai/${this.projectId}/object`);
|
|
461
|
+
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
|
|
462
|
+
const headers = {
|
|
463
|
+
"Content-Type": "application/json"
|
|
464
|
+
};
|
|
465
|
+
if (token) {
|
|
466
|
+
headers.Authorization = `Bearer ${token}`;
|
|
467
|
+
}
|
|
468
|
+
const body = {
|
|
469
|
+
prompt,
|
|
470
|
+
stream: true,
|
|
471
|
+
...options
|
|
472
|
+
};
|
|
473
|
+
const { signal: _signal2, ...jsonBody2 } = body;
|
|
474
|
+
try {
|
|
475
|
+
const response = await fetch(url, {
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers,
|
|
478
|
+
body: JSON.stringify(jsonBody2),
|
|
479
|
+
signal: options.signal
|
|
480
|
+
});
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
await this.handleErrorResponse(response);
|
|
483
|
+
}
|
|
484
|
+
if (!response.body) {
|
|
485
|
+
throw new BlinkNetworkError("No response body for streaming");
|
|
486
|
+
}
|
|
487
|
+
return this.parseDataStream(response.body, void 0, onPartial);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error instanceof BlinkError) {
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
throw new BlinkNetworkError(
|
|
493
|
+
`Streaming request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
494
|
+
0,
|
|
495
|
+
{ originalError: error }
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async aiImage(prompt, options = {}) {
|
|
500
|
+
const { signal, ...body } = options;
|
|
501
|
+
return this.request(`/api/ai/${this.projectId}/image`, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
body: {
|
|
504
|
+
prompt,
|
|
505
|
+
...body
|
|
506
|
+
},
|
|
507
|
+
signal
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async aiSpeech(text, options = {}) {
|
|
511
|
+
const { signal, ...body } = options;
|
|
512
|
+
return this.request(`/api/ai/${this.projectId}/speech`, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body: {
|
|
515
|
+
text,
|
|
516
|
+
...body
|
|
517
|
+
},
|
|
518
|
+
signal
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
async aiTranscribe(audio, options = {}) {
|
|
522
|
+
const { signal, ...body } = options;
|
|
523
|
+
return this.request(`/api/ai/${this.projectId}/transcribe`, {
|
|
524
|
+
method: "POST",
|
|
525
|
+
body: {
|
|
526
|
+
audio,
|
|
527
|
+
...body
|
|
528
|
+
},
|
|
529
|
+
signal
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Private helper methods
|
|
534
|
+
*/
|
|
535
|
+
buildUrl(path, searchParams) {
|
|
536
|
+
const baseUrl = path.includes("/api/auth/") ? this.authUrl : this.coreUrl;
|
|
537
|
+
const url = new URL(path, baseUrl);
|
|
538
|
+
if (searchParams) {
|
|
539
|
+
Object.entries(searchParams).forEach(([key, value]) => {
|
|
540
|
+
url.searchParams.set(key, value);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return url.toString();
|
|
544
|
+
}
|
|
545
|
+
async parseResponse(response) {
|
|
546
|
+
const contentType = response.headers.get("content-type");
|
|
547
|
+
if (contentType?.includes("application/json")) {
|
|
548
|
+
return response.json();
|
|
549
|
+
}
|
|
550
|
+
if (contentType?.includes("text/")) {
|
|
551
|
+
return response.text();
|
|
552
|
+
}
|
|
553
|
+
return response.blob();
|
|
554
|
+
}
|
|
555
|
+
async handleErrorResponse(response) {
|
|
556
|
+
let errorData;
|
|
557
|
+
try {
|
|
558
|
+
const contentType = response.headers.get("content-type");
|
|
559
|
+
if (contentType?.includes("application/json")) {
|
|
560
|
+
errorData = await response.json();
|
|
561
|
+
} else {
|
|
562
|
+
errorData = { message: await response.text() };
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
errorData = { message: "Unknown error occurred" };
|
|
566
|
+
}
|
|
567
|
+
const message = errorData.error?.message || errorData.message || `HTTP ${response.status}`;
|
|
568
|
+
errorData.error?.code || errorData.code;
|
|
569
|
+
switch (response.status) {
|
|
570
|
+
case 401:
|
|
571
|
+
throw new BlinkAuthError(message, errorData);
|
|
572
|
+
case 400:
|
|
573
|
+
throw new BlinkValidationError(message, errorData);
|
|
574
|
+
default:
|
|
575
|
+
throw new BlinkNetworkError(message, response.status, errorData);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Parse Vercel AI SDK data stream format
|
|
580
|
+
* Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
|
|
581
|
+
*/
|
|
582
|
+
async parseDataStream(body, onChunk, onPartial) {
|
|
583
|
+
const reader = body.getReader();
|
|
584
|
+
const decoder = new TextDecoder();
|
|
585
|
+
let buffer = "";
|
|
586
|
+
let finalResult = {};
|
|
587
|
+
try {
|
|
588
|
+
while (true) {
|
|
589
|
+
const { done, value } = await reader.read();
|
|
590
|
+
if (done) break;
|
|
591
|
+
buffer += decoder.decode(value, { stream: true });
|
|
592
|
+
const lines = buffer.split(/\r?\n/);
|
|
593
|
+
buffer = lines.pop() || "";
|
|
594
|
+
for (const line of lines) {
|
|
595
|
+
if (!line.trim()) continue;
|
|
596
|
+
try {
|
|
597
|
+
if (line.startsWith("f:")) {
|
|
598
|
+
const metadata = JSON.parse(line.slice(2));
|
|
599
|
+
finalResult.messageId = metadata.messageId;
|
|
600
|
+
} else if (line.startsWith("0:")) {
|
|
601
|
+
const textChunk = JSON.parse(line.slice(2));
|
|
602
|
+
if (onChunk) {
|
|
603
|
+
onChunk(textChunk);
|
|
604
|
+
}
|
|
605
|
+
finalResult.text = (finalResult.text || "") + textChunk;
|
|
606
|
+
} else if (line.startsWith("2:")) {
|
|
607
|
+
const data = JSON.parse(line.slice(2));
|
|
608
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
609
|
+
const item = data[0];
|
|
610
|
+
if (typeof item === "string") {
|
|
611
|
+
finalResult.status = item;
|
|
612
|
+
} else if (typeof item === "object") {
|
|
613
|
+
if (onPartial) {
|
|
614
|
+
onPartial(item);
|
|
615
|
+
}
|
|
616
|
+
finalResult.object = item;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else if (line.startsWith("d:")) {
|
|
620
|
+
const metadata = JSON.parse(line.slice(2));
|
|
621
|
+
if (metadata.usage) {
|
|
622
|
+
finalResult.usage = metadata.usage;
|
|
623
|
+
}
|
|
624
|
+
if (metadata.finishReason) {
|
|
625
|
+
finalResult.finishReason = metadata.finishReason;
|
|
626
|
+
}
|
|
627
|
+
} else if (line.startsWith("e:")) {
|
|
628
|
+
const errorData = JSON.parse(line.slice(2));
|
|
629
|
+
finalResult.error = errorData;
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.warn("Failed to parse stream line:", line, error);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (buffer.trim()) {
|
|
637
|
+
try {
|
|
638
|
+
if (buffer.startsWith("0:")) {
|
|
639
|
+
const textChunk = JSON.parse(buffer.slice(2));
|
|
640
|
+
if (onChunk) {
|
|
641
|
+
onChunk(textChunk);
|
|
642
|
+
}
|
|
643
|
+
finalResult.text = (finalResult.text || "") + textChunk;
|
|
644
|
+
} else if (buffer.startsWith("2:")) {
|
|
645
|
+
const data = JSON.parse(buffer.slice(2));
|
|
646
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
647
|
+
const item = data[0];
|
|
648
|
+
if (typeof item === "object") {
|
|
649
|
+
if (onPartial) {
|
|
650
|
+
onPartial(item);
|
|
651
|
+
}
|
|
652
|
+
finalResult.object = item;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} else if (buffer.startsWith("d:")) {
|
|
656
|
+
const metadata = JSON.parse(buffer.slice(2));
|
|
657
|
+
if (metadata.usage) {
|
|
658
|
+
finalResult.usage = metadata.usage;
|
|
659
|
+
}
|
|
660
|
+
if (metadata.finishReason) {
|
|
661
|
+
finalResult.finishReason = metadata.finishReason;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} catch (error) {
|
|
665
|
+
console.warn("Failed to parse final buffer:", buffer, error);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return finalResult;
|
|
669
|
+
} finally {
|
|
670
|
+
reader.releaseLock();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/auth.ts
|
|
676
|
+
var BlinkAuth = class {
|
|
677
|
+
config;
|
|
678
|
+
authState;
|
|
679
|
+
listeners = /* @__PURE__ */ new Set();
|
|
680
|
+
authUrl = "https://blink.new";
|
|
681
|
+
constructor(config) {
|
|
682
|
+
this.config = config;
|
|
683
|
+
this.authState = {
|
|
684
|
+
user: null,
|
|
685
|
+
tokens: null,
|
|
686
|
+
isAuthenticated: false,
|
|
687
|
+
isLoading: false
|
|
688
|
+
};
|
|
689
|
+
if (typeof window !== "undefined") {
|
|
690
|
+
this.initialize();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Initialize authentication from stored tokens or URL fragments
|
|
695
|
+
*/
|
|
696
|
+
async initialize() {
|
|
697
|
+
console.log("\u{1F680} Initializing Blink Auth...");
|
|
698
|
+
this.setLoading(true);
|
|
699
|
+
try {
|
|
700
|
+
const tokensFromUrl = this.extractTokensFromUrl();
|
|
701
|
+
if (tokensFromUrl) {
|
|
702
|
+
console.log("\u{1F4E5} Found tokens in URL, setting them...");
|
|
703
|
+
await this.setTokens(tokensFromUrl, true);
|
|
704
|
+
this.clearUrlTokens();
|
|
705
|
+
console.log("\u2705 Auth initialization complete (from URL)");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const storedTokens = this.getStoredTokens();
|
|
709
|
+
if (storedTokens) {
|
|
710
|
+
console.log("\u{1F4BE} Found stored tokens, validating...", {
|
|
711
|
+
hasAccessToken: !!storedTokens.access_token,
|
|
712
|
+
hasRefreshToken: !!storedTokens.refresh_token,
|
|
713
|
+
issuedAt: storedTokens.issued_at,
|
|
714
|
+
expiresIn: storedTokens.expires_in,
|
|
715
|
+
refreshExpiresIn: storedTokens.refresh_expires_in,
|
|
716
|
+
currentTime: Math.floor(Date.now() / 1e3)
|
|
717
|
+
});
|
|
718
|
+
this.authState.tokens = storedTokens;
|
|
719
|
+
console.log("\u{1F527} Tokens set in auth state, refresh token available:", !!this.authState.tokens?.refresh_token);
|
|
720
|
+
const isValid = await this.validateStoredTokens(storedTokens);
|
|
721
|
+
if (isValid) {
|
|
722
|
+
console.log("\u2705 Auth initialization complete (from storage)");
|
|
723
|
+
return;
|
|
724
|
+
} else {
|
|
725
|
+
console.log("\u{1F504} Stored tokens invalid, clearing...");
|
|
726
|
+
this.clearTokens();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
console.log("\u274C No tokens found");
|
|
730
|
+
if (this.config.authRequired) {
|
|
731
|
+
console.log("\u{1F504} Auth required, redirecting to auth page...");
|
|
732
|
+
this.redirectToAuth();
|
|
733
|
+
} else {
|
|
734
|
+
console.log("\u26A0\uFE0F Auth not required, continuing without authentication");
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
this.setLoading(false);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Redirect to Blink auth page
|
|
742
|
+
*/
|
|
743
|
+
login(nextUrl) {
|
|
744
|
+
const redirectUrl = nextUrl || (typeof window !== "undefined" ? window.location.href : "");
|
|
745
|
+
const authUrl = new URL("/auth", this.authUrl);
|
|
746
|
+
authUrl.searchParams.set("redirect_url", redirectUrl);
|
|
747
|
+
if (this.config.projectId) {
|
|
748
|
+
authUrl.searchParams.set("project_id", this.config.projectId);
|
|
749
|
+
}
|
|
750
|
+
if (typeof window !== "undefined") {
|
|
751
|
+
window.location.href = authUrl.toString();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Logout and clear stored tokens
|
|
756
|
+
*/
|
|
757
|
+
logout(redirectUrl) {
|
|
758
|
+
this.clearTokens();
|
|
759
|
+
if (redirectUrl && typeof window !== "undefined") {
|
|
760
|
+
window.location.href = redirectUrl;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Check if user is authenticated
|
|
765
|
+
*/
|
|
766
|
+
isAuthenticated() {
|
|
767
|
+
return this.authState.isAuthenticated;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Get current user (sync)
|
|
771
|
+
*/
|
|
772
|
+
currentUser() {
|
|
773
|
+
return this.authState.user;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Get current access token
|
|
777
|
+
*/
|
|
778
|
+
getToken() {
|
|
779
|
+
return this.authState.tokens?.access_token || null;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Check if access token is expired based on timestamp
|
|
783
|
+
*/
|
|
784
|
+
isAccessTokenExpired() {
|
|
785
|
+
const tokens = this.authState.tokens;
|
|
786
|
+
if (!tokens || !tokens.issued_at) {
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
790
|
+
const expiresAt = tokens.issued_at + tokens.expires_in;
|
|
791
|
+
const bufferTime = 30;
|
|
792
|
+
return now >= expiresAt - bufferTime;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Check if refresh token is expired based on timestamp
|
|
796
|
+
*/
|
|
797
|
+
isRefreshTokenExpired() {
|
|
798
|
+
const tokens = this.authState.tokens;
|
|
799
|
+
if (!tokens || !tokens.refresh_token || !tokens.issued_at || !tokens.refresh_expires_in) {
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
803
|
+
const expiresAt = tokens.issued_at + tokens.refresh_expires_in;
|
|
804
|
+
return now >= expiresAt;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get a valid access token, refreshing if necessary
|
|
808
|
+
*/
|
|
809
|
+
async getValidToken() {
|
|
810
|
+
const tokens = this.authState.tokens;
|
|
811
|
+
if (!tokens) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
if (!this.isAccessTokenExpired()) {
|
|
815
|
+
console.log("\u2705 Access token is still valid");
|
|
816
|
+
return tokens.access_token;
|
|
817
|
+
}
|
|
818
|
+
console.log("\u23F0 Access token expired, attempting refresh...");
|
|
819
|
+
if (this.isRefreshTokenExpired()) {
|
|
820
|
+
console.log("\u274C Refresh token also expired, clearing tokens");
|
|
821
|
+
this.clearTokens();
|
|
822
|
+
if (this.config.authRequired) {
|
|
823
|
+
this.redirectToAuth();
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
const refreshed = await this.refreshToken();
|
|
828
|
+
if (refreshed) {
|
|
829
|
+
console.log("\u2705 Token refreshed successfully");
|
|
830
|
+
return this.authState.tokens?.access_token || null;
|
|
831
|
+
} else {
|
|
832
|
+
console.log("\u274C Token refresh failed");
|
|
833
|
+
this.clearTokens();
|
|
834
|
+
if (this.config.authRequired) {
|
|
835
|
+
this.redirectToAuth();
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Fetch current user profile from API
|
|
842
|
+
*/
|
|
843
|
+
async me() {
|
|
844
|
+
let token = this.getToken();
|
|
845
|
+
if (!token) {
|
|
846
|
+
throw new BlinkAuthError("No access token available");
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
const response = await fetch(`${this.authUrl}/api/auth/me`, {
|
|
850
|
+
headers: {
|
|
851
|
+
"Authorization": `Bearer ${token}`
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
if (!response.ok) {
|
|
855
|
+
if (response.status === 401) {
|
|
856
|
+
const refreshed = await this.refreshToken();
|
|
857
|
+
if (refreshed) {
|
|
858
|
+
token = this.getToken();
|
|
859
|
+
if (token) {
|
|
860
|
+
const retryResponse = await fetch(`${this.authUrl}/api/auth/me`, {
|
|
861
|
+
headers: {
|
|
862
|
+
"Authorization": `Bearer ${token}`
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
if (retryResponse.ok) {
|
|
866
|
+
const retryData = await retryResponse.json();
|
|
867
|
+
const user2 = retryData.user;
|
|
868
|
+
this.updateAuthState({
|
|
869
|
+
...this.authState,
|
|
870
|
+
user: user2
|
|
871
|
+
});
|
|
872
|
+
return user2;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
this.clearTokens();
|
|
877
|
+
if (this.config.authRequired) {
|
|
878
|
+
this.redirectToAuth();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
throw new BlinkAuthError(`Failed to fetch user: ${response.statusText}`);
|
|
882
|
+
}
|
|
883
|
+
const data = await response.json();
|
|
884
|
+
const user = data.user;
|
|
885
|
+
this.updateAuthState({
|
|
886
|
+
...this.authState,
|
|
887
|
+
user
|
|
888
|
+
});
|
|
889
|
+
return user;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
if (error instanceof BlinkAuthError) {
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
throw new BlinkAuthError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Update user profile
|
|
899
|
+
*/
|
|
900
|
+
async updateMe(updates) {
|
|
901
|
+
const token = this.getToken();
|
|
902
|
+
if (!token) {
|
|
903
|
+
throw new BlinkAuthError("No access token available");
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const response = await fetch(`${this.authUrl}/api/auth/me`, {
|
|
907
|
+
method: "PATCH",
|
|
908
|
+
headers: {
|
|
909
|
+
"Authorization": `Bearer ${token}`,
|
|
910
|
+
"Content-Type": "application/json"
|
|
911
|
+
},
|
|
912
|
+
body: JSON.stringify(updates)
|
|
913
|
+
});
|
|
914
|
+
if (!response.ok) {
|
|
915
|
+
throw new BlinkAuthError(`Failed to update user: ${response.statusText}`);
|
|
916
|
+
}
|
|
917
|
+
const data = await response.json();
|
|
918
|
+
const user = data.user;
|
|
919
|
+
this.updateAuthState({
|
|
920
|
+
...this.authState,
|
|
921
|
+
user
|
|
922
|
+
});
|
|
923
|
+
return user;
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (error instanceof BlinkAuthError) {
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
928
|
+
throw new BlinkAuthError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Manually set tokens (for server-side usage)
|
|
933
|
+
*/
|
|
934
|
+
async setToken(jwt, persist = false) {
|
|
935
|
+
const tokens = {
|
|
936
|
+
access_token: jwt,
|
|
937
|
+
token_type: "Bearer",
|
|
938
|
+
expires_in: 15 * 60
|
|
939
|
+
// Default 15 minutes
|
|
940
|
+
};
|
|
941
|
+
await this.setTokens(tokens, persist);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Refresh access token using refresh token
|
|
945
|
+
*/
|
|
946
|
+
async refreshToken() {
|
|
947
|
+
const refreshToken = this.authState.tokens?.refresh_token;
|
|
948
|
+
if (!refreshToken) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
const response = await fetch(`${this.authUrl}/api/auth/refresh`, {
|
|
953
|
+
method: "POST",
|
|
954
|
+
headers: {
|
|
955
|
+
"Content-Type": "application/json"
|
|
956
|
+
},
|
|
957
|
+
body: JSON.stringify({
|
|
958
|
+
refresh_token: refreshToken
|
|
959
|
+
})
|
|
960
|
+
});
|
|
961
|
+
if (!response.ok) {
|
|
962
|
+
if (response.status === 401) {
|
|
963
|
+
this.clearTokens();
|
|
964
|
+
if (this.config.authRequired) {
|
|
965
|
+
this.redirectToAuth();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
const data = await response.json();
|
|
971
|
+
await this.setTokens({
|
|
972
|
+
access_token: data.access_token,
|
|
973
|
+
refresh_token: data.refresh_token,
|
|
974
|
+
token_type: data.token_type,
|
|
975
|
+
expires_in: data.expires_in,
|
|
976
|
+
refresh_expires_in: data.refresh_expires_in
|
|
977
|
+
}, true);
|
|
978
|
+
return true;
|
|
979
|
+
} catch (error) {
|
|
980
|
+
console.error("Token refresh failed:", error);
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Add auth state change listener
|
|
986
|
+
*/
|
|
987
|
+
onAuthStateChanged(callback) {
|
|
988
|
+
this.listeners.add(callback);
|
|
989
|
+
callback(this.authState);
|
|
990
|
+
return () => {
|
|
991
|
+
this.listeners.delete(callback);
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Private helper methods
|
|
996
|
+
*/
|
|
997
|
+
async validateStoredTokens(tokens) {
|
|
998
|
+
try {
|
|
999
|
+
console.log("\u{1F50D} Validating stored tokens...");
|
|
1000
|
+
if (this.isAccessTokenExpired()) {
|
|
1001
|
+
console.log("\u23F0 Access token expired based on timestamp, attempting refresh...");
|
|
1002
|
+
if (!tokens.refresh_token) {
|
|
1003
|
+
console.log("\u274C No refresh token available");
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
if (this.isRefreshTokenExpired()) {
|
|
1007
|
+
console.log("\u274C Refresh token also expired");
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
const refreshed = await this.refreshToken();
|
|
1011
|
+
if (refreshed) {
|
|
1012
|
+
console.log("\u2705 Token refreshed successfully during validation");
|
|
1013
|
+
return true;
|
|
1014
|
+
} else {
|
|
1015
|
+
console.log("\u274C Token refresh failed during validation");
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const response = await fetch(`${this.authUrl}/api/auth/me`, {
|
|
1020
|
+
headers: {
|
|
1021
|
+
"Authorization": `Bearer ${tokens.access_token}`
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
if (response.ok) {
|
|
1025
|
+
const data = await response.json();
|
|
1026
|
+
const user = data.user;
|
|
1027
|
+
this.updateAuthState({
|
|
1028
|
+
user,
|
|
1029
|
+
tokens,
|
|
1030
|
+
isAuthenticated: true,
|
|
1031
|
+
isLoading: false
|
|
1032
|
+
});
|
|
1033
|
+
console.log("\u2705 Stored tokens are valid, user authenticated");
|
|
1034
|
+
return true;
|
|
1035
|
+
} else if (response.status === 401 && tokens.refresh_token) {
|
|
1036
|
+
console.log("\u{1F504} Access token expired (server validation), attempting refresh...");
|
|
1037
|
+
if (this.isRefreshTokenExpired()) {
|
|
1038
|
+
console.log("\u274C Refresh token expired");
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
const refreshed = await this.refreshToken();
|
|
1042
|
+
if (refreshed) {
|
|
1043
|
+
console.log("\u2705 Token refreshed successfully after server validation");
|
|
1044
|
+
return true;
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log("\u274C Token refresh failed after server validation");
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
} else {
|
|
1050
|
+
console.log("\u274C Token validation failed:", response.status, response.statusText);
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.log("\u{1F4A5} Error validating tokens:", error);
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async setTokens(tokens, persist) {
|
|
1059
|
+
const tokensWithTimestamp = {
|
|
1060
|
+
...tokens,
|
|
1061
|
+
issued_at: tokens.issued_at || Math.floor(Date.now() / 1e3)
|
|
1062
|
+
};
|
|
1063
|
+
console.log("\u{1F510} Setting tokens:", {
|
|
1064
|
+
persist,
|
|
1065
|
+
hasAccessToken: !!tokensWithTimestamp.access_token,
|
|
1066
|
+
hasRefreshToken: !!tokensWithTimestamp.refresh_token,
|
|
1067
|
+
expiresIn: tokensWithTimestamp.expires_in,
|
|
1068
|
+
issuedAt: tokensWithTimestamp.issued_at
|
|
1069
|
+
});
|
|
1070
|
+
if (persist && typeof window !== "undefined") {
|
|
1071
|
+
localStorage.setItem("blink_tokens", JSON.stringify(tokensWithTimestamp));
|
|
1072
|
+
console.log("\u{1F4BE} Tokens persisted to localStorage");
|
|
1073
|
+
}
|
|
1074
|
+
let user = null;
|
|
1075
|
+
try {
|
|
1076
|
+
console.log("\u{1F464} Fetching user data...");
|
|
1077
|
+
const response = await fetch(`${this.authUrl}/api/auth/me`, {
|
|
1078
|
+
headers: {
|
|
1079
|
+
"Authorization": `Bearer ${tokensWithTimestamp.access_token}`
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
console.log("\u{1F4E1} User fetch response:", {
|
|
1083
|
+
status: response.status,
|
|
1084
|
+
statusText: response.statusText,
|
|
1085
|
+
ok: response.ok
|
|
1086
|
+
});
|
|
1087
|
+
if (response.ok) {
|
|
1088
|
+
const data = await response.json();
|
|
1089
|
+
user = data.user;
|
|
1090
|
+
console.log("\u2705 User data fetched successfully:", {
|
|
1091
|
+
id: user?.id,
|
|
1092
|
+
email: user?.email,
|
|
1093
|
+
displayName: user?.displayName
|
|
1094
|
+
});
|
|
1095
|
+
} else {
|
|
1096
|
+
console.log("\u274C Failed to fetch user data:", await response.text());
|
|
1097
|
+
}
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
console.log("\u{1F4A5} Error fetching user data:", error);
|
|
1100
|
+
}
|
|
1101
|
+
this.updateAuthState({
|
|
1102
|
+
user,
|
|
1103
|
+
tokens: tokensWithTimestamp,
|
|
1104
|
+
isAuthenticated: !!user,
|
|
1105
|
+
isLoading: false
|
|
1106
|
+
});
|
|
1107
|
+
console.log("\u{1F3AF} Auth state updated:", {
|
|
1108
|
+
hasUser: !!user,
|
|
1109
|
+
isAuthenticated: !!user,
|
|
1110
|
+
isLoading: false
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
clearTokens() {
|
|
1114
|
+
if (typeof window !== "undefined") {
|
|
1115
|
+
localStorage.removeItem("blink_tokens");
|
|
1116
|
+
}
|
|
1117
|
+
this.updateAuthState({
|
|
1118
|
+
user: null,
|
|
1119
|
+
tokens: null,
|
|
1120
|
+
isAuthenticated: false,
|
|
1121
|
+
isLoading: false
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
getStoredTokens() {
|
|
1125
|
+
if (typeof window === "undefined") return null;
|
|
1126
|
+
try {
|
|
1127
|
+
const stored = localStorage.getItem("blink_tokens");
|
|
1128
|
+
console.log("\u{1F50D} Checking localStorage for tokens:", {
|
|
1129
|
+
hasStoredData: !!stored,
|
|
1130
|
+
storedLength: stored?.length || 0
|
|
1131
|
+
});
|
|
1132
|
+
if (stored) {
|
|
1133
|
+
const tokens = JSON.parse(stored);
|
|
1134
|
+
console.log("\u{1F4E6} Parsed stored tokens:", {
|
|
1135
|
+
hasAccessToken: !!tokens.access_token,
|
|
1136
|
+
hasRefreshToken: !!tokens.refresh_token,
|
|
1137
|
+
tokenType: tokens.token_type,
|
|
1138
|
+
expiresIn: tokens.expires_in
|
|
1139
|
+
});
|
|
1140
|
+
return tokens;
|
|
1141
|
+
}
|
|
1142
|
+
return null;
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
console.log("\u{1F4A5} Error parsing stored tokens:", error);
|
|
1145
|
+
localStorage.removeItem("blink_tokens");
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
extractTokensFromUrl() {
|
|
1150
|
+
if (typeof window === "undefined") return null;
|
|
1151
|
+
const params = new URLSearchParams(window.location.search);
|
|
1152
|
+
const accessToken = params.get("access_token");
|
|
1153
|
+
const refreshToken = params.get("refresh_token");
|
|
1154
|
+
console.log("\u{1F50D} Extracting tokens from URL:", {
|
|
1155
|
+
url: window.location.href,
|
|
1156
|
+
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
|
|
1157
|
+
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
|
|
1158
|
+
allParams: Object.fromEntries(params.entries())
|
|
1159
|
+
});
|
|
1160
|
+
if (accessToken) {
|
|
1161
|
+
const tokens = {
|
|
1162
|
+
access_token: accessToken,
|
|
1163
|
+
refresh_token: refreshToken || void 0,
|
|
1164
|
+
token_type: "Bearer",
|
|
1165
|
+
expires_in: 15 * 60,
|
|
1166
|
+
// 15 minutes default
|
|
1167
|
+
refresh_expires_in: refreshToken ? 30 * 24 * 60 * 60 : void 0,
|
|
1168
|
+
// 30 days default
|
|
1169
|
+
issued_at: Math.floor(Date.now() / 1e3)
|
|
1170
|
+
// Current timestamp
|
|
1171
|
+
};
|
|
1172
|
+
console.log("\u2705 Tokens extracted successfully:", {
|
|
1173
|
+
hasAccessToken: !!tokens.access_token,
|
|
1174
|
+
hasRefreshToken: !!tokens.refresh_token
|
|
1175
|
+
});
|
|
1176
|
+
return tokens;
|
|
1177
|
+
}
|
|
1178
|
+
console.log("\u274C No access token found in URL");
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
clearUrlTokens() {
|
|
1182
|
+
if (typeof window === "undefined") return;
|
|
1183
|
+
const url = new URL(window.location.href);
|
|
1184
|
+
url.searchParams.delete("access_token");
|
|
1185
|
+
url.searchParams.delete("refresh_token");
|
|
1186
|
+
url.searchParams.delete("token_type");
|
|
1187
|
+
url.searchParams.delete("project_id");
|
|
1188
|
+
url.searchParams.delete("expires_in");
|
|
1189
|
+
url.searchParams.delete("refresh_expires_in");
|
|
1190
|
+
url.searchParams.delete("state");
|
|
1191
|
+
url.searchParams.delete("code");
|
|
1192
|
+
url.searchParams.delete("error");
|
|
1193
|
+
url.searchParams.delete("error_description");
|
|
1194
|
+
window.history.replaceState({}, "", url.toString());
|
|
1195
|
+
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
|
|
1196
|
+
}
|
|
1197
|
+
redirectToAuth() {
|
|
1198
|
+
if (typeof window !== "undefined") {
|
|
1199
|
+
this.login();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
setLoading(loading) {
|
|
1203
|
+
this.updateAuthState({
|
|
1204
|
+
...this.authState,
|
|
1205
|
+
isLoading: loading
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
updateAuthState(newState) {
|
|
1209
|
+
this.authState = newState;
|
|
1210
|
+
this.listeners.forEach((callback) => {
|
|
1211
|
+
try {
|
|
1212
|
+
callback(newState);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
console.error("Error in auth state change callback:", error);
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// src/database.ts
|
|
1221
|
+
var BlinkTable = class {
|
|
1222
|
+
constructor(tableName, httpClient) {
|
|
1223
|
+
this.tableName = tableName;
|
|
1224
|
+
this.httpClient = httpClient;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Create a single record
|
|
1228
|
+
*/
|
|
1229
|
+
async create(data, options = {}) {
|
|
1230
|
+
const response = await this.httpClient.dbPost(
|
|
1231
|
+
this.tableName,
|
|
1232
|
+
data,
|
|
1233
|
+
{ returning: options.returning !== false }
|
|
1234
|
+
);
|
|
1235
|
+
const result = Array.isArray(response.data) ? response.data[0] : response.data;
|
|
1236
|
+
if (!result) {
|
|
1237
|
+
throw new Error("Failed to create record");
|
|
1238
|
+
}
|
|
1239
|
+
return result;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Create multiple records
|
|
1243
|
+
*/
|
|
1244
|
+
async createMany(data, options = {}) {
|
|
1245
|
+
const response = await this.httpClient.dbPost(
|
|
1246
|
+
this.tableName,
|
|
1247
|
+
data,
|
|
1248
|
+
{ returning: options.returning !== false }
|
|
1249
|
+
);
|
|
1250
|
+
return Array.isArray(response.data) ? response.data : [response.data];
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Upsert a single record (insert or update on conflict)
|
|
1254
|
+
*/
|
|
1255
|
+
async upsert(data, options = {}) {
|
|
1256
|
+
const headers = {};
|
|
1257
|
+
if (options.returning !== false) {
|
|
1258
|
+
headers.Prefer = "return=representation";
|
|
1259
|
+
}
|
|
1260
|
+
if (options.onConflict) {
|
|
1261
|
+
headers["Prefer"] = `${headers["Prefer"] || ""} resolution=merge-duplicates`.trim();
|
|
1262
|
+
}
|
|
1263
|
+
const response = await this.httpClient.request(
|
|
1264
|
+
`/api/db/${this.httpClient.projectId}/rest/v1/${this.tableName}?on_conflict=${options.onConflict || "id"}`,
|
|
1265
|
+
{
|
|
1266
|
+
method: "POST",
|
|
1267
|
+
body: data,
|
|
1268
|
+
headers
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
const result = Array.isArray(response.data) ? response.data[0] : response.data;
|
|
1272
|
+
if (!result) {
|
|
1273
|
+
throw new Error("Failed to upsert record");
|
|
1274
|
+
}
|
|
1275
|
+
return result;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Upsert multiple records
|
|
1279
|
+
*/
|
|
1280
|
+
async upsertMany(data, options = {}) {
|
|
1281
|
+
const headers = {};
|
|
1282
|
+
if (options.returning !== false) {
|
|
1283
|
+
headers.Prefer = "return=representation";
|
|
1284
|
+
}
|
|
1285
|
+
if (options.onConflict) {
|
|
1286
|
+
headers["Prefer"] = `${headers["Prefer"] || ""} resolution=merge-duplicates`.trim();
|
|
1287
|
+
}
|
|
1288
|
+
const response = await this.httpClient.request(
|
|
1289
|
+
`/api/db/${this.httpClient.projectId}/rest/v1/${this.tableName}?on_conflict=${options.onConflict || "id"}`,
|
|
1290
|
+
{
|
|
1291
|
+
method: "POST",
|
|
1292
|
+
body: data,
|
|
1293
|
+
headers
|
|
1294
|
+
}
|
|
1295
|
+
);
|
|
1296
|
+
return Array.isArray(response.data) ? response.data : [response.data];
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Get a single record by ID
|
|
1300
|
+
*/
|
|
1301
|
+
async get(id) {
|
|
1302
|
+
const searchParams = {
|
|
1303
|
+
id: `eq.${id}`,
|
|
1304
|
+
limit: "1"
|
|
1305
|
+
};
|
|
1306
|
+
const response = await this.httpClient.dbGet(this.tableName, searchParams);
|
|
1307
|
+
const records = response.data;
|
|
1308
|
+
return records.length > 0 ? records[0] : null;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* List records with filtering, sorting, and pagination
|
|
1312
|
+
*/
|
|
1313
|
+
async list(options = {}) {
|
|
1314
|
+
const queryParams = buildQuery(options);
|
|
1315
|
+
const searchParams = queryParams;
|
|
1316
|
+
const response = await this.httpClient.dbGet(this.tableName, searchParams);
|
|
1317
|
+
const records = response.data;
|
|
1318
|
+
const hasMore = options.limit ? records.length === options.limit : false;
|
|
1319
|
+
const nextCursor = hasMore && records.length > 0 ? this.extractCursor(records[records.length - 1]) : void 0;
|
|
1320
|
+
return {
|
|
1321
|
+
data: records,
|
|
1322
|
+
count: records.length,
|
|
1323
|
+
nextCursor,
|
|
1324
|
+
hasMore
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Update a single record by ID
|
|
1329
|
+
*/
|
|
1330
|
+
async update(id, data, options = {}) {
|
|
1331
|
+
const searchParams = {
|
|
1332
|
+
id: `eq.${id}`
|
|
1333
|
+
};
|
|
1334
|
+
const response = await this.httpClient.dbPatch(
|
|
1335
|
+
this.tableName,
|
|
1336
|
+
data,
|
|
1337
|
+
searchParams,
|
|
1338
|
+
{ returning: options.returning !== false }
|
|
1339
|
+
);
|
|
1340
|
+
const records = response.data;
|
|
1341
|
+
if (!records || records.length === 0) {
|
|
1342
|
+
throw new Error(`Record with id ${id} not found`);
|
|
1343
|
+
}
|
|
1344
|
+
return records[0];
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Update multiple records
|
|
1348
|
+
*/
|
|
1349
|
+
async updateMany(updates, options = {}) {
|
|
1350
|
+
const results = [];
|
|
1351
|
+
for (const update of updates) {
|
|
1352
|
+
const { id, ...data } = update;
|
|
1353
|
+
const result = await this.update(id, data, options);
|
|
1354
|
+
results.push(result);
|
|
1355
|
+
}
|
|
1356
|
+
return results;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Delete a single record by ID
|
|
1360
|
+
*/
|
|
1361
|
+
async delete(id) {
|
|
1362
|
+
const searchParams = {
|
|
1363
|
+
id: `eq.${id}`
|
|
1364
|
+
};
|
|
1365
|
+
await this.httpClient.dbDelete(this.tableName, searchParams);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Delete multiple records based on filter
|
|
1369
|
+
*/
|
|
1370
|
+
async deleteMany(options) {
|
|
1371
|
+
const queryParams = buildQuery({ where: options.where });
|
|
1372
|
+
const searchParams = queryParams;
|
|
1373
|
+
await this.httpClient.dbDelete(this.tableName, searchParams);
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Count records matching filter
|
|
1377
|
+
*/
|
|
1378
|
+
async count(options = {}) {
|
|
1379
|
+
const queryParams = buildQuery({
|
|
1380
|
+
where: options.where,
|
|
1381
|
+
select: ["id"]
|
|
1382
|
+
});
|
|
1383
|
+
const response = await this.httpClient.request(
|
|
1384
|
+
`/api/db/${this.httpClient.projectId}/rest/v1/${this.tableName}`,
|
|
1385
|
+
{
|
|
1386
|
+
method: "GET",
|
|
1387
|
+
searchParams: queryParams,
|
|
1388
|
+
headers: {
|
|
1389
|
+
"Prefer": "count=exact"
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
const contentRange = response.headers.get("content-range");
|
|
1394
|
+
if (contentRange) {
|
|
1395
|
+
const match = contentRange.match(/\/(\d+)$/);
|
|
1396
|
+
if (match && match[1]) {
|
|
1397
|
+
return parseInt(match[1], 10);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return Array.isArray(response.data) ? response.data.length : 0;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Check if any records exist matching filter
|
|
1404
|
+
*/
|
|
1405
|
+
async exists(options) {
|
|
1406
|
+
const count = await this.count(options);
|
|
1407
|
+
return count > 0;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Raw SQL query on this table (for advanced use cases)
|
|
1411
|
+
*/
|
|
1412
|
+
async sql(query, params) {
|
|
1413
|
+
const response = await this.httpClient.dbSql(query, params);
|
|
1414
|
+
return response.data;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Private helper methods
|
|
1418
|
+
*/
|
|
1419
|
+
extractCursor(record) {
|
|
1420
|
+
return record.id || record._id || String(Math.random());
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
var BlinkDatabase = class {
|
|
1424
|
+
constructor(httpClient) {
|
|
1425
|
+
this.httpClient = httpClient;
|
|
1426
|
+
const proxy = new Proxy(this, {
|
|
1427
|
+
get(target, prop) {
|
|
1428
|
+
if (prop === "table") {
|
|
1429
|
+
return target.table.bind(target);
|
|
1430
|
+
}
|
|
1431
|
+
if (prop in target) {
|
|
1432
|
+
const value = target[prop];
|
|
1433
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1434
|
+
}
|
|
1435
|
+
if (typeof prop === "string") {
|
|
1436
|
+
return target.table(prop);
|
|
1437
|
+
}
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
return proxy;
|
|
1442
|
+
}
|
|
1443
|
+
tables = /* @__PURE__ */ new Map();
|
|
1444
|
+
/**
|
|
1445
|
+
* Get a table instance for any table name
|
|
1446
|
+
*/
|
|
1447
|
+
table(tableName) {
|
|
1448
|
+
if (!this.tables.has(tableName)) {
|
|
1449
|
+
this.tables.set(tableName, new BlinkTable(tableName, this.httpClient));
|
|
1450
|
+
}
|
|
1451
|
+
const table = this.tables.get(tableName);
|
|
1452
|
+
if (!table) {
|
|
1453
|
+
throw new Error(`Table ${tableName} not found`);
|
|
1454
|
+
}
|
|
1455
|
+
return table;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Execute raw SQL query
|
|
1459
|
+
*/
|
|
1460
|
+
async sql(query, params) {
|
|
1461
|
+
const response = await this.httpClient.dbSql(query, params);
|
|
1462
|
+
return response.data;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Execute batch SQL operations
|
|
1466
|
+
*/
|
|
1467
|
+
async batch(statements, mode = "write") {
|
|
1468
|
+
const response = await this.httpClient.dbBatch(statements, mode);
|
|
1469
|
+
return response.data;
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
// src/storage.ts
|
|
1474
|
+
var BlinkStorageImpl = class {
|
|
1475
|
+
constructor(httpClient) {
|
|
1476
|
+
this.httpClient = httpClient;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Upload a file to project storage
|
|
1480
|
+
*
|
|
1481
|
+
* @param file - File, Blob, or Buffer to upload
|
|
1482
|
+
* @param path - Destination path within project storage
|
|
1483
|
+
* @param options - Upload options including upsert and progress callback
|
|
1484
|
+
* @returns Promise resolving to upload response with public URL
|
|
1485
|
+
*
|
|
1486
|
+
* @example
|
|
1487
|
+
* ```ts
|
|
1488
|
+
* const { publicUrl } = await blink.storage.upload(
|
|
1489
|
+
* fileInput.files[0],
|
|
1490
|
+
* `avatars/${user.id}.png`,
|
|
1491
|
+
* {
|
|
1492
|
+
* upsert: true,
|
|
1493
|
+
* onProgress: pct => console.log(`${pct}%`)
|
|
1494
|
+
* }
|
|
1495
|
+
* );
|
|
1496
|
+
* ```
|
|
1497
|
+
*/
|
|
1498
|
+
async upload(file, path, options = {}) {
|
|
1499
|
+
try {
|
|
1500
|
+
if (!file) {
|
|
1501
|
+
throw new BlinkStorageError("File is required");
|
|
1502
|
+
}
|
|
1503
|
+
if (!path || typeof path !== "string" || !path.trim()) {
|
|
1504
|
+
throw new BlinkStorageError("Path must be a non-empty string");
|
|
1505
|
+
}
|
|
1506
|
+
const maxSize = 50 * 1024 * 1024;
|
|
1507
|
+
let fileSize = 0;
|
|
1508
|
+
if (file instanceof File || file instanceof Blob) {
|
|
1509
|
+
fileSize = file.size;
|
|
1510
|
+
} else if (typeof Buffer !== "undefined" && file instanceof Buffer) {
|
|
1511
|
+
fileSize = file.length;
|
|
1512
|
+
}
|
|
1513
|
+
if (fileSize > maxSize) {
|
|
1514
|
+
throw new BlinkStorageError(`File size (${Math.round(fileSize / 1024 / 1024)}MB) exceeds maximum allowed size (50MB)`);
|
|
1515
|
+
}
|
|
1516
|
+
const response = await this.httpClient.uploadFile(
|
|
1517
|
+
`/api/storage/${this.httpClient.projectId}/upload`,
|
|
1518
|
+
file,
|
|
1519
|
+
path,
|
|
1520
|
+
{
|
|
1521
|
+
upsert: options.upsert,
|
|
1522
|
+
onProgress: options.onProgress
|
|
1523
|
+
}
|
|
1524
|
+
);
|
|
1525
|
+
if (response.data?.data?.publicUrl) {
|
|
1526
|
+
return { publicUrl: response.data.data.publicUrl };
|
|
1527
|
+
} else if (response.data?.publicUrl) {
|
|
1528
|
+
return { publicUrl: response.data.publicUrl };
|
|
1529
|
+
} else {
|
|
1530
|
+
throw new BlinkStorageError("Invalid response format: missing publicUrl");
|
|
1531
|
+
}
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
if (error instanceof BlinkStorageError) {
|
|
1534
|
+
throw error;
|
|
1535
|
+
}
|
|
1536
|
+
if (error instanceof Error && "status" in error) {
|
|
1537
|
+
const status = error.status;
|
|
1538
|
+
if (status === 409) {
|
|
1539
|
+
throw new BlinkStorageError("File already exists. Set upsert: true to overwrite.", 409);
|
|
1540
|
+
}
|
|
1541
|
+
if (status === 400) {
|
|
1542
|
+
throw new BlinkStorageError("Invalid request parameters", 400);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
throw new BlinkStorageError(
|
|
1546
|
+
`Upload failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1547
|
+
void 0,
|
|
1548
|
+
{ originalError: error }
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Remove one or more files from project storage
|
|
1554
|
+
*
|
|
1555
|
+
* @param paths - File paths to remove
|
|
1556
|
+
* @returns Promise that resolves when files are removed
|
|
1557
|
+
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```ts
|
|
1560
|
+
* await blink.storage.remove('avatars/user1.png');
|
|
1561
|
+
* await blink.storage.remove('file1.pdf', 'file2.pdf', 'file3.pdf');
|
|
1562
|
+
* ```
|
|
1563
|
+
*/
|
|
1564
|
+
async remove(...paths) {
|
|
1565
|
+
try {
|
|
1566
|
+
if (paths.length === 0) {
|
|
1567
|
+
throw new BlinkStorageError("At least one path must be provided");
|
|
1568
|
+
}
|
|
1569
|
+
for (const path of paths) {
|
|
1570
|
+
if (!path || typeof path !== "string") {
|
|
1571
|
+
throw new BlinkStorageError("All paths must be non-empty strings");
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
await this.httpClient.request(
|
|
1575
|
+
`/api/storage/${this.httpClient.projectId}/remove`,
|
|
1576
|
+
{
|
|
1577
|
+
method: "DELETE",
|
|
1578
|
+
body: { paths },
|
|
1579
|
+
headers: { "Content-Type": "application/json" }
|
|
1580
|
+
}
|
|
1581
|
+
);
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
if (error instanceof BlinkStorageError) {
|
|
1584
|
+
throw error;
|
|
1585
|
+
}
|
|
1586
|
+
if (error instanceof Error && "status" in error) {
|
|
1587
|
+
const status = error.status;
|
|
1588
|
+
if (status === 400) {
|
|
1589
|
+
throw new BlinkStorageError("Invalid request parameters", 400);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
throw new BlinkStorageError(
|
|
1593
|
+
`Failed to remove files: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1594
|
+
void 0,
|
|
1595
|
+
{ originalError: error }
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
// src/ai.ts
|
|
1602
|
+
var BlinkAIImpl = class {
|
|
1603
|
+
constructor(httpClient) {
|
|
1604
|
+
this.httpClient = httpClient;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Get MIME type for audio format
|
|
1608
|
+
*/
|
|
1609
|
+
getMimeTypeForFormat(format) {
|
|
1610
|
+
const mimeTypes = {
|
|
1611
|
+
mp3: "audio/mpeg",
|
|
1612
|
+
opus: "audio/opus",
|
|
1613
|
+
aac: "audio/aac",
|
|
1614
|
+
flac: "audio/flac",
|
|
1615
|
+
wav: "audio/wav",
|
|
1616
|
+
pcm: "audio/pcm"
|
|
1617
|
+
};
|
|
1618
|
+
return mimeTypes[format] || "audio/mpeg";
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Generates a text response using the Blink AI engine.
|
|
1622
|
+
*
|
|
1623
|
+
* @param options - An object containing either:
|
|
1624
|
+
* - `prompt`: a simple string prompt
|
|
1625
|
+
* - OR `messages`: an array of chat messages for conversation
|
|
1626
|
+
* - Plus optional model, maxTokens, temperature, signal parameters
|
|
1627
|
+
*
|
|
1628
|
+
* @example
|
|
1629
|
+
* ```ts
|
|
1630
|
+
* // Simple prompt
|
|
1631
|
+
* const { text } = await blink.ai.generateText({
|
|
1632
|
+
* prompt: "Write a poem about coding"
|
|
1633
|
+
* });
|
|
1634
|
+
*
|
|
1635
|
+
* // Chat messages
|
|
1636
|
+
* const { text } = await blink.ai.generateText({
|
|
1637
|
+
* messages: [
|
|
1638
|
+
* { role: "system", content: "You are a helpful assistant" },
|
|
1639
|
+
* { role: "user", content: "Explain quantum computing" }
|
|
1640
|
+
* ]
|
|
1641
|
+
* });
|
|
1642
|
+
*
|
|
1643
|
+
* // With options
|
|
1644
|
+
* const { text, usage } = await blink.ai.generateText({
|
|
1645
|
+
* prompt: "Summarize this article",
|
|
1646
|
+
* model: "gpt-4o-mini",
|
|
1647
|
+
* maxTokens: 150,
|
|
1648
|
+
* temperature: 0.7
|
|
1649
|
+
* });
|
|
1650
|
+
* ```
|
|
1651
|
+
*
|
|
1652
|
+
* @returns Promise<TextGenerationResponse> - Object containing:
|
|
1653
|
+
* - `text`: Generated text string
|
|
1654
|
+
* - `usage`: Token usage information
|
|
1655
|
+
* - `finishReason`: Why generation stopped ("stop", "length", etc.)
|
|
1656
|
+
*/
|
|
1657
|
+
async generateText(options) {
|
|
1658
|
+
try {
|
|
1659
|
+
if (!options.prompt && !options.messages) {
|
|
1660
|
+
throw new BlinkAIError("Either prompt or messages is required");
|
|
1661
|
+
}
|
|
1662
|
+
const requestBody = {
|
|
1663
|
+
model: options.model,
|
|
1664
|
+
stream: false,
|
|
1665
|
+
maxTokens: options.maxTokens,
|
|
1666
|
+
temperature: options.temperature,
|
|
1667
|
+
signal: options.signal
|
|
1668
|
+
};
|
|
1669
|
+
if (options.prompt) {
|
|
1670
|
+
requestBody.prompt = options.prompt;
|
|
1671
|
+
}
|
|
1672
|
+
if (options.messages) {
|
|
1673
|
+
requestBody.messages = options.messages;
|
|
1674
|
+
}
|
|
1675
|
+
const response = await this.httpClient.aiText(
|
|
1676
|
+
options.prompt || "",
|
|
1677
|
+
requestBody
|
|
1678
|
+
);
|
|
1679
|
+
if (response.data?.result) {
|
|
1680
|
+
return response.data.result;
|
|
1681
|
+
} else if (response.data?.text) {
|
|
1682
|
+
return response.data;
|
|
1683
|
+
} else {
|
|
1684
|
+
throw new BlinkAIError("Invalid response format: missing text");
|
|
1685
|
+
}
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
if (error instanceof BlinkAIError) {
|
|
1688
|
+
throw error;
|
|
1689
|
+
}
|
|
1690
|
+
throw new BlinkAIError(
|
|
1691
|
+
`Text generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1692
|
+
void 0,
|
|
1693
|
+
{ originalError: error }
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Streams text generation with real-time updates as the AI generates content.
|
|
1699
|
+
*
|
|
1700
|
+
* @param options - Same as generateText: either `prompt` or `messages` with optional parameters
|
|
1701
|
+
* @param onChunk - Callback function that receives each text chunk as it's generated
|
|
1702
|
+
*
|
|
1703
|
+
* @example
|
|
1704
|
+
* ```ts
|
|
1705
|
+
* // Stream with prompt
|
|
1706
|
+
* await blink.ai.streamText(
|
|
1707
|
+
* { prompt: "Write a short story about space exploration" },
|
|
1708
|
+
* (chunk) => {
|
|
1709
|
+
* process.stdout.write(chunk); // Real-time output
|
|
1710
|
+
* }
|
|
1711
|
+
* );
|
|
1712
|
+
*
|
|
1713
|
+
* // Stream with messages
|
|
1714
|
+
* await blink.ai.streamText(
|
|
1715
|
+
* {
|
|
1716
|
+
* messages: [
|
|
1717
|
+
* { role: "system", content: "You are a creative writer" },
|
|
1718
|
+
* { role: "user", content: "Write a haiku about programming" }
|
|
1719
|
+
* ]
|
|
1720
|
+
* },
|
|
1721
|
+
* (chunk) => updateUI(chunk)
|
|
1722
|
+
* );
|
|
1723
|
+
* ```
|
|
1724
|
+
*
|
|
1725
|
+
* @returns Promise<TextGenerationResponse> - Final complete response with full text and metadata
|
|
1726
|
+
*/
|
|
1727
|
+
async streamText(options, onChunk) {
|
|
1728
|
+
try {
|
|
1729
|
+
if (!options.prompt && !options.messages) {
|
|
1730
|
+
throw new BlinkAIError("Either prompt or messages is required");
|
|
1731
|
+
}
|
|
1732
|
+
const result = await this.httpClient.streamAiText(
|
|
1733
|
+
options.prompt || "",
|
|
1734
|
+
{
|
|
1735
|
+
model: options.model,
|
|
1736
|
+
messages: options.messages,
|
|
1737
|
+
maxTokens: options.maxTokens,
|
|
1738
|
+
temperature: options.temperature,
|
|
1739
|
+
signal: options.signal
|
|
1740
|
+
},
|
|
1741
|
+
onChunk
|
|
1742
|
+
);
|
|
1743
|
+
return {
|
|
1744
|
+
text: result.text || "",
|
|
1745
|
+
finishReason: "stop",
|
|
1746
|
+
usage: result.usage,
|
|
1747
|
+
...result
|
|
1748
|
+
};
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
if (error instanceof BlinkAIError) {
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
throw new BlinkAIError(
|
|
1754
|
+
`Text streaming failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1755
|
+
void 0,
|
|
1756
|
+
{ originalError: error }
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Generates structured JSON objects using AI with schema validation.
|
|
1762
|
+
*
|
|
1763
|
+
* @param options - Object containing:
|
|
1764
|
+
* - `prompt`: Description of what object to generate (required)
|
|
1765
|
+
* - `schema`: JSON Schema to validate the generated object
|
|
1766
|
+
* - `output`: Type of output ("object", "array", "enum")
|
|
1767
|
+
* - `enum`: Array of allowed values for enum output
|
|
1768
|
+
* - Plus optional model, signal parameters
|
|
1769
|
+
*
|
|
1770
|
+
* @example
|
|
1771
|
+
* ```ts
|
|
1772
|
+
* // Generate user profile
|
|
1773
|
+
* const { object } = await blink.ai.generateObject({
|
|
1774
|
+
* prompt: "Generate a user profile for a software developer",
|
|
1775
|
+
* schema: {
|
|
1776
|
+
* type: "object",
|
|
1777
|
+
* properties: {
|
|
1778
|
+
* name: { type: "string" },
|
|
1779
|
+
* age: { type: "number" },
|
|
1780
|
+
* skills: { type: "array", items: { type: "string" } },
|
|
1781
|
+
* experience: { type: "number" }
|
|
1782
|
+
* },
|
|
1783
|
+
* required: ["name", "skills"]
|
|
1784
|
+
* }
|
|
1785
|
+
* });
|
|
1786
|
+
*
|
|
1787
|
+
* // Generate array of items
|
|
1788
|
+
* const { object } = await blink.ai.generateObject({
|
|
1789
|
+
* prompt: "List 5 programming languages",
|
|
1790
|
+
* output: "array",
|
|
1791
|
+
* schema: {
|
|
1792
|
+
* type: "array",
|
|
1793
|
+
* items: { type: "string" }
|
|
1794
|
+
* }
|
|
1795
|
+
* });
|
|
1796
|
+
*
|
|
1797
|
+
* // Generate enum value
|
|
1798
|
+
* const { object } = await blink.ai.generateObject({
|
|
1799
|
+
* prompt: "Choose the best programming language for web development",
|
|
1800
|
+
* output: "enum",
|
|
1801
|
+
* enum: ["JavaScript", "Python", "TypeScript", "Go"]
|
|
1802
|
+
* });
|
|
1803
|
+
* ```
|
|
1804
|
+
*
|
|
1805
|
+
* @returns Promise<ObjectGenerationResponse> - Object containing:
|
|
1806
|
+
* - `object`: The generated and validated JSON object/array/enum
|
|
1807
|
+
* - `usage`: Token usage information
|
|
1808
|
+
* - `finishReason`: Why generation stopped
|
|
1809
|
+
*/
|
|
1810
|
+
async generateObject(options) {
|
|
1811
|
+
try {
|
|
1812
|
+
if (!options.prompt) {
|
|
1813
|
+
throw new BlinkAIError("Prompt is required");
|
|
1814
|
+
}
|
|
1815
|
+
const response = await this.httpClient.aiObject(
|
|
1816
|
+
options.prompt,
|
|
1817
|
+
{
|
|
1818
|
+
model: options.model,
|
|
1819
|
+
output: options.output,
|
|
1820
|
+
schema: options.schema,
|
|
1821
|
+
enum: options.enum,
|
|
1822
|
+
stream: false,
|
|
1823
|
+
signal: options.signal
|
|
1824
|
+
}
|
|
1825
|
+
);
|
|
1826
|
+
if (response.data?.result) {
|
|
1827
|
+
return response.data.result;
|
|
1828
|
+
} else if (response.data?.object) {
|
|
1829
|
+
return response.data;
|
|
1830
|
+
} else {
|
|
1831
|
+
throw new BlinkAIError("Invalid response format: missing object");
|
|
1832
|
+
}
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
if (error instanceof BlinkAIError) {
|
|
1835
|
+
throw error;
|
|
1836
|
+
}
|
|
1837
|
+
throw new BlinkAIError(
|
|
1838
|
+
`Object generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1839
|
+
void 0,
|
|
1840
|
+
{ originalError: error }
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Streams structured object generation with real-time partial updates as the AI builds the object.
|
|
1846
|
+
*
|
|
1847
|
+
* @param options - Same as generateObject: prompt, schema, output type, etc.
|
|
1848
|
+
* @param onPartial - Callback function that receives partial object updates as they're generated
|
|
1849
|
+
*
|
|
1850
|
+
* @example
|
|
1851
|
+
* ```ts
|
|
1852
|
+
* // Stream object generation with schema
|
|
1853
|
+
* await blink.ai.streamObject(
|
|
1854
|
+
* {
|
|
1855
|
+
* prompt: "Generate a detailed product catalog entry",
|
|
1856
|
+
* schema: {
|
|
1857
|
+
* type: "object",
|
|
1858
|
+
* properties: {
|
|
1859
|
+
* name: { type: "string" },
|
|
1860
|
+
* price: { type: "number" },
|
|
1861
|
+
* description: { type: "string" },
|
|
1862
|
+
* features: { type: "array", items: { type: "string" } }
|
|
1863
|
+
* }
|
|
1864
|
+
* }
|
|
1865
|
+
* },
|
|
1866
|
+
* (partial) => {
|
|
1867
|
+
* console.log("Partial update:", partial);
|
|
1868
|
+
* updateProductForm(partial); // Update UI in real-time
|
|
1869
|
+
* }
|
|
1870
|
+
* );
|
|
1871
|
+
* ```
|
|
1872
|
+
*
|
|
1873
|
+
* @returns Promise<ObjectGenerationResponse> - Final complete object with metadata
|
|
1874
|
+
*/
|
|
1875
|
+
async streamObject(options, onPartial) {
|
|
1876
|
+
try {
|
|
1877
|
+
if (!options.prompt) {
|
|
1878
|
+
throw new BlinkAIError("Prompt is required");
|
|
1879
|
+
}
|
|
1880
|
+
const result = await this.httpClient.streamAiObject(
|
|
1881
|
+
options.prompt,
|
|
1882
|
+
{
|
|
1883
|
+
model: options.model,
|
|
1884
|
+
output: options.output,
|
|
1885
|
+
schema: options.schema,
|
|
1886
|
+
enum: options.enum,
|
|
1887
|
+
signal: options.signal
|
|
1888
|
+
},
|
|
1889
|
+
onPartial
|
|
1890
|
+
);
|
|
1891
|
+
return {
|
|
1892
|
+
object: result.object || {},
|
|
1893
|
+
finishReason: "stop",
|
|
1894
|
+
usage: result.usage,
|
|
1895
|
+
...result
|
|
1896
|
+
};
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
if (error instanceof BlinkAIError) {
|
|
1899
|
+
throw error;
|
|
1900
|
+
}
|
|
1901
|
+
throw new BlinkAIError(
|
|
1902
|
+
`Object streaming failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1903
|
+
void 0,
|
|
1904
|
+
{ originalError: error }
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Generates images from text descriptions using AI image models.
|
|
1910
|
+
*
|
|
1911
|
+
* @param options - Object containing:
|
|
1912
|
+
* - `prompt`: Text description of the image to generate (required)
|
|
1913
|
+
* - `size`: Image dimensions (e.g., "1024x1024", "512x512") - varies by model
|
|
1914
|
+
* - `quality`: Image quality ("standard" or "hd")
|
|
1915
|
+
* - `n`: Number of images to generate (default: 1)
|
|
1916
|
+
* - `response_format`: Output format ("url" or "b64_json")
|
|
1917
|
+
* - Plus optional model, signal parameters
|
|
1918
|
+
*
|
|
1919
|
+
* @example
|
|
1920
|
+
* ```ts
|
|
1921
|
+
* // Basic image generation
|
|
1922
|
+
* const { data } = await blink.ai.generateImage({
|
|
1923
|
+
* prompt: "A serene landscape with mountains and a lake at sunset"
|
|
1924
|
+
* });
|
|
1925
|
+
* console.log("Image URL:", data[0].url);
|
|
1926
|
+
*
|
|
1927
|
+
* // High-quality image with specific size
|
|
1928
|
+
* const { data } = await blink.ai.generateImage({
|
|
1929
|
+
* prompt: "A futuristic city skyline with flying cars",
|
|
1930
|
+
* size: "1792x1024",
|
|
1931
|
+
* quality: "hd",
|
|
1932
|
+
* model: "dall-e-3"
|
|
1933
|
+
* });
|
|
1934
|
+
*
|
|
1935
|
+
* // Multiple images
|
|
1936
|
+
* const { data } = await blink.ai.generateImage({
|
|
1937
|
+
* prompt: "A cute robot mascot for a tech company",
|
|
1938
|
+
* n: 3,
|
|
1939
|
+
* size: "1024x1024"
|
|
1940
|
+
* });
|
|
1941
|
+
* data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url));
|
|
1942
|
+
*
|
|
1943
|
+
* // Base64 format for direct embedding
|
|
1944
|
+
* const { data } = await blink.ai.generateImage({
|
|
1945
|
+
* prompt: "A minimalist logo design",
|
|
1946
|
+
* response_format: "b64_json"
|
|
1947
|
+
* });
|
|
1948
|
+
* console.log("Base64 data:", data[0].b64_json);
|
|
1949
|
+
* ```
|
|
1950
|
+
*
|
|
1951
|
+
* @returns Promise<ImageGenerationResponse> - Object containing:
|
|
1952
|
+
* - `data`: Array of generated images with url or b64_json
|
|
1953
|
+
*/
|
|
1954
|
+
async generateImage(options) {
|
|
1955
|
+
try {
|
|
1956
|
+
if (!options.prompt) {
|
|
1957
|
+
throw new BlinkAIError("Prompt is required");
|
|
1958
|
+
}
|
|
1959
|
+
const response = await this.httpClient.aiImage(
|
|
1960
|
+
options.prompt,
|
|
1961
|
+
{
|
|
1962
|
+
model: options.model,
|
|
1963
|
+
size: options.size,
|
|
1964
|
+
quality: options.quality,
|
|
1965
|
+
n: options.n,
|
|
1966
|
+
response_format: options.response_format,
|
|
1967
|
+
signal: options.signal
|
|
1968
|
+
}
|
|
1969
|
+
);
|
|
1970
|
+
let imageResponse;
|
|
1971
|
+
if (response.data?.result?.data) {
|
|
1972
|
+
imageResponse = response.data.result;
|
|
1973
|
+
} else if (response.data?.data) {
|
|
1974
|
+
imageResponse = response.data;
|
|
1975
|
+
} else {
|
|
1976
|
+
throw new BlinkAIError("Invalid response format: missing image data");
|
|
1977
|
+
}
|
|
1978
|
+
if (!Array.isArray(imageResponse.data)) {
|
|
1979
|
+
throw new BlinkAIError("Invalid response format: data should be an array");
|
|
1980
|
+
}
|
|
1981
|
+
imageResponse.data = imageResponse.data.map((item) => {
|
|
1982
|
+
if (typeof item === "string") {
|
|
1983
|
+
return { url: item };
|
|
1984
|
+
} else if (item.url) {
|
|
1985
|
+
return item;
|
|
1986
|
+
} else if (item.b64_json) {
|
|
1987
|
+
return { b64_json: item.b64_json };
|
|
1988
|
+
} else {
|
|
1989
|
+
return { url: item };
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
return imageResponse;
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
if (error instanceof BlinkAIError) {
|
|
1995
|
+
throw error;
|
|
1996
|
+
}
|
|
1997
|
+
throw new BlinkAIError(
|
|
1998
|
+
`Image generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1999
|
+
void 0,
|
|
2000
|
+
{ originalError: error }
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Converts text to speech using AI voice synthesis models.
|
|
2006
|
+
*
|
|
2007
|
+
* @param options - Object containing:
|
|
2008
|
+
* - `text`: Text content to convert to speech (required)
|
|
2009
|
+
* - `voice`: Voice to use ("alloy", "echo", "fable", "onyx", "nova", "shimmer")
|
|
2010
|
+
* - `response_format`: Audio format ("mp3", "opus", "aac", "flac", "wav", "pcm")
|
|
2011
|
+
* - `speed`: Speech speed (0.25 to 4.0, default: 1.0)
|
|
2012
|
+
* - Plus optional model, signal parameters
|
|
2013
|
+
*
|
|
2014
|
+
* @example
|
|
2015
|
+
* ```ts
|
|
2016
|
+
* // Basic text-to-speech
|
|
2017
|
+
* const { url } = await blink.ai.generateSpeech({
|
|
2018
|
+
* text: "Hello, welcome to our AI-powered application!"
|
|
2019
|
+
* });
|
|
2020
|
+
* console.log("Audio URL:", url);
|
|
2021
|
+
*
|
|
2022
|
+
* // Custom voice and format
|
|
2023
|
+
* const { url, voice, format } = await blink.ai.generateSpeech({
|
|
2024
|
+
* text: "This is a demonstration of our speech synthesis capabilities.",
|
|
2025
|
+
* voice: "nova",
|
|
2026
|
+
* response_format: "wav",
|
|
2027
|
+
* speed: 1.2
|
|
2028
|
+
* });
|
|
2029
|
+
* console.log(`Generated ${format} audio with ${voice} voice:`, url);
|
|
2030
|
+
*
|
|
2031
|
+
* // Slow, clear speech for accessibility
|
|
2032
|
+
* const { url } = await blink.ai.generateSpeech({
|
|
2033
|
+
* text: "Please listen carefully to these important instructions.",
|
|
2034
|
+
* voice: "echo",
|
|
2035
|
+
* speed: 0.8
|
|
2036
|
+
* });
|
|
2037
|
+
* ```
|
|
2038
|
+
*
|
|
2039
|
+
* @returns Promise<SpeechGenerationResponse> - Object containing:
|
|
2040
|
+
* - `url`: URL to the generated audio file
|
|
2041
|
+
* - `voice`: Voice used for generation
|
|
2042
|
+
* - `format`: Audio format
|
|
2043
|
+
* - `mimeType`: MIME type of the audio
|
|
2044
|
+
*/
|
|
2045
|
+
async generateSpeech(options) {
|
|
2046
|
+
try {
|
|
2047
|
+
if (!options.text) {
|
|
2048
|
+
throw new BlinkAIError("Text is required");
|
|
2049
|
+
}
|
|
2050
|
+
const response = await this.httpClient.aiSpeech(
|
|
2051
|
+
options.text,
|
|
2052
|
+
{
|
|
2053
|
+
model: options.model,
|
|
2054
|
+
voice: options.voice,
|
|
2055
|
+
response_format: options.response_format,
|
|
2056
|
+
speed: options.speed,
|
|
2057
|
+
signal: options.signal
|
|
2058
|
+
}
|
|
2059
|
+
);
|
|
2060
|
+
let speechResponse;
|
|
2061
|
+
if (response.data?.result) {
|
|
2062
|
+
speechResponse = response.data.result;
|
|
2063
|
+
} else if (response.data?.url) {
|
|
2064
|
+
speechResponse = response.data;
|
|
2065
|
+
} else {
|
|
2066
|
+
throw new BlinkAIError("Invalid response format: missing speech data");
|
|
2067
|
+
}
|
|
2068
|
+
if (!speechResponse.url) {
|
|
2069
|
+
if (typeof response.data === "string") {
|
|
2070
|
+
speechResponse = {
|
|
2071
|
+
url: response.data,
|
|
2072
|
+
voice: options.voice || "alloy",
|
|
2073
|
+
format: options.response_format || "mp3",
|
|
2074
|
+
mimeType: this.getMimeTypeForFormat(options.response_format || "mp3")
|
|
2075
|
+
};
|
|
2076
|
+
} else if (response.data?.data) {
|
|
2077
|
+
speechResponse = {
|
|
2078
|
+
url: response.data.data,
|
|
2079
|
+
voice: options.voice || "alloy",
|
|
2080
|
+
format: options.response_format || "mp3",
|
|
2081
|
+
mimeType: this.getMimeTypeForFormat(options.response_format || "mp3")
|
|
2082
|
+
};
|
|
2083
|
+
} else {
|
|
2084
|
+
throw new BlinkAIError("Invalid response format: no audio URL found");
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (!speechResponse.voice) {
|
|
2088
|
+
speechResponse.voice = options.voice || "alloy";
|
|
2089
|
+
}
|
|
2090
|
+
if (!speechResponse.format) {
|
|
2091
|
+
speechResponse.format = options.response_format || "mp3";
|
|
2092
|
+
}
|
|
2093
|
+
if (!speechResponse.mimeType) {
|
|
2094
|
+
speechResponse.mimeType = this.getMimeTypeForFormat(speechResponse.format);
|
|
2095
|
+
}
|
|
2096
|
+
return speechResponse;
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
if (error instanceof BlinkAIError) {
|
|
2099
|
+
throw error;
|
|
2100
|
+
}
|
|
2101
|
+
throw new BlinkAIError(
|
|
2102
|
+
`Speech generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2103
|
+
void 0,
|
|
2104
|
+
{ originalError: error }
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Transcribes audio content to text using AI speech recognition models.
|
|
2110
|
+
*
|
|
2111
|
+
* @param options - Object containing:
|
|
2112
|
+
* - `audio`: Audio input as URL string, base64 string, or number array buffer (required)
|
|
2113
|
+
* - `language`: Language code for transcription (e.g., "en", "es", "fr")
|
|
2114
|
+
* - `response_format`: Output format ("json", "text", "srt", "verbose_json", "vtt")
|
|
2115
|
+
* - Plus optional model, signal parameters
|
|
2116
|
+
*
|
|
2117
|
+
* @example
|
|
2118
|
+
* ```ts
|
|
2119
|
+
* // Transcribe from URL
|
|
2120
|
+
* const { text } = await blink.ai.transcribeAudio({
|
|
2121
|
+
* audio: "https://example.com/meeting-recording.mp3"
|
|
2122
|
+
* });
|
|
2123
|
+
* console.log("Transcription:", text);
|
|
2124
|
+
*
|
|
2125
|
+
* // Transcribe with language hint
|
|
2126
|
+
* const { text, language } = await blink.ai.transcribeAudio({
|
|
2127
|
+
* audio: "https://example.com/spanish-audio.wav",
|
|
2128
|
+
* language: "es"
|
|
2129
|
+
* });
|
|
2130
|
+
* console.log(`Transcribed ${language}:`, text);
|
|
2131
|
+
*
|
|
2132
|
+
* // Transcribe with timestamps (verbose format)
|
|
2133
|
+
* const result = await blink.ai.transcribeAudio({
|
|
2134
|
+
* audio: audioFileUrl,
|
|
2135
|
+
* response_format: "verbose_json"
|
|
2136
|
+
* });
|
|
2137
|
+
* result.segments?.forEach(segment => {
|
|
2138
|
+
* console.log(`${segment.start}s - ${segment.end}s: ${segment.text}`);
|
|
2139
|
+
* });
|
|
2140
|
+
*
|
|
2141
|
+
* // Transcribe from audio buffer
|
|
2142
|
+
* const audioBuffer = new Array(1024).fill(0); // Your audio data
|
|
2143
|
+
* const { text } = await blink.ai.transcribeAudio({
|
|
2144
|
+
* audio: audioBuffer,
|
|
2145
|
+
* language: "en"
|
|
2146
|
+
* });
|
|
2147
|
+
* ```
|
|
2148
|
+
*
|
|
2149
|
+
* @returns Promise<TranscriptionResponse> - Object containing:
|
|
2150
|
+
* - `text`: Transcribed text content
|
|
2151
|
+
* - `transcript`: Alias for text
|
|
2152
|
+
* - `segments`: Array of timestamped segments (if verbose format)
|
|
2153
|
+
* - `language`: Detected language
|
|
2154
|
+
* - `duration`: Audio duration in seconds
|
|
2155
|
+
*/
|
|
2156
|
+
async transcribeAudio(options) {
|
|
2157
|
+
try {
|
|
2158
|
+
if (!options.audio) {
|
|
2159
|
+
throw new BlinkAIError("Audio is required");
|
|
2160
|
+
}
|
|
2161
|
+
const response = await this.httpClient.aiTranscribe(
|
|
2162
|
+
options.audio,
|
|
2163
|
+
{
|
|
2164
|
+
model: options.model,
|
|
2165
|
+
language: options.language,
|
|
2166
|
+
response_format: options.response_format,
|
|
2167
|
+
signal: options.signal
|
|
2168
|
+
}
|
|
2169
|
+
);
|
|
2170
|
+
if (response.data?.result) {
|
|
2171
|
+
return response.data.result;
|
|
2172
|
+
} else if (response.data?.text || response.data?.transcript) {
|
|
2173
|
+
return {
|
|
2174
|
+
text: response.data.text || response.data.transcript,
|
|
2175
|
+
transcript: response.data.transcript || response.data.text,
|
|
2176
|
+
...response.data
|
|
2177
|
+
};
|
|
2178
|
+
} else {
|
|
2179
|
+
throw new BlinkAIError("Invalid response format: missing transcription text");
|
|
2180
|
+
}
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
if (error instanceof BlinkAIError) {
|
|
2183
|
+
throw error;
|
|
2184
|
+
}
|
|
2185
|
+
throw new BlinkAIError(
|
|
2186
|
+
`Audio transcription failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2187
|
+
void 0,
|
|
2188
|
+
{ originalError: error }
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
|
|
2194
|
+
// src/client.ts
|
|
2195
|
+
var BlinkClientImpl = class {
|
|
2196
|
+
auth;
|
|
2197
|
+
db;
|
|
2198
|
+
storage;
|
|
2199
|
+
ai;
|
|
2200
|
+
httpClient;
|
|
2201
|
+
constructor(config) {
|
|
2202
|
+
this.auth = new BlinkAuth(config);
|
|
2203
|
+
this.httpClient = new HttpClient(
|
|
2204
|
+
config,
|
|
2205
|
+
() => this.auth.getToken(),
|
|
2206
|
+
() => this.auth.getValidToken()
|
|
2207
|
+
);
|
|
2208
|
+
this.db = new BlinkDatabase(this.httpClient);
|
|
2209
|
+
this.storage = new BlinkStorageImpl(this.httpClient);
|
|
2210
|
+
this.ai = new BlinkAIImpl(this.httpClient);
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
function createClient(config) {
|
|
2214
|
+
if (!config.projectId) {
|
|
2215
|
+
throw new Error("projectId is required");
|
|
2216
|
+
}
|
|
2217
|
+
const clientConfig = {
|
|
2218
|
+
authRequired: true,
|
|
2219
|
+
...config
|
|
2220
|
+
};
|
|
2221
|
+
return new BlinkClientImpl(clientConfig);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
exports.BlinkAIImpl = BlinkAIImpl;
|
|
2225
|
+
exports.BlinkDatabase = BlinkDatabase;
|
|
2226
|
+
exports.BlinkStorageImpl = BlinkStorageImpl;
|
|
2227
|
+
exports.BlinkTable = BlinkTable;
|
|
2228
|
+
exports.createClient = createClient;
|
|
2229
|
+
//# sourceMappingURL=index.js.map
|
|
2230
|
+
//# sourceMappingURL=index.js.map
|