@blinkdotnew/sdk 0.17.4 → 0.18.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/README.md +640 -85
- package/dist/index.d.mts +209 -47
- package/dist/index.d.ts +209 -47
- package/dist/index.js +894 -124
- package/dist/index.mjs +894 -124
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,10 +27,22 @@ import { createClient } from '@blinkdotnew/sdk'
|
|
|
27
27
|
|
|
28
28
|
const blink = createClient({
|
|
29
29
|
projectId: 'your-blink-project-id', // From blink.new dashboard
|
|
30
|
-
authRequired:
|
|
30
|
+
authRequired: false // Don't force immediate auth - let users browse first
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
// Authentication (
|
|
33
|
+
// Authentication (flexible) - Two modes available
|
|
34
|
+
|
|
35
|
+
// For websites: Let users browse, require auth for protected areas
|
|
36
|
+
if (!blink.auth.isAuthenticated() && needsAuth) {
|
|
37
|
+
blink.auth.login() // Redirects to blink.new auth page when needed
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// For apps with custom auth UI (headless mode)
|
|
41
|
+
const user = await blink.auth.signUp({ email: 'user@example.com', password: 'secure123' })
|
|
42
|
+
const user = await blink.auth.signInWithEmail('user@example.com', 'secure123')
|
|
43
|
+
const user = await blink.auth.signInWithGoogle()
|
|
44
|
+
|
|
45
|
+
// Current user (works in both modes)
|
|
34
46
|
const user = await blink.auth.me()
|
|
35
47
|
|
|
36
48
|
// Database operations (zero config)
|
|
@@ -110,7 +122,7 @@ blink.analytics.enable()
|
|
|
110
122
|
// Storage operations (instant - returns public URL directly)
|
|
111
123
|
const { publicUrl } = await blink.storage.upload(
|
|
112
124
|
file,
|
|
113
|
-
`avatars/${user.id}.
|
|
125
|
+
`avatars/${user.id}-${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
|
|
114
126
|
{ upsert: true }
|
|
115
127
|
)
|
|
116
128
|
```
|
|
@@ -128,9 +140,9 @@ const { publicUrl } = await blink.storage.upload(
|
|
|
128
140
|
|
|
129
141
|
This SDK powers every Blink-generated app with:
|
|
130
142
|
|
|
131
|
-
- **🔐 Authentication**:
|
|
143
|
+
- **🔐 Authentication**: Flexible auth system with managed (redirect) and headless (custom UI) modes, email/password, social providers (Google, GitHub, Apple, Microsoft), magic links, RBAC, and custom email branding
|
|
132
144
|
- **🗄️ Database**: PostgREST-compatible CRUD operations with advanced filtering
|
|
133
|
-
- **🤖 AI**: Text generation with web search, object generation, image creation, speech synthesis, and transcription
|
|
145
|
+
- **🤖 AI**: Text generation with web search, object generation, image creation (Gemini 2.5 Flash), speech synthesis, and transcription
|
|
134
146
|
- **📄 Data**: Extract text content from documents, secure API proxy with secret substitution, web scraping, screenshots, and web search
|
|
135
147
|
- **📁 Storage**: File upload, download, and management
|
|
136
148
|
- **📧 Notifications**: Email sending with attachments, custom branding, and delivery tracking
|
|
@@ -152,10 +164,15 @@ import { createClient } from '@blinkdotnew/sdk'
|
|
|
152
164
|
|
|
153
165
|
const blink = createClient({
|
|
154
166
|
projectId: 'your-blink-project-id', // From blink.new dashboard
|
|
155
|
-
authRequired:
|
|
167
|
+
authRequired: false, // Let users browse first - require auth only for protected areas
|
|
168
|
+
auth: {
|
|
169
|
+
mode: 'managed' // Use new explicit configuration
|
|
170
|
+
}
|
|
156
171
|
})
|
|
157
172
|
```
|
|
158
173
|
|
|
174
|
+
> **⚠️ Version Requirement**: The flexible authentication system (managed vs headless modes) requires SDK version **0.18.0 or higher**. If you're using version 0.17.x or below, you'll only have access to the legacy authentication system. Please upgrade to access the new authentication features.
|
|
175
|
+
|
|
159
176
|
### Server-side (Node.js, Deno, Edge functions)
|
|
160
177
|
|
|
161
178
|
```typescript
|
|
@@ -163,7 +180,7 @@ import { createClient } from '@blinkdotnew/sdk'
|
|
|
163
180
|
|
|
164
181
|
const blink = createClient({
|
|
165
182
|
projectId: 'your-blink-project-id', // From blink.new dashboard
|
|
166
|
-
|
|
183
|
+
auth: { mode: 'managed' } // Manual token management
|
|
167
184
|
})
|
|
168
185
|
|
|
169
186
|
// Set JWT manually
|
|
@@ -174,11 +191,224 @@ blink.auth.setToken(jwtFromHeader)
|
|
|
174
191
|
|
|
175
192
|
### Authentication
|
|
176
193
|
|
|
194
|
+
> **⚠️ Version Requirement**: The flexible authentication system requires SDK version **0.18.0 or higher**. Version 0.17.x and below only support the legacy authentication system.
|
|
195
|
+
|
|
196
|
+
Blink provides **two authentication modes** for maximum flexibility:
|
|
197
|
+
|
|
198
|
+
#### Managed Mode (Default - Redirect-based)
|
|
199
|
+
|
|
200
|
+
Perfect for quick setup with Blink's hosted auth page:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// ⚠️ DEPRECATED: authRequired is legacy - use auth.mode instead
|
|
204
|
+
const blink = createClient({
|
|
205
|
+
projectId: 'your-project',
|
|
206
|
+
authRequired: false // Legacy - still works but deprecated
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// ✅ RECOMMENDED: Use new auth configuration
|
|
210
|
+
const blink = createClient({
|
|
211
|
+
projectId: 'your-project',
|
|
212
|
+
auth: {
|
|
213
|
+
mode: 'managed', // Explicit mode configuration
|
|
214
|
+
redirectUrl: 'https://myapp.com/dashboard'
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Simple redirect authentication
|
|
219
|
+
blink.auth.login() // Redirects to blink.new auth page
|
|
220
|
+
blink.auth.logout() // Clear tokens and redirect
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Headless Mode (Custom UI Control)
|
|
224
|
+
|
|
225
|
+
Build your own authentication UI with full control:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const blink = createClient({
|
|
229
|
+
projectId: 'your-project',
|
|
230
|
+
auth: {
|
|
231
|
+
mode: 'headless'
|
|
232
|
+
// Providers controlled via project settings
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Email/password authentication
|
|
237
|
+
const user = await blink.auth.signUp({
|
|
238
|
+
email: 'user@example.com',
|
|
239
|
+
password: 'SecurePass123!',
|
|
240
|
+
metadata: { displayName: 'John Doe' }
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const user = await blink.auth.signInWithEmail('user@example.com', 'password')
|
|
244
|
+
|
|
245
|
+
// Social provider authentication
|
|
246
|
+
const user = await blink.auth.signInWithGoogle()
|
|
247
|
+
const user = await blink.auth.signInWithGitHub()
|
|
248
|
+
const user = await blink.auth.signInWithApple()
|
|
249
|
+
const user = await blink.auth.signInWithMicrosoft()
|
|
250
|
+
|
|
251
|
+
// Magic links (passwordless)
|
|
252
|
+
await blink.auth.sendMagicLink('user@example.com', {
|
|
253
|
+
redirectUrl: 'https://myapp.com/dashboard'
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### 🔧 Provider Configuration
|
|
258
|
+
|
|
259
|
+
**Step 1: Enable Providers in Your Project**
|
|
260
|
+
1. Go to [blink.new](https://blink.new) and open your project
|
|
261
|
+
2. Navigate to **Project → Workspace → Authentication**
|
|
262
|
+
3. Toggle providers on/off:
|
|
263
|
+
- **Email** ✅ (enabled by default) - Includes email/password, magic links, and verification
|
|
264
|
+
- **Google** ✅ (enabled by default)
|
|
265
|
+
- **GitHub** ⚪ (disabled by default)
|
|
266
|
+
- **Apple** ⚪ (disabled by default)
|
|
267
|
+
- **Microsoft** ⚪ (disabled by default)
|
|
268
|
+
4. Configure email settings:
|
|
269
|
+
- **Require email verification**: Off by default (easier implementation)
|
|
270
|
+
- **Allow user signup**: On by default
|
|
271
|
+
|
|
272
|
+
**Step 2: Discover Available Providers**
|
|
273
|
+
```typescript
|
|
274
|
+
// Get providers enabled for your project
|
|
275
|
+
const availableProviders = await blink.auth.getAvailableProviders()
|
|
276
|
+
// Returns: ['email', 'google'] (based on your project settings)
|
|
277
|
+
|
|
278
|
+
// Use in your UI to show only enabled providers
|
|
279
|
+
const showGoogleButton = availableProviders.includes('google')
|
|
280
|
+
const showGitHubButton = availableProviders.includes('github')
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Step 3: Client-Side Filtering (Headless Mode)**
|
|
284
|
+
```typescript
|
|
285
|
+
const blink = createClient({
|
|
286
|
+
projectId: 'your-project',
|
|
287
|
+
auth: {
|
|
288
|
+
mode: 'headless'
|
|
289
|
+
// All providers controlled via project settings
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Managed Mode: Automatic Provider Display**
|
|
295
|
+
```typescript
|
|
296
|
+
const blink = createClient({
|
|
297
|
+
projectId: 'your-project',
|
|
298
|
+
auth: { mode: 'managed' }
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// The hosted auth page automatically shows only enabled providers
|
|
302
|
+
blink.auth.login() // Shows Email + Google by default
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### 📧 Email Verification Flow
|
|
306
|
+
|
|
307
|
+
**By default, email verification is NOT required** for easier implementation. Enable it only if needed:
|
|
308
|
+
|
|
309
|
+
**Step 1: Configure Verification (Optional)**
|
|
310
|
+
1. Go to [blink.new](https://blink.new) → Project → Workspace → Authentication
|
|
311
|
+
2. Toggle **"Require email verification"** ON (disabled by default)
|
|
312
|
+
|
|
313
|
+
**Step 2: Handle the Complete Flow**
|
|
314
|
+
```typescript
|
|
315
|
+
// User signup - always send verification email for security
|
|
316
|
+
try {
|
|
317
|
+
const user = await blink.auth.signUp({ email, password })
|
|
318
|
+
await blink.auth.sendEmailVerification()
|
|
319
|
+
setMessage('Account created! Check your email to verify.')
|
|
320
|
+
} catch (error) {
|
|
321
|
+
setError(error.message)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// User signin - handle verification requirement
|
|
325
|
+
try {
|
|
326
|
+
await blink.auth.signInWithEmail(email, password)
|
|
327
|
+
// Success - user is signed in
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error.code === 'EMAIL_NOT_VERIFIED') {
|
|
330
|
+
setError('Please verify your email first')
|
|
331
|
+
await blink.auth.sendEmailVerification() // Resend verification
|
|
332
|
+
} else {
|
|
333
|
+
setError(error.message)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Manual verification resend
|
|
338
|
+
await blink.auth.sendEmailVerification()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**What Happens:**
|
|
342
|
+
1. **Signup**: User account created, `email_verified = false`
|
|
343
|
+
2. **Verification Email**: User clicks link → `email_verified = true`
|
|
344
|
+
3. **Signin Check**: If verification required AND not verified → `EMAIL_NOT_VERIFIED` error
|
|
345
|
+
4. **Success**: User can sign in once verified (or if verification not required)
|
|
346
|
+
|
|
347
|
+
#### Flexible Email System
|
|
348
|
+
|
|
349
|
+
**Maximum flexibility** - you control email branding while Blink handles secure tokens:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Option A: Custom email delivery (full branding control)
|
|
353
|
+
const resetData = await blink.auth.generatePasswordResetToken('user@example.com')
|
|
354
|
+
// Returns: { token, expiresAt, resetUrl }
|
|
355
|
+
|
|
356
|
+
// Send with your email service and branding
|
|
357
|
+
await yourEmailService.send({
|
|
358
|
+
to: 'user@example.com',
|
|
359
|
+
subject: 'Reset your YourApp password',
|
|
360
|
+
html: `
|
|
361
|
+
<div style="font-family: Arial, sans-serif;">
|
|
362
|
+
<img src="https://yourapp.com/logo.png" alt="YourApp" />
|
|
363
|
+
<h1>Reset Your Password</h1>
|
|
364
|
+
<a href="${resetData.resetUrl}"
|
|
365
|
+
style="background: #0070f3; color: white; padding: 16px 32px;
|
|
366
|
+
text-decoration: none; border-radius: 8px;">
|
|
367
|
+
Reset Password
|
|
368
|
+
</a>
|
|
369
|
+
</div>
|
|
370
|
+
`
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// Option B: Blink default email (zero setup)
|
|
374
|
+
await blink.auth.sendPasswordResetEmail('user@example.com')
|
|
375
|
+
|
|
376
|
+
// Same flexibility for email verification and magic links
|
|
377
|
+
const verifyData = await blink.auth.generateEmailVerificationToken()
|
|
378
|
+
const magicData = await blink.auth.generateMagicLinkToken('user@example.com')
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Role-Based Access Control (RBAC)
|
|
382
|
+
|
|
177
383
|
```typescript
|
|
178
|
-
//
|
|
179
|
-
blink
|
|
180
|
-
|
|
384
|
+
// Configure roles and permissions
|
|
385
|
+
const blink = createClient({
|
|
386
|
+
projectId: 'your-project',
|
|
387
|
+
auth: {
|
|
388
|
+
mode: 'headless',
|
|
389
|
+
roles: {
|
|
390
|
+
admin: { permissions: ['*'] },
|
|
391
|
+
editor: { permissions: ['posts.create', 'posts.update'] },
|
|
392
|
+
viewer: { permissions: ['posts.read'] }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// Check permissions
|
|
398
|
+
const canEdit = blink.auth.can('posts.update')
|
|
399
|
+
const isAdmin = blink.auth.hasRole('admin')
|
|
400
|
+
const isStaff = blink.auth.hasRole(['admin', 'editor'])
|
|
401
|
+
|
|
402
|
+
// Use in components
|
|
403
|
+
function EditButton() {
|
|
404
|
+
if (!blink.auth.can('posts.update')) return null
|
|
405
|
+
return <button onClick={editPost}>Edit Post</button>
|
|
406
|
+
}
|
|
407
|
+
```
|
|
181
408
|
|
|
409
|
+
#### Core Methods
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
182
412
|
// User management
|
|
183
413
|
const user = await blink.auth.me()
|
|
184
414
|
await blink.auth.updateMe({ displayName: 'New Name' })
|
|
@@ -187,6 +417,16 @@ await blink.auth.updateMe({ displayName: 'New Name' })
|
|
|
187
417
|
blink.auth.setToken(jwt, persist?)
|
|
188
418
|
const isAuth = blink.auth.isAuthenticated()
|
|
189
419
|
|
|
420
|
+
// Password management
|
|
421
|
+
await blink.auth.changePassword('oldPass', 'newPass')
|
|
422
|
+
await blink.auth.confirmPasswordReset(token, newPassword)
|
|
423
|
+
|
|
424
|
+
// Email verification
|
|
425
|
+
await blink.auth.verifyEmail(token)
|
|
426
|
+
|
|
427
|
+
// Provider discovery
|
|
428
|
+
const providers = await blink.auth.getAvailableProviders()
|
|
429
|
+
|
|
190
430
|
// Auth state listener (REQUIRED for React apps!)
|
|
191
431
|
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
|
|
192
432
|
console.log('Auth state:', state)
|
|
@@ -285,7 +525,6 @@ await blink.db.todos.upsertMany([...])
|
|
|
285
525
|
// Text generation (simple prompt)
|
|
286
526
|
const { text } = await blink.ai.generateText({
|
|
287
527
|
prompt: 'Write a poem about coding',
|
|
288
|
-
model: 'gpt-4o-mini',
|
|
289
528
|
maxTokens: 150
|
|
290
529
|
})
|
|
291
530
|
|
|
@@ -306,6 +545,7 @@ const { text } = await blink.ai.generateText({
|
|
|
306
545
|
// Text generation with image content
|
|
307
546
|
// ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
|
|
308
547
|
// For file uploads, use blink.storage.upload() first to get public HTTPS URLs
|
|
548
|
+
// 🚫 CRITICAL: When uploading files, NEVER hardcode extensions - use file.name or auto-detection
|
|
309
549
|
const { text } = await blink.ai.generateText({
|
|
310
550
|
messages: [
|
|
311
551
|
{
|
|
@@ -380,26 +620,97 @@ const { object: todoList } = await blink.ai.generateObject({
|
|
|
380
620
|
// })
|
|
381
621
|
// Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"
|
|
382
622
|
|
|
383
|
-
// Generate and modify images with AI
|
|
623
|
+
// Generate and modify images with AI (powered by Gemini 2.5 Flash Image)
|
|
624
|
+
// 🔥 Automatic optimization - no need to specify size, quality, or style parameters
|
|
625
|
+
// 🎨 For style transfer: provide ALL images in the images array, don't reference URLs in prompts
|
|
384
626
|
|
|
385
627
|
// Basic image generation - returns public URLs directly
|
|
386
628
|
const { data } = await blink.ai.generateImage({
|
|
387
|
-
prompt: 'A serene landscape with mountains and a lake at sunset'
|
|
388
|
-
size: '1024x1024',
|
|
389
|
-
quality: 'high',
|
|
390
|
-
n: 2
|
|
629
|
+
prompt: 'A serene landscape with mountains and a lake at sunset'
|
|
391
630
|
})
|
|
392
631
|
console.log('Image URL:', data[0].url)
|
|
393
632
|
|
|
633
|
+
// Multiple images
|
|
634
|
+
const { data } = await blink.ai.generateImage({
|
|
635
|
+
prompt: 'A futuristic robot in different poses',
|
|
636
|
+
n: 3
|
|
637
|
+
})
|
|
638
|
+
data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url))
|
|
639
|
+
|
|
394
640
|
// Image editing - transform existing images with prompts
|
|
395
641
|
const { data: headshots } = await blink.ai.modifyImage({
|
|
396
|
-
images: ['https://storage.example.com/user-photo.jpg'], //
|
|
397
|
-
prompt: '
|
|
398
|
-
|
|
399
|
-
|
|
642
|
+
images: ['https://storage.example.com/user-photo.jpg'], // Up to 50 images supported!
|
|
643
|
+
prompt: 'Transform into professional business headshot with studio lighting'
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
// Advanced image editing with multiple input images
|
|
647
|
+
const { data } = await blink.ai.modifyImage({
|
|
648
|
+
images: [
|
|
649
|
+
'https://storage.example.com/photo1.jpg',
|
|
650
|
+
'https://storage.example.com/photo2.jpg',
|
|
651
|
+
'https://storage.example.com/photo3.jpg'
|
|
652
|
+
],
|
|
653
|
+
prompt: 'Combine these architectural styles into a futuristic building design',
|
|
654
|
+
n: 2
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// 🎨 Style Transfer & Feature Application
|
|
658
|
+
// ⚠️ IMPORTANT: When applying styles/features from one image to another,
|
|
659
|
+
// provide ALL images in the array - don't reference images in the prompt text
|
|
660
|
+
|
|
661
|
+
// ❌ WRONG - Don't reference images in prompt
|
|
662
|
+
const wrongWay = await blink.ai.modifyImage({
|
|
663
|
+
images: [userPhotoUrl],
|
|
664
|
+
prompt: `Apply this hairstyle from the reference image to the person's head. Reference hairstyle: ${hairstyleUrl}`
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
// ✅ CORRECT - Provide all images in the array
|
|
668
|
+
const { data } = await blink.ai.modifyImage({
|
|
669
|
+
images: [userPhotoUrl, hairstyleUrl], // Both images provided
|
|
670
|
+
prompt: 'Apply the hairstyle from the second image to the person in the first image. Blend naturally with face shape.'
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
// More style transfer examples
|
|
674
|
+
const { data } = await blink.ai.modifyImage({
|
|
675
|
+
images: [portraitUrl, artworkUrl],
|
|
676
|
+
prompt: 'Apply the artistic style and color palette from the second image to the portrait in the first image'
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
const { data } = await blink.ai.modifyImage({
|
|
680
|
+
images: [roomPhotoUrl, designReferenceUrl],
|
|
681
|
+
prompt: 'Redesign the room in the first image using the interior design style shown in the second image'
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
// Multiple reference images for complex transformations
|
|
685
|
+
const { data } = await blink.ai.modifyImage({
|
|
686
|
+
images: [
|
|
687
|
+
originalPhotoUrl,
|
|
688
|
+
lightingReferenceUrl,
|
|
689
|
+
colorPaletteReferenceUrl,
|
|
690
|
+
compositionReferenceUrl
|
|
691
|
+
],
|
|
692
|
+
prompt: 'Transform the first image using the lighting style from image 2, color palette from image 3, and composition from image 4'
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
// 📱 File Upload + Style Transfer
|
|
696
|
+
// ⚠️ Extract file extension properly - never hardcode .jpg/.png
|
|
697
|
+
|
|
698
|
+
// ❌ WRONG - Hardcoded extension
|
|
699
|
+
const userUpload = await blink.storage.upload(file, `photos/${Date.now()}.jpg`) // Breaks HEIC/PNG files
|
|
700
|
+
// ✅ CORRECT - Extract original extension
|
|
701
|
+
const userUpload = await blink.storage.upload(
|
|
702
|
+
userPhoto.file,
|
|
703
|
+
`photos/${Date.now()}.${userPhoto.file.name.split('.').pop()}`
|
|
704
|
+
)
|
|
705
|
+
const hairstyleUpload = await blink.storage.upload(
|
|
706
|
+
hairstylePhoto.file,
|
|
707
|
+
`haircuts/${Date.now()}.${hairstylePhoto.file.name.split('.').pop()}`
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
const { data } = await blink.ai.modifyImage({
|
|
711
|
+
images: [userUpload.publicUrl, hairstyleUpload.publicUrl],
|
|
712
|
+
prompt: 'Apply hairstyle from second image to person in first image'
|
|
400
713
|
})
|
|
401
|
-
// Options: size ('1024x1024', '1536x1024'), quality ('auto'|'low'|'medium'|'high'),
|
|
402
|
-
// background ('auto'|'transparent'|'opaque'), n (1-10 images)
|
|
403
714
|
|
|
404
715
|
|
|
405
716
|
// Speech synthesis
|
|
@@ -732,20 +1043,17 @@ try {
|
|
|
732
1043
|
// Upload files (returns public URL directly)
|
|
733
1044
|
const { publicUrl } = await blink.storage.upload(
|
|
734
1045
|
file,
|
|
735
|
-
|
|
1046
|
+
`uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
|
|
736
1047
|
{
|
|
737
1048
|
upsert: true,
|
|
738
1049
|
onProgress: (percent) => console.log(`${percent}%`)
|
|
739
1050
|
}
|
|
740
1051
|
)
|
|
741
|
-
// If file is PNG, final path will be: path/to/file.png
|
|
742
1052
|
|
|
743
|
-
//
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
{ upsert: true }
|
|
748
|
-
)
|
|
1053
|
+
// ❌ WRONG - Hardcoded extensions break HEIC/PNG/WebP files
|
|
1054
|
+
const wrong = await blink.storage.upload(file, `uploads/${Date.now()}.jpg`) // Corrupts non-JPG files
|
|
1055
|
+
// ✅ CORRECT - Extract file extension
|
|
1056
|
+
const correct = await blink.storage.upload(file, `uploads/${Date.now()}.${file.name.split('.').pop()}`)
|
|
749
1057
|
|
|
750
1058
|
// Remove files
|
|
751
1059
|
await blink.storage.remove('file1.jpg', 'file2.jpg')
|
|
@@ -1209,14 +1517,44 @@ try {
|
|
|
1209
1517
|
### Error Handling
|
|
1210
1518
|
|
|
1211
1519
|
```typescript
|
|
1212
|
-
import {
|
|
1213
|
-
|
|
1520
|
+
import {
|
|
1521
|
+
BlinkAuthError,
|
|
1522
|
+
BlinkAuthErrorCode,
|
|
1523
|
+
BlinkAIError,
|
|
1524
|
+
BlinkStorageError,
|
|
1525
|
+
BlinkDataError,
|
|
1526
|
+
BlinkRealtimeError,
|
|
1527
|
+
BlinkNotificationsError
|
|
1528
|
+
} from '@blinkdotnew/sdk'
|
|
1529
|
+
|
|
1530
|
+
// Authentication error handling
|
|
1214
1531
|
try {
|
|
1215
|
-
const user = await blink.auth.
|
|
1532
|
+
const user = await blink.auth.signInWithEmail(email, password)
|
|
1216
1533
|
} catch (error) {
|
|
1217
1534
|
if (error instanceof BlinkAuthError) {
|
|
1218
|
-
|
|
1219
|
-
|
|
1535
|
+
switch (error.code) {
|
|
1536
|
+
case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
|
|
1537
|
+
console.log('Email verification required')
|
|
1538
|
+
await blink.auth.sendEmailVerification()
|
|
1539
|
+
break
|
|
1540
|
+
case BlinkAuthErrorCode.INVALID_CREDENTIALS:
|
|
1541
|
+
console.error('Invalid email or password')
|
|
1542
|
+
break
|
|
1543
|
+
case BlinkAuthErrorCode.RATE_LIMITED:
|
|
1544
|
+
console.error('Too many attempts, try again later')
|
|
1545
|
+
break
|
|
1546
|
+
default:
|
|
1547
|
+
console.error('Auth error:', error.message)
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Other error types
|
|
1553
|
+
try {
|
|
1554
|
+
const { text } = await blink.ai.generateText({ prompt: 'Hello' })
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
if (error instanceof BlinkAIError) {
|
|
1557
|
+
console.error('AI error:', error.message)
|
|
1220
1558
|
}
|
|
1221
1559
|
}
|
|
1222
1560
|
```
|
|
@@ -1227,7 +1565,18 @@ try {
|
|
|
1227
1565
|
const blink = createClient({
|
|
1228
1566
|
projectId: 'your-project',
|
|
1229
1567
|
baseUrl: 'https://custom-api.example.com',
|
|
1230
|
-
|
|
1568
|
+
auth: {
|
|
1569
|
+
mode: 'headless', // 'managed' | 'headless'
|
|
1570
|
+
authUrl: 'https://custom-auth.example.com', // Custom auth domain
|
|
1571
|
+
coreUrl: 'https://custom-core.example.com', // Custom API domain
|
|
1572
|
+
// Providers controlled via project settings
|
|
1573
|
+
redirectUrl: 'https://myapp.com/dashboard',
|
|
1574
|
+
roles: {
|
|
1575
|
+
admin: { permissions: ['*'] },
|
|
1576
|
+
editor: { permissions: ['posts.create', 'posts.update'], inherit: ['viewer'] },
|
|
1577
|
+
viewer: { permissions: ['posts.read'] }
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1231
1580
|
httpClient: {
|
|
1232
1581
|
timeout: 30000,
|
|
1233
1582
|
retries: 3
|
|
@@ -1428,6 +1777,220 @@ function MyRealtimeComponent() {
|
|
|
1428
1777
|
|
|
1429
1778
|
### React
|
|
1430
1779
|
|
|
1780
|
+
#### Authentication Integration
|
|
1781
|
+
|
|
1782
|
+
**Complete authentication example with custom UI:**
|
|
1783
|
+
|
|
1784
|
+
```typescript
|
|
1785
|
+
import { createClient } from '@blinkdotnew/sdk'
|
|
1786
|
+
import { useState, useEffect } from 'react'
|
|
1787
|
+
|
|
1788
|
+
// ✅ RECOMMENDED: Don't force immediate auth - let users browse first
|
|
1789
|
+
const blink = createClient({
|
|
1790
|
+
projectId: 'your-project',
|
|
1791
|
+
authRequired: false, // Let users browse public areas
|
|
1792
|
+
auth: {
|
|
1793
|
+
mode: 'headless' // Use headless for custom UI
|
|
1794
|
+
// Providers controlled via project settings
|
|
1795
|
+
}
|
|
1796
|
+
})
|
|
1797
|
+
|
|
1798
|
+
function App() {
|
|
1799
|
+
const [user, setUser] = useState(null)
|
|
1800
|
+
const [loading, setLoading] = useState(true)
|
|
1801
|
+
const [currentPage, setCurrentPage] = useState('home')
|
|
1802
|
+
|
|
1803
|
+
// ✅ CRITICAL: Always use onAuthStateChanged
|
|
1804
|
+
useEffect(() => {
|
|
1805
|
+
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
|
|
1806
|
+
setUser(state.user)
|
|
1807
|
+
setLoading(state.isLoading)
|
|
1808
|
+
})
|
|
1809
|
+
return unsubscribe
|
|
1810
|
+
}, [])
|
|
1811
|
+
|
|
1812
|
+
// Protected route check - only require auth for dashboard/account areas
|
|
1813
|
+
const requiresAuth = ['dashboard', 'account', 'settings'].includes(currentPage)
|
|
1814
|
+
|
|
1815
|
+
if (loading) return <div>Loading...</div>
|
|
1816
|
+
if (requiresAuth && !user) return <AuthForm />
|
|
1817
|
+
|
|
1818
|
+
// Public pages (home, pricing, about) don't require auth
|
|
1819
|
+
if (currentPage === 'home') return <HomePage />
|
|
1820
|
+
if (currentPage === 'pricing') return <PricingPage />
|
|
1821
|
+
|
|
1822
|
+
// Protected pages require authentication
|
|
1823
|
+
return <Dashboard user={user} />
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function AuthForm() {
|
|
1827
|
+
const [mode, setMode] = useState('signin') // 'signin' | 'signup' | 'reset'
|
|
1828
|
+
const [email, setEmail] = useState('')
|
|
1829
|
+
const [password, setPassword] = useState('')
|
|
1830
|
+
const [error, setError] = useState('')
|
|
1831
|
+
const [message, setMessage] = useState('')
|
|
1832
|
+
|
|
1833
|
+
const handleEmailAuth = async (e) => {
|
|
1834
|
+
e.preventDefault()
|
|
1835
|
+
setError('')
|
|
1836
|
+
|
|
1837
|
+
try {
|
|
1838
|
+
if (mode === 'signup') {
|
|
1839
|
+
const user = await blink.auth.signUp({ email, password })
|
|
1840
|
+
setMessage('Account created! Please check your email to verify.')
|
|
1841
|
+
await blink.auth.sendEmailVerification()
|
|
1842
|
+
} else if (mode === 'signin') {
|
|
1843
|
+
await blink.auth.signInWithEmail(email, password)
|
|
1844
|
+
// User state updates automatically via onAuthStateChanged
|
|
1845
|
+
} else if (mode === 'reset') {
|
|
1846
|
+
await blink.auth.sendPasswordResetEmail(email)
|
|
1847
|
+
setMessage('Password reset email sent!')
|
|
1848
|
+
}
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
if (err.code === 'EMAIL_NOT_VERIFIED') {
|
|
1851
|
+
setError('Please verify your email first')
|
|
1852
|
+
await blink.auth.sendEmailVerification()
|
|
1853
|
+
} else {
|
|
1854
|
+
setError(err.message)
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const handleSocialAuth = async (provider) => {
|
|
1860
|
+
try {
|
|
1861
|
+
await blink.auth.signInWithProvider(provider)
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
setError(err.message)
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
return (
|
|
1868
|
+
<div style={{ maxWidth: '400px', margin: '0 auto', padding: '2rem' }}>
|
|
1869
|
+
<h1>{mode === 'signin' ? 'Sign In' : mode === 'signup' ? 'Sign Up' : 'Reset Password'}</h1>
|
|
1870
|
+
|
|
1871
|
+
{error && <div style={{ color: 'red', marginBottom: '1rem' }}>{error}</div>}
|
|
1872
|
+
{message && <div style={{ color: 'green', marginBottom: '1rem' }}>{message}</div>}
|
|
1873
|
+
|
|
1874
|
+
<form onSubmit={handleEmailAuth} style={{ marginBottom: '1rem' }}>
|
|
1875
|
+
<input
|
|
1876
|
+
type="email"
|
|
1877
|
+
placeholder="Email"
|
|
1878
|
+
value={email}
|
|
1879
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
1880
|
+
style={{ width: '100%', padding: '0.5rem', marginBottom: '0.5rem' }}
|
|
1881
|
+
/>
|
|
1882
|
+
|
|
1883
|
+
{mode !== 'reset' && (
|
|
1884
|
+
<input
|
|
1885
|
+
type="password"
|
|
1886
|
+
placeholder="Password"
|
|
1887
|
+
value={password}
|
|
1888
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
1889
|
+
style={{ width: '100%', padding: '0.5rem', marginBottom: '1rem' }}
|
|
1890
|
+
/>
|
|
1891
|
+
)}
|
|
1892
|
+
|
|
1893
|
+
<button type="submit" style={{ width: '100%', padding: '0.75rem', marginBottom: '1rem' }}>
|
|
1894
|
+
{mode === 'signin' ? 'Sign In' : mode === 'signup' ? 'Create Account' : 'Send Reset Email'}
|
|
1895
|
+
</button>
|
|
1896
|
+
</form>
|
|
1897
|
+
|
|
1898
|
+
{mode !== 'reset' && (
|
|
1899
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
|
1900
|
+
<button onClick={() => handleSocialAuth('google')} style={{ flex: 1, padding: '0.5rem' }}>
|
|
1901
|
+
Google
|
|
1902
|
+
</button>
|
|
1903
|
+
<button onClick={() => handleSocialAuth('github')} style={{ flex: 1, padding: '0.5rem' }}>
|
|
1904
|
+
GitHub
|
|
1905
|
+
</button>
|
|
1906
|
+
</div>
|
|
1907
|
+
)}
|
|
1908
|
+
|
|
1909
|
+
<div style={{ textAlign: 'center', fontSize: '0.875rem' }}>
|
|
1910
|
+
{mode === 'signin' && (
|
|
1911
|
+
<>
|
|
1912
|
+
<button onClick={() => setMode('signup')} style={{ marginRight: '1rem' }}>
|
|
1913
|
+
Create Account
|
|
1914
|
+
</button>
|
|
1915
|
+
<button onClick={() => setMode('reset')}>Forgot Password?</button>
|
|
1916
|
+
</>
|
|
1917
|
+
)}
|
|
1918
|
+
{mode === 'signup' && (
|
|
1919
|
+
<button onClick={() => setMode('signin')}>Already have an account?</button>
|
|
1920
|
+
)}
|
|
1921
|
+
{mode === 'reset' && (
|
|
1922
|
+
<button onClick={() => setMode('signin')}>Back to Sign In</button>
|
|
1923
|
+
)}
|
|
1924
|
+
</div>
|
|
1925
|
+
</div>
|
|
1926
|
+
)
|
|
1927
|
+
}
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
#### Custom Email Branding Example
|
|
1931
|
+
|
|
1932
|
+
**Send password reset with your own email service:**
|
|
1933
|
+
|
|
1934
|
+
```typescript
|
|
1935
|
+
function PasswordResetForm() {
|
|
1936
|
+
const [email, setEmail] = useState('')
|
|
1937
|
+
const [message, setMessage] = useState('')
|
|
1938
|
+
|
|
1939
|
+
const handleReset = async (e) => {
|
|
1940
|
+
e.preventDefault()
|
|
1941
|
+
|
|
1942
|
+
try {
|
|
1943
|
+
// Generate secure token (no email sent by Blink)
|
|
1944
|
+
const resetData = await blink.auth.generatePasswordResetToken(email)
|
|
1945
|
+
|
|
1946
|
+
// Send with your own email service and branding
|
|
1947
|
+
await yourEmailService.send({
|
|
1948
|
+
to: email,
|
|
1949
|
+
from: 'support@yourapp.com',
|
|
1950
|
+
subject: 'Reset your YourApp password',
|
|
1951
|
+
html: `
|
|
1952
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 600px; margin: 0 auto;">
|
|
1953
|
+
<div style="text-align: center; padding: 40px;">
|
|
1954
|
+
<img src="https://yourapp.com/logo.svg" alt="YourApp" style="width: 120px; margin-bottom: 32px;" />
|
|
1955
|
+
<h1 style="color: #1a1a1a; font-size: 28px; margin-bottom: 16px;">Reset Your Password</h1>
|
|
1956
|
+
<p style="color: #666; font-size: 16px; margin-bottom: 32px;">
|
|
1957
|
+
We received a request to reset your YourApp password.
|
|
1958
|
+
</p>
|
|
1959
|
+
<a href="${resetData.resetUrl}"
|
|
1960
|
+
style="display: inline-block; background: #0070f3; color: white;
|
|
1961
|
+
padding: 16px 32px; text-decoration: none; border-radius: 8px;
|
|
1962
|
+
font-weight: 600; font-size: 16px;">
|
|
1963
|
+
Reset My Password
|
|
1964
|
+
</a>
|
|
1965
|
+
<p style="color: #999; font-size: 14px; margin-top: 32px;">
|
|
1966
|
+
This link expires in 1 hour. If you didn't request this, you can ignore this email.
|
|
1967
|
+
</p>
|
|
1968
|
+
</div>
|
|
1969
|
+
</div>
|
|
1970
|
+
`
|
|
1971
|
+
})
|
|
1972
|
+
|
|
1973
|
+
setMessage('Password reset email sent! Check your inbox.')
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
console.error('Reset failed:', error.message)
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
return (
|
|
1980
|
+
<form onSubmit={handleReset}>
|
|
1981
|
+
<input
|
|
1982
|
+
type="email"
|
|
1983
|
+
value={email}
|
|
1984
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
1985
|
+
placeholder="Enter your email"
|
|
1986
|
+
/>
|
|
1987
|
+
<button type="submit">Send Reset Link</button>
|
|
1988
|
+
{message && <p>{message}</p>}
|
|
1989
|
+
</form>
|
|
1990
|
+
)
|
|
1991
|
+
}
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1431
1994
|
**⚠️ Critical: Always Use Auth State Listener, Never One-Time Checks**
|
|
1432
1995
|
|
|
1433
1996
|
The most common authentication mistake is checking auth status once instead of listening to changes:
|
|
@@ -1458,61 +2021,45 @@ useEffect(() => {
|
|
|
1458
2021
|
}, [])
|
|
1459
2022
|
```
|
|
1460
2023
|
|
|
1461
|
-
|
|
1462
|
-
1. App loads → `blink.auth.me()` called immediately
|
|
1463
|
-
2. Auth still initializing → Call fails, user set to `null`
|
|
1464
|
-
3. Auth completes later → App never knows because it only checked once
|
|
1465
|
-
4. User stuck on "Please sign in" screen forever
|
|
1466
|
-
|
|
1467
|
-
**Always use `onAuthStateChanged()` for React apps!**
|
|
2024
|
+
#### Error Handling Example
|
|
1468
2025
|
|
|
1469
2026
|
```typescript
|
|
1470
|
-
import {
|
|
1471
|
-
import { useState, useEffect } from 'react'
|
|
1472
|
-
|
|
1473
|
-
const blink = createClient({ projectId: 'your-project', authRequired: true })
|
|
1474
|
-
|
|
1475
|
-
function App() {
|
|
1476
|
-
const [user, setUser] = useState(null)
|
|
1477
|
-
const [loading, setLoading] = useState(true)
|
|
1478
|
-
|
|
1479
|
-
useEffect(() => {
|
|
1480
|
-
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
|
|
1481
|
-
setUser(state.user)
|
|
1482
|
-
setLoading(state.isLoading)
|
|
1483
|
-
})
|
|
1484
|
-
return unsubscribe
|
|
1485
|
-
}, [])
|
|
1486
|
-
|
|
1487
|
-
if (loading) return <div>Loading...</div>
|
|
1488
|
-
if (!user) return <div>Please log in</div>
|
|
2027
|
+
import { BlinkAuthError, BlinkAuthErrorCode } from '@blinkdotnew/sdk'
|
|
1489
2028
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
// ❌ WRONG - One-time auth check (misses auth state changes)
|
|
1494
|
-
function BadApp() {
|
|
1495
|
-
const [user, setUser] = useState(null)
|
|
1496
|
-
const [loading, setLoading] = useState(true)
|
|
2029
|
+
function AuthForm() {
|
|
2030
|
+
const [error, setError] = useState('')
|
|
1497
2031
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
2032
|
+
const handleAuth = async () => {
|
|
2033
|
+
try {
|
|
2034
|
+
await blink.auth.signInWithEmail(email, password)
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
if (err instanceof BlinkAuthError) {
|
|
2037
|
+
switch (err.code) {
|
|
2038
|
+
case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
|
|
2039
|
+
setError('Please verify your email first')
|
|
2040
|
+
await blink.auth.sendEmailVerification()
|
|
2041
|
+
break
|
|
2042
|
+
case BlinkAuthErrorCode.INVALID_CREDENTIALS:
|
|
2043
|
+
setError('Invalid email or password')
|
|
2044
|
+
break
|
|
2045
|
+
case BlinkAuthErrorCode.RATE_LIMITED:
|
|
2046
|
+
setError('Too many attempts. Please try again later.')
|
|
2047
|
+
break
|
|
2048
|
+
default:
|
|
2049
|
+
setError('Authentication failed. Please try again.')
|
|
2050
|
+
}
|
|
1507
2051
|
}
|
|
1508
2052
|
}
|
|
2053
|
+
}
|
|
1509
2054
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2055
|
+
return (
|
|
2056
|
+
<div>
|
|
2057
|
+
{error && <div style={{ color: 'red' }}>{error}</div>}
|
|
2058
|
+
{/* Auth form */}
|
|
2059
|
+
</div>
|
|
2060
|
+
)
|
|
1515
2061
|
}
|
|
2062
|
+
```
|
|
1516
2063
|
|
|
1517
2064
|
// React example with search functionality
|
|
1518
2065
|
function SearchResults() {
|
|
@@ -1749,7 +2296,11 @@ function RealtimeChat() {
|
|
|
1749
2296
|
// pages/api/todos.ts
|
|
1750
2297
|
import { createClient } from '@blinkdotnew/sdk'
|
|
1751
2298
|
|
|
1752
|
-
|
|
2299
|
+
// ✅ RECOMMENDED: Use new auth configuration
|
|
2300
|
+
const blink = createClient({
|
|
2301
|
+
projectId: 'your-project',
|
|
2302
|
+
auth: { mode: 'managed' } // Explicit configuration
|
|
2303
|
+
})
|
|
1753
2304
|
|
|
1754
2305
|
export default async function handler(req, res) {
|
|
1755
2306
|
const jwt = req.headers.authorization?.replace('Bearer ', '')
|
|
@@ -1765,7 +2316,11 @@ export default async function handler(req, res) {
|
|
|
1765
2316
|
```typescript
|
|
1766
2317
|
import { createClient } from '@blinkdotnew/sdk'
|
|
1767
2318
|
|
|
1768
|
-
|
|
2319
|
+
// ✅ RECOMMENDED: Use new auth configuration
|
|
2320
|
+
const blink = createClient({
|
|
2321
|
+
projectId: 'your-project',
|
|
2322
|
+
auth: { mode: 'managed' } // Explicit configuration
|
|
2323
|
+
})
|
|
1769
2324
|
|
|
1770
2325
|
Deno.serve(async (req) => {
|
|
1771
2326
|
const jwt = req.headers.get('authorization')?.replace('Bearer ', '')
|