UNPKG

@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
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