@blinkdotnew/sdk
Version:
Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps
1,577 lines (1,572 loc) • 131 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// ../core/src/types.ts
var BlinkError = class extends Error {
constructor(message, code, status, details) {
super(message);
this.code = code;
this.status = status;
this.details = details;
this.name = "BlinkError";
}
};
var BlinkAuthError = class extends BlinkError {
constructor(message, details) {
super(message, "AUTH_ERROR", 401, details);
this.name = "BlinkAuthError";
}
};
var BlinkNetworkError = class extends BlinkError {
constructor(message, status, details) {
super(message, "NETWORK_ERROR", status, details);
this.name = "BlinkNetworkError";
}
};
var BlinkValidationError = class extends BlinkError {
constructor(message, details) {
super(message, "VALIDATION_ERROR", 400, details);
this.name = "BlinkValidationError";
}
};
var BlinkStorageError = class extends BlinkError {
constructor(message, status, details) {
super(message, "STORAGE_ERROR", status, details);
this.name = "BlinkStorageError";
}
};
var BlinkAIError = class extends BlinkError {
constructor(message, status, details) {
super(message, "AI_ERROR", status, details);
this.name = "BlinkAIError";
}
};
var BlinkDataError = class extends BlinkError {
constructor(message, status, details) {
super(message, "DATA_ERROR", status, details);
this.name = "BlinkDataError";
}
};
var BlinkRealtimeError = class extends BlinkError {
constructor(message, status, details) {
super(message, "REALTIME_ERROR", status, details);
this.name = "BlinkRealtimeError";
}
};
var BlinkNotificationsError = class extends BlinkError {
constructor(message, status, details) {
super(message, "NOTIFICATIONS_ERROR", status, details);
this.name = "BlinkNotificationsError";
}
};
// ../core/src/query-builder.ts
function camelToSnake(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function convertFilterKeysToSnakeCase(condition) {
if (!condition) return condition;
if ("AND" in condition) {
return {
AND: condition.AND?.map(convertFilterKeysToSnakeCase)
};
}
if ("OR" in condition) {
return {
OR: condition.OR?.map(convertFilterKeysToSnakeCase)
};
}
const converted = {};
for (const [field, value] of Object.entries(condition)) {
const snakeField = camelToSnake(field);
converted[snakeField] = value;
}
return converted;
}
function buildFilterQuery(condition) {
if (!condition) return "";
if ("AND" in condition) {
const andConditions = condition.AND?.map(buildFilterQuery).filter(Boolean) || [];
return andConditions.length > 0 ? `and=(${andConditions.join(",")})` : "";
}
if ("OR" in condition) {
const orConditions = condition.OR?.map(buildFilterQuery).filter(Boolean) || [];
return orConditions.length > 0 ? `or=(${orConditions.join(",")})` : "";
}
const params = [];
for (const [field, value] of Object.entries(condition)) {
if (value === void 0 || value === null) continue;
if (typeof value === "object" && !Array.isArray(value)) {
for (const [operator, operatorValue] of Object.entries(value)) {
const param = buildOperatorQuery(field, operator, operatorValue);
if (param) params.push(param);
}
} else {
params.push(`${field}=eq.${encodeQueryValue(value)}`);
}
}
return params.join("&");
}
function buildOperatorQuery(field, operator, value) {
switch (operator) {
case "eq":
return `${field}=eq.${encodeQueryValue(value)}`;
case "neq":
return `${field}=neq.${encodeQueryValue(value)}`;
case "gt":
return `${field}=gt.${encodeQueryValue(value)}`;
case "gte":
return `${field}=gte.${encodeQueryValue(value)}`;
case "lt":
return `${field}=lt.${encodeQueryValue(value)}`;
case "lte":
return `${field}=lte.${encodeQueryValue(value)}`;
case "like":
return `${field}=like.${encodeQueryValue(value)}`;
case "ilike":
return `${field}=ilike.${encodeQueryValue(value)}`;
case "is":
return `${field}=is.${value === null ? "null" : encodeQueryValue(value)}`;
case "not":
return `${field}=not.${encodeQueryValue(value)}`;
case "in":
if (Array.isArray(value)) {
const values = value.map(encodeQueryValue).join(",");
return `${field}=in.(${values})`;
}
return "";
case "not_in":
if (Array.isArray(value)) {
const values = value.map(encodeQueryValue).join(",");
return `${field}=not.in.(${values})`;
}
return "";
default:
return "";
}
}
function encodeQueryValue(value) {
if (value === null) return "null";
if (typeof value === "boolean") {
return value ? "1" : "0";
}
if (typeof value === "number") return value.toString();
return encodeURIComponent(String(value));
}
function buildQuery(options = {}) {
const params = {};
if (options.select && options.select.length > 0) {
const snakeFields = options.select.map(camelToSnake);
params.select = snakeFields.join(",");
} else {
params.select = "*";
}
if (options.where) {
const convertedWhere = convertFilterKeysToSnakeCase(options.where);
const filterQuery = buildFilterQuery(convertedWhere);
if (filterQuery) {
const filterParams = filterQuery.split("&");
for (const param of filterParams) {
const [key, value] = param.split("=", 2);
if (key && value) {
params[key] = value;
}
}
}
}
if (options.orderBy) {
if (typeof options.orderBy === "string") {
params.order = options.orderBy;
} else {
const orderClauses = Object.entries(options.orderBy).map(([field, direction]) => `${camelToSnake(field)}.${direction}`);
params.order = orderClauses.join(",");
}
}
if (options.limit !== void 0) {
params.limit = options.limit.toString();
}
if (options.offset !== void 0) {
params.offset = options.offset.toString();
}
if (options.cursor) {
params.cursor = options.cursor;
}
return params;
}
// ../core/src/http-client.ts
function camelToSnake2(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
function convertKeysToSnakeCase(obj) {
if (obj === null || obj === void 0) return obj;
if (typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(convertKeysToSnakeCase);
const converted = {};
for (const [key, value] of Object.entries(obj)) {
const snakeKey = camelToSnake2(key);
converted[snakeKey] = convertKeysToSnakeCase(value);
}
return converted;
}
function convertKeysToCamelCase(obj) {
if (obj === null || obj === void 0) return obj;
if (typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(convertKeysToCamelCase);
const converted = {};
for (const [key, value] of Object.entries(obj)) {
const camelKey = snakeToCamel(key);
converted[camelKey] = convertKeysToCamelCase(value);
}
return converted;
}
var HttpClient = class {
authUrl = "https://blink.new";
coreUrl = "https://core.blink.new";
projectId;
getToken;
getValidToken;
constructor(config, getToken, getValidToken) {
this.projectId = config.projectId;
this.getToken = getToken;
this.getValidToken = getValidToken;
}
/**
* Make an authenticated request to the Blink API
*/
async request(path, options = {}) {
const url = this.buildUrl(path, options.searchParams);
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
const headers = {
"Content-Type": "application/json",
...options.headers
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const requestInit = {
method: options.method || "GET",
headers,
signal: options.signal
};
if (options.body && options.method !== "GET") {
requestInit.body = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
}
try {
const response = await fetch(url, requestInit);
if (!response.ok) {
await this.handleErrorResponse(response);
}
const data = await this.parseResponse(response);
return {
data,
status: response.status,
headers: response.headers
};
} catch (error) {
if (error instanceof BlinkError) {
throw error;
}
throw new BlinkNetworkError(
`Network request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
0,
{ originalError: error }
);
}
}
/**
* GET request
*/
async get(path, searchParams) {
return this.request(path, { method: "GET", searchParams });
}
/**
* POST request
*/
async post(path, body, headers) {
return this.request(path, { method: "POST", body, headers });
}
/**
* PATCH request
*/
async patch(path, body, headers) {
return this.request(path, { method: "PATCH", body, headers });
}
/**
* DELETE request
*/
async delete(path, searchParams) {
return this.request(path, { method: "DELETE", searchParams });
}
/**
* Database-specific requests
*/
// Table operations (PostgREST-compatible)
async dbGet(table, searchParams) {
const response = await this.get(`/api/db/${this.projectId}/rest/v1/${table}`, searchParams);
const convertedData = convertKeysToCamelCase(response.data);
return {
...response,
data: convertedData
};
}
async dbPost(table, body, options = {}) {
const headers = {};
if (options.returning) {
headers.Prefer = "return=representation";
}
const convertedBody = convertKeysToSnakeCase(body);
const response = await this.post(`/api/db/${this.projectId}/rest/v1/${table}`, convertedBody, headers);
const convertedData = convertKeysToCamelCase(response.data);
return {
...response,
data: convertedData
};
}
async dbPatch(table, body, searchParams, options = {}) {
const headers = {};
if (options.returning) {
headers.Prefer = "return=representation";
}
const convertedBody = convertKeysToSnakeCase(body);
const response = await this.request(`/api/db/${this.projectId}/rest/v1/${table}`, {
method: "PATCH",
body: convertedBody,
headers,
searchParams
});
const convertedData = convertKeysToCamelCase(response.data);
return {
...response,
data: convertedData
};
}
async dbDelete(table, searchParams, options = {}) {
const headers = {};
if (options.returning) {
headers.Prefer = "return=representation";
}
const response = await this.request(`/api/db/${this.projectId}/rest/v1/${table}`, {
method: "DELETE",
headers,
searchParams
});
const convertedData = convertKeysToCamelCase(response.data);
return {
...response,
data: convertedData
};
}
// Raw SQL operations
async dbSql(query, params) {
const response = await this.post(`/api/db/${this.projectId}/sql`, { query, params });
const convertedData = {
...response.data,
rows: convertKeysToCamelCase(response.data.rows)
};
return {
...response,
data: convertedData
};
}
// Batch SQL operations
async dbBatch(statements, mode = "write") {
const response = await this.post(`/api/db/${this.projectId}/batch`, { statements, mode });
const convertedData = {
...response.data,
results: response.data.results.map((result) => ({
...result,
rows: convertKeysToCamelCase(result.rows)
}))
};
return {
...response,
data: convertedData
};
}
/**
* Upload file with progress tracking
*/
async uploadFile(path, file, filePath, options = {}) {
const url = this.buildUrl(path);
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
const formData = new FormData();
if (file instanceof File) {
formData.append("file", file);
} else if (file instanceof Blob) {
const blobWithType = options.contentType ? new Blob([file], { type: options.contentType }) : file;
formData.append("file", blobWithType);
} else if (typeof Buffer !== "undefined" && file instanceof Buffer) {
const blob = new Blob([file], { type: options.contentType || "application/octet-stream" });
formData.append("file", blob);
} else {
throw new BlinkValidationError("Unsupported file type");
}
formData.append("path", filePath);
if (options.upsert !== void 0) {
formData.append("options", JSON.stringify({ upsert: options.upsert }));
}
const headers = {};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
try {
if (typeof XMLHttpRequest !== "undefined" && options.onProgress) {
return this.uploadWithProgress(url, formData, headers, options.onProgress);
}
const response = await fetch(url, {
method: "POST",
headers,
body: formData
});
if (!response.ok) {
await this.handleErrorResponse(response);
}
const data = await this.parseResponse(response);
return {
data,
status: response.status,
headers: response.headers
};
} catch (error) {
if (error instanceof BlinkError) {
throw error;
}
throw new BlinkNetworkError(
`File upload failed: ${error instanceof Error ? error.message : "Unknown error"}`,
0,
{ originalError: error }
);
}
}
/**
* Upload with progress tracking using XMLHttpRequest
*/
uploadWithProgress(url, formData, headers, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
const percent = Math.round(event.loaded / event.total * 100);
onProgress(percent);
}
});
xhr.addEventListener("load", async () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
resolve({
data,
status: xhr.status,
headers: new Headers()
// XMLHttpRequest doesn't provide easy access to response headers
});
} catch (error) {
reject(new BlinkNetworkError("Failed to parse response", xhr.status));
}
} else {
try {
const errorData = JSON.parse(xhr.responseText);
const message = errorData.error?.message || errorData.message || `HTTP ${xhr.status}`;
switch (xhr.status) {
case 401:
reject(new BlinkAuthError(message, errorData));
break;
case 400:
reject(new BlinkValidationError(message, errorData));
break;
default:
reject(new BlinkNetworkError(message, xhr.status, errorData));
}
} catch {
reject(new BlinkNetworkError(`HTTP ${xhr.status}`, xhr.status));
}
}
});
xhr.addEventListener("error", () => {
reject(new BlinkNetworkError("Network error during file upload"));
});
xhr.open("POST", url);
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
xhr.send(formData);
});
}
/**
* AI-specific requests
*/
async aiText(prompt, options = {}) {
const { signal, ...body } = options;
const requestBody = { ...body };
if (prompt) {
requestBody.prompt = prompt;
}
return this.request(`/api/ai/${this.projectId}/text`, {
method: "POST",
body: requestBody,
signal
});
}
/**
* Stream AI text generation with Vercel AI SDK data stream format
*/
async streamAiText(prompt, options = {}, onChunk) {
const url = this.buildUrl(`/api/ai/${this.projectId}/text`);
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
const headers = {
"Content-Type": "application/json"
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const body = {
prompt,
stream: true,
...options
};
const { signal: _signal, ...jsonBody } = body;
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(jsonBody),
signal: options.signal
});
if (!response.ok) {
await this.handleErrorResponse(response);
}
if (!response.body) {
throw new BlinkNetworkError("No response body for streaming");
}
return this.parseDataStream(response.body, onChunk);
} catch (error) {
if (error instanceof BlinkError) {
throw error;
}
throw new BlinkNetworkError(
`Streaming request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
0,
{ originalError: error }
);
}
}
async aiObject(prompt, options = {}) {
const { signal, ...body } = options;
const requestBody = { ...body };
if (prompt) {
requestBody.prompt = prompt;
}
return this.request(`/api/ai/${this.projectId}/object`, {
method: "POST",
body: requestBody,
signal
});
}
/**
* Stream AI object generation with Vercel AI SDK data stream format
*/
async streamAiObject(prompt, options = {}, onPartial) {
const url = this.buildUrl(`/api/ai/${this.projectId}/object`);
const token = this.getValidToken ? await this.getValidToken() : this.getToken();
const headers = {
"Content-Type": "application/json"
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const body = {
prompt,
stream: true,
...options
};
const { signal: _signal2, ...jsonBody2 } = body;
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(jsonBody2),
signal: options.signal
});
if (!response.ok) {
await this.handleErrorResponse(response);
}
if (!response.body) {
throw new BlinkNetworkError("No response body for streaming");
}
return this.parseDataStream(response.body, void 0, onPartial);
} catch (error) {
if (error instanceof BlinkError) {
throw error;
}
throw new BlinkNetworkError(
`Streaming request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
0,
{ originalError: error }
);
}
}
async aiImage(prompt, options = {}) {
const { signal, ...body } = options;
return this.request(`/api/ai/${this.projectId}/image`, {
method: "POST",
body: {
prompt,
...body
},
signal
});
}
async aiSpeech(text, options = {}) {
const { signal, ...body } = options;
return this.request(`/api/ai/${this.projectId}/speech`, {
method: "POST",
body: {
text,
...body
},
signal
});
}
async aiTranscribe(audio, options = {}) {
const { signal, ...body } = options;
let payloadAudio;
if (typeof audio === "string" || Array.isArray(audio)) {
payloadAudio = audio;
} else if (audio instanceof Uint8Array) {
payloadAudio = Array.from(audio);
} else if (audio instanceof ArrayBuffer) {
payloadAudio = Array.from(new Uint8Array(audio));
} else if (typeof Buffer !== "undefined" && Buffer.isBuffer(audio)) {
payloadAudio = Array.from(new Uint8Array(audio));
} else {
throw new BlinkValidationError("Unsupported audio input type");
}
return this.request(`/api/ai/${this.projectId}/transcribe`, {
method: "POST",
body: {
audio: payloadAudio,
...body
},
signal
});
}
/**
* Data-specific requests
*/
async dataExtractFromUrl(projectId, request) {
return this.request(`/api/data/${projectId}/extract-from-url`, {
method: "POST",
body: JSON.stringify(request)
});
}
async dataExtractFromBlob(projectId, file, chunking, chunkSize) {
const formData = new FormData();
formData.append("file", file);
if (chunking !== void 0) {
formData.append("chunking", String(chunking));
}
if (chunkSize !== void 0) {
formData.append("chunkSize", String(chunkSize));
}
return this.request(`/api/data/${projectId}/extract-from-blob`, {
method: "POST",
body: formData
});
}
async dataScrape(projectId, request) {
return this.request(`/api/data/${projectId}/scrape`, {
method: "POST",
body: JSON.stringify(request)
});
}
async dataScreenshot(projectId, request) {
return this.request(`/api/data/${projectId}/screenshot`, {
method: "POST",
body: JSON.stringify(request)
});
}
async dataFetch(projectId, request) {
return this.post(`/api/data/${projectId}/fetch`, request);
}
async dataSearch(projectId, request) {
return this.post(`/api/data/${projectId}/search`, request);
}
/**
* Realtime-specific requests
*/
async realtimePublish(projectId, request) {
return this.post(`/api/realtime/${projectId}/publish`, request);
}
async realtimeGetPresence(projectId, channel) {
return this.get(`/api/realtime/${projectId}/presence`, { channel });
}
async realtimeGetMessages(projectId, options) {
const { channel, ...searchParams } = options;
return this.get(`/api/realtime/${projectId}/messages`, {
channel,
...Object.fromEntries(
Object.entries(searchParams).filter(([k, v]) => v !== void 0).map(([k, v]) => [k, String(v)])
)
});
}
/**
* Private helper methods
*/
buildUrl(path, searchParams) {
const baseUrl = path.includes("/api/auth/") ? this.authUrl : this.coreUrl;
const url = new URL(path, baseUrl);
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
return url.toString();
}
async parseResponse(response) {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
}
if (contentType?.includes("text/")) {
return response.text();
}
return response.blob();
}
async handleErrorResponse(response) {
let errorData;
try {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
errorData = await response.json();
} else {
errorData = { message: await response.text() };
}
} catch {
errorData = { message: "Unknown error occurred" };
}
const message = errorData.error?.message || errorData.message || `HTTP ${response.status}`;
errorData.error?.code || errorData.code;
switch (response.status) {
case 401:
throw new BlinkAuthError(message, errorData);
case 400:
throw new BlinkValidationError(message, errorData);
default:
throw new BlinkNetworkError(message, response.status, errorData);
}
}
/**
* Parse Vercel AI SDK data stream format
* Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
*/
async parseDataStream(body, onChunk, onPartial) {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let finalResult = {};
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
try {
if (line.startsWith("f:")) {
const metadata = JSON.parse(line.slice(2));
finalResult.messageId = metadata.messageId;
} else if (line.startsWith("0:")) {
const textChunk = JSON.parse(line.slice(2));
if (onChunk) {
onChunk(textChunk);
}
finalResult.text = (finalResult.text || "") + textChunk;
} else if (line.startsWith("2:")) {
const data = JSON.parse(line.slice(2));
if (Array.isArray(data) && data.length > 0) {
const item = data[0];
if (typeof item === "string") {
finalResult.status = item;
} else if (typeof item === "object") {
if (onPartial) {
onPartial(item);
}
finalResult.object = item;
}
}
} else if (line.startsWith("d:")) {
const metadata = JSON.parse(line.slice(2));
if (metadata.usage) {
finalResult.usage = metadata.usage;
}
if (metadata.finishReason) {
finalResult.finishReason = metadata.finishReason;
}
} else if (line.startsWith("e:")) {
const errorData = JSON.parse(line.slice(2));
finalResult.error = errorData;
}
} catch (error) {
console.warn("Failed to parse stream line:", line, error);
}
}
}
if (buffer.trim()) {
try {
if (buffer.startsWith("0:")) {
const textChunk = JSON.parse(buffer.slice(2));
if (onChunk) {
onChunk(textChunk);
}
finalResult.text = (finalResult.text || "") + textChunk;
} else if (buffer.startsWith("2:")) {
const data = JSON.parse(buffer.slice(2));
if (Array.isArray(data) && data.length > 0) {
const item = data[0];
if (typeof item === "object") {
if (onPartial) {
onPartial(item);
}
finalResult.object = item;
}
}
} else if (buffer.startsWith("d:")) {
const metadata = JSON.parse(buffer.slice(2));
if (metadata.usage) {
finalResult.usage = metadata.usage;
}
if (metadata.finishReason) {
finalResult.finishReason = metadata.finishReason;
}
}
} catch (error) {
console.warn("Failed to parse final buffer:", buffer, error);
}
}
return finalResult;
} finally {
reader.releaseLock();
}
}
};
// src/auth.ts
var BlinkAuth = class {
config;
authState;
listeners = /* @__PURE__ */ new Set();
authUrl = "https://blink.new";
parentWindowTokens = null;
isIframe = false;
initializationPromise = null;
isInitialized = false;
constructor(config) {
this.config = config;
this.authState = {
user: null,
tokens: null,
isAuthenticated: false,
isLoading: false
};
if (typeof window !== "undefined") {
this.isIframe = window.self !== window.top;
this.setupParentWindowListener();
this.initializationPromise = this.initialize();
} else {
this.isInitialized = true;
}
}
/**
* Wait for authentication initialization to complete
*/
async waitForInitialization() {
if (this.isInitialized) return;
if (this.initializationPromise) {
await this.initializationPromise;
}
}
/**
* Setup listener for tokens from parent window
*/
setupParentWindowListener() {
if (!this.isIframe) return;
window.addEventListener("message", (event) => {
if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
return;
}
if (event.data?.type === "BLINK_AUTH_TOKENS") {
console.log("\u{1F4E5} Received auth tokens from parent window");
const { tokens } = event.data;
if (tokens) {
this.parentWindowTokens = tokens;
this.setTokens(tokens, false).then(() => {
console.log("\u2705 Tokens from parent window applied");
}).catch((error) => {
console.error("Failed to apply parent window tokens:", error);
});
}
}
if (event.data?.type === "BLINK_AUTH_LOGOUT") {
console.log("\u{1F4E4} Received logout command from parent window");
this.clearTokens();
}
});
if (window.parent !== window) {
console.log("\u{1F504} Requesting auth tokens from parent window");
window.parent.postMessage({
type: "BLINK_REQUEST_AUTH_TOKENS",
projectId: this.config.projectId
}, "*");
}
}
/**
* Initialize authentication from stored tokens or URL fragments
*/
async initialize() {
console.log("\u{1F680} Initializing Blink Auth...");
this.setLoading(true);
try {
if (this.isIframe) {
console.log("\u{1F50D} Detected iframe environment, waiting for parent tokens...");
await new Promise((resolve) => setTimeout(resolve, 100));
if (this.parentWindowTokens) {
console.log("\u2705 Using tokens from parent window");
await this.setTokens(this.parentWindowTokens, false);
return;
}
}
const tokensFromUrl = this.extractTokensFromUrl();
if (tokensFromUrl) {
console.log("\u{1F4E5} Found tokens in URL, setting them...");
await this.setTokens(tokensFromUrl, true);
this.clearUrlTokens();
console.log("\u2705 Auth initialization complete (from URL)");
return;
}
const storedTokens = this.getStoredTokens();
if (storedTokens) {
console.log("\u{1F4BE} Found stored tokens, validating...", {
hasAccessToken: !!storedTokens.access_token,
hasRefreshToken: !!storedTokens.refresh_token,
issuedAt: storedTokens.issued_at,
expiresIn: storedTokens.expires_in,
refreshExpiresIn: storedTokens.refresh_expires_in,
currentTime: Math.floor(Date.now() / 1e3)
});
this.authState.tokens = storedTokens;
console.log("\u{1F527} Tokens set in auth state, refresh token available:", !!this.authState.tokens?.refresh_token);
const isValid = await this.validateStoredTokens(storedTokens);
if (isValid) {
console.log("\u2705 Auth initialization complete (from storage)");
return;
} else {
console.log("\u{1F504} Stored tokens invalid, clearing...");
this.clearTokens();
}
}
console.log("\u274C No tokens found");
if (this.config.authRequired) {
console.log("\u{1F504} Auth required, redirecting to auth page...");
this.redirectToAuth();
} else {
console.log("\u26A0\uFE0F Auth not required, continuing without authentication");
}
} finally {
this.setLoading(false);
this.isInitialized = true;
}
}
/**
* Redirect to Blink auth page
*/
login(nextUrl) {
let redirectUrl = nextUrl;
if (!redirectUrl && typeof window !== "undefined") {
if (window.location.href.startsWith("http")) {
redirectUrl = window.location.href;
} else {
redirectUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`;
}
}
if (redirectUrl && typeof window !== "undefined") {
try {
const url = new URL(redirectUrl);
url.searchParams.delete("redirect_url");
url.searchParams.delete("redirect");
redirectUrl = url.toString();
} catch (e) {
console.warn("Failed to parse redirect URL:", e);
}
}
const authUrl = new URL("/auth", this.authUrl);
authUrl.searchParams.set("redirect_url", redirectUrl || "");
if (this.config.projectId) {
authUrl.searchParams.set("project_id", this.config.projectId);
}
if (typeof window !== "undefined") {
window.location.href = authUrl.toString();
}
}
/**
* Logout and clear stored tokens
*/
logout(redirectUrl) {
this.clearTokens();
if (redirectUrl && typeof window !== "undefined") {
window.location.href = redirectUrl;
}
}
/**
* Check if user is authenticated
*/
isAuthenticated() {
return this.authState.isAuthenticated;
}
/**
* Get current user (sync)
*/
currentUser() {
return this.authState.user;
}
/**
* Get current access token
*/
getToken() {
return this.authState.tokens?.access_token || null;
}
/**
* Check if access token is expired based on timestamp
*/
isAccessTokenExpired() {
const tokens = this.authState.tokens;
if (!tokens || !tokens.issued_at) {
return true;
}
const now = Math.floor(Date.now() / 1e3);
const expiresAt = tokens.issued_at + tokens.expires_in;
const bufferTime = 30;
return now >= expiresAt - bufferTime;
}
/**
* Check if refresh token is expired based on timestamp
*/
isRefreshTokenExpired() {
const tokens = this.authState.tokens;
if (!tokens || !tokens.refresh_token || !tokens.issued_at || !tokens.refresh_expires_in) {
return true;
}
const now = Math.floor(Date.now() / 1e3);
const expiresAt = tokens.issued_at + tokens.refresh_expires_in;
return now >= expiresAt;
}
/**
* Get a valid access token, refreshing if necessary
*/
async getValidToken() {
const tokens = this.authState.tokens;
if (!tokens) {
return null;
}
if (!this.isAccessTokenExpired()) {
console.log("\u2705 Access token is still valid");
return tokens.access_token;
}
console.log("\u23F0 Access token expired, attempting refresh...");
if (this.isRefreshTokenExpired()) {
console.log("\u274C Refresh token also expired, clearing tokens");
this.clearTokens();
if (this.config.authRequired) {
this.redirectToAuth();
}
return null;
}
const refreshed = await this.refreshToken();
if (refreshed) {
console.log("\u2705 Token refreshed successfully");
return this.authState.tokens?.access_token || null;
} else {
console.log("\u274C Token refresh failed");
this.clearTokens();
if (this.config.authRequired) {
this.redirectToAuth();
}
return null;
}
}
/**
* Fetch current user profile from API
* Gracefully waits for auth initialization to complete before throwing errors
*/
async me() {
await this.waitForInitialization();
if (this.authState.isAuthenticated && this.authState.user) {
return this.authState.user;
}
if (!this.authState.isAuthenticated) {
return new Promise((resolve, reject) => {
if (this.authState.user) {
resolve(this.authState.user);
return;
}
const timeout = setTimeout(() => {
unsubscribe();
reject(new BlinkAuthError("Authentication timeout - no user available"));
}, 5e3);
const unsubscribe = this.onAuthStateChanged((state) => {
if (state.user) {
clearTimeout(timeout);
unsubscribe();
resolve(state.user);
} else if (!state.isLoading && !state.isAuthenticated) {
clearTimeout(timeout);
unsubscribe();
reject(new BlinkAuthError("Not authenticated"));
}
});
});
}
let token = this.getToken();
if (!token) {
throw new BlinkAuthError("No access token available");
}
try {
const response = await fetch(`${this.authUrl}/api/auth/me`, {
headers: {
"Authorization": `Bearer ${token}`
}
});
if (!response.ok) {
if (response.status === 401) {
const refreshed = await this.refreshToken();
if (refreshed) {
token = this.getToken();
if (token) {
const retryResponse = await fetch(`${this.authUrl}/api/auth/me`, {
headers: {
"Authorization": `Bearer ${token}`
}
});
if (retryResponse.ok) {
const retryData = await retryResponse.json();
const user2 = retryData.user;
this.updateAuthState({
...this.authState,
user: user2
});
return user2;
}
}
}
this.clearTokens();
if (this.config.authRequired) {
this.redirectToAuth();
}
}
throw new BlinkAuthError(`Failed to fetch user: ${response.statusText}`);
}
const data = await response.json();
const user = data.user;
this.updateAuthState({
...this.authState,
user
});
return user;
} catch (error) {
if (error instanceof BlinkAuthError) {
throw error;
}
throw new BlinkAuthError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Update user profile
*/
async updateMe(updates) {
const token = this.getToken();
if (!token) {
throw new BlinkAuthError("No access token available");
}
try {
const response = await fetch(`${this.authUrl}/api/auth/me`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new BlinkAuthError(`Failed to update user: ${response.statusText}`);
}
const data = await response.json();
const user = data.user;
this.updateAuthState({
...this.authState,
user
});
return user;
} catch (error) {
if (error instanceof BlinkAuthError) {
throw error;
}
throw new BlinkAuthError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Manually set tokens (for server-side usage)
*/
async setToken(jwt, persist = false) {
const tokens = {
access_token: jwt,
token_type: "Bearer",
expires_in: 15 * 60
// Default 15 minutes
};
await this.setTokens(tokens, persist);
}
/**
* Refresh access token using refresh token
*/
async refreshToken() {
const refreshToken = this.authState.tokens?.refresh_token;
if (!refreshToken) {
return false;
}
try {
const response = await fetch(`${this.authUrl}/api/auth/refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
refresh_token: refreshToken
})
});
if (!response.ok) {
if (response.status === 401) {
this.clearTokens();
if (this.config.authRequired) {
this.redirectToAuth();
}
}
return false;
}
const data = await response.json();
await this.setTokens({
access_token: data.access_token,
refresh_token: data.refresh_token,
token_type: data.token_type,
expires_in: data.expires_in,
refresh_expires_in: data.refresh_expires_in
}, true);
return true;
} catch (error) {
console.error("Token refresh failed:", error);
return false;
}
}
/**
* Add auth state change listener
*/
onAuthStateChanged(callback) {
this.listeners.add(callback);
queueMicrotask(() => {
try {
callback(this.authState);
} catch (error) {
console.error("Error in auth state change callback:", error);
}
});
return () => {
this.listeners.delete(callback);
};
}
/**
* Private helper methods
*/
async validateStoredTokens(tokens) {
try {
console.log("\u{1F50D} Validating stored tokens...");
if (this.isAccessTokenExpired()) {
console.log("\u23F0 Access token expired based on timestamp, attempting refresh...");
if (!tokens.refresh_token) {
console.log("\u274C No refresh token available");
return false;
}
if (this.isRefreshTokenExpired()) {
console.log("\u274C Refresh token also expired");
return false;
}
const refreshed = await this.refreshToken();
if (refreshed) {
console.log("\u2705 Token refreshed successfully during validation");
return true;
} else {
console.log("\u274C Token refresh failed during validation");
return false;
}
}
const response = await fetch(`${this.authUrl}/api/auth/me`, {
headers: {
"Authorization": `Bearer ${tokens.access_token}`
}
});
if (response.ok) {
const data = await response.json();
const user = data.user;
this.updateAuthState({
user,
tokens,
isAuthenticated: true,
isLoading: false
});
console.log("\u2705 Stored tokens are valid, user authenticated");
return true;
} else if (response.status === 401 && tokens.refresh_token) {
console.log("\u{1F504} Access token expired (server validation), attempting refresh...");
if (this.isRefreshTokenExpired()) {
console.log("\u274C Refresh token expired");
return false;
}
const refreshed = await this.refreshToken();
if (refreshed) {
console.log("\u2705 Token refreshed successfully after server validation");
return true;
} else {
console.log("\u274C Token refresh failed after server validation");
return false;
}
} else {
console.log("\u274C Token validation failed:", response.status, response.statusText);
return false;
}
} catch (error) {
console.log("\u{1F4A5} Error validating tokens:", error);
return false;
}
}
async setTokens(tokens, persist) {
const tokensWithTimestamp = {
...tokens,
issued_at: tokens.issued_at || Math.floor(Date.now() / 1e3)
};
console.log("\u{1F510} Setting tokens:", {
persist,
hasAccessToken: !!tokensWithTimestamp.access_token,
hasRefreshToken: !!tokensWithTimestamp.refresh_token,
expiresIn: tokensWithTimestamp.expires_in,
issuedAt: tokensWithTimestamp.issued_at
});
if (persist && typeof window !== "undefined") {
try {
localStorage.setItem("blink_tokens", JSON.stringify(tokensWithTimestamp));
console.log("\u{1F4BE} Tokens persisted to localStorage");
} catch (error) {
console.log("\u{1F4A5} Error persisting tokens to localStorage:", error);
if (error instanceof DOMException && error.name === "SecurityError") {
console.log("\u{1F6AB} localStorage access blocked - running in cross-origin iframe");
}
}
}
let user = null;
try {
console.log("\u{1F464} Fetching user data...");
const response = await fetch(`${this.authUrl}/api/auth/me`, {
headers: {
"Authorization": `Bearer ${tokensWithTimestamp.access_token}`
}
});
console.log("\u{1F4E1} User fetch response:", {
status: response.status,
statusText: response.statusText,
ok: response.ok
});
if (response.ok) {
const data = await response.json();
user = data.user;
console.log("\u2705 User data fetched successfully:", {
id: user?.id,
email: user?.email,
displayName: user?.displayName
});
} else {
console.log("\u274C Failed to fetch user data:", await response.text());
}
} catch (error) {
console.log("\u{1F4A5} Error fetching user data:", error);
}
this.updateAuthState({
user,
tokens: tokensWithTimestamp,
isAuthenticated: !!user,
isLoading: false
});
console.log("\u{1F3AF} Auth state updated:", {
hasUser: !!user,
isAuthenticated: !!user,
isLoading: false
});
}
clearTokens() {
if (typeof window !== "undefined") {
try {
localStorage.removeItem("blink_tokens");
} catch (error) {
console.log("\u{1F4A5} Error clearing tokens from localStorage:", error);
}
}
this.updateAuthState({
user: null,
tokens: null,
isAuthenticated: false,
isLoading: false
});
}
getStoredTokens() {
if (typeof window === "undefined") return null;
if (this.isIframe && this.parentWindowTokens) {
return this.parentWindowTokens;
}
try {
const stored = localStorage.getItem("blink_tokens");
console.log("\u{1F50D} Checking localStorage for tokens:", {
hasStoredData: !!stored,
storedLength: stored?.length || 0,
origin: window.location.origin,
isIframe: window.self !== window.top
});
if (stored) {
const tokens = JSON.parse(stored);
console.log("\u{1F4E6} Parsed stored tokens:", {
hasAccessToken: !!tokens.access_token,
hasRefreshToken: !!tokens.refresh_token,
tokenType: tokens.token_type,
expiresIn: tokens.expires_in
});
return tokens;
}
return null;
} catch (error) {
console.log("\u{1F4A5} Error accessing localStorage:", error);
if (error instanceof DOMException && error.name === "SecurityError") {
console.log("\u{1F6AB} localStorage access blocked - likely due to cross-origin iframe restrictions");
}
return null;
}
}
extractTokensFromUrl() {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
const accessToken = params.get("access_token");
const refreshToken = params.get("refresh_token");
console.log("\u{1F50D} Extracting tokens from URL:", {
url: window.location.href,
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
allParams: Object.fromEntries(params.entries())
});
if (accessToken) {
const tokens = {
access_token: accessToken,
refresh_token: refreshToken || void 0,
token_type: "Bearer",
expires_in: 15 * 60,
// 15 minutes default
refresh_expires_in: refreshToken ? 30 * 24 * 60 * 60 : void 0,
// 30 days default
issued_at: Math.floor(Date.now() / 1e3)
// Current timestamp
};
console.log("\u2705 Tokens extracted successfully:", {
hasAccessToken: !!tokens.access_token,
hasRefreshToken: !!tokens.refresh_token
});
return tokens;
}
console.log("\u274C No access token found in URL");
return null;
}
clearUrlTokens() {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.delete("access_token");
url.searchParams.delete("refresh_token");
url.searchParams.delete("token_type");
url.searchParams.delete("project_id");
url.searchParams.delete("expires_in");
url.searchParams.delete("refresh_expires_in");
url.searchParams.delete("state");
url.searchParams.delete("code");
url.searchParams.delete("error");
url.searchParams.delete("error_description");
window.history.replaceState({}, "", url.toString());
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
}
redirectToAuth() {
if (typeof window !== "undefined") {
this.login();
}
}
setLoading(loading) {
this.updateAuthState({
...this.authState,
isLoading: loading
});
}
updateAuthState(newState) {
this.authState = newState;
this.listeners.forEach((callback) => {
try {
callback(newState);
} catch (error) {
console.error("Error in auth state change callback:", error);
}
});
}
};
// src/database.ts
function camelToSnake3(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function generateSecureId() {
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
} else {
const timestamp = Date.now().toString(36);
const randomPart = Math.random().toString(36).substring(2, 15);
const extraRandom = Math.random().toString(36).substring(2, 15);
return `${timestamp}_${randomPart}_${extraRandom}`;
}
}
function ensureRecordId(record) {
if (!record.id) {
return { ...record, id: generateSecureId() };
}
return record;
}
var BlinkTable = class {
constructor(tableName, httpClient) {
this.tableName = tableName;
this.httpClient = httpClient;
this.actualTableName = camelToSnake3(tableName);
}
actualTableName;
/**
* Create a single record
*/
async create(data, options = {}) {
const record = ensureRecordId(data);
const response = await this.httpClient.dbPost(
this.actualTableName,
record,
{ returning: options.returning !== false }
);
const result = Array.isArray(response.d