@flarelink/client 0.2.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 +371 -0
- package/dist/index.cjs +508 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +276 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +501 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# @flarelink/client
|
|
2
|
+
|
|
3
|
+
Typed client SDK for [Flarelink](https://flarelink.dev) — auth + storage + database for the Cloudflare developer stack.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @flarelink/client
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createFlarelink } from '@flarelink/client';
|
|
11
|
+
|
|
12
|
+
const flarelink = createFlarelink({
|
|
13
|
+
url: 'https://myapp-auth.your-subdomain.workers.dev',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
await flarelink.auth.signUp({ email, password, name });
|
|
17
|
+
await flarelink.auth.signIn({ email, password });
|
|
18
|
+
const user = await flarelink.auth.getMe();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What you get
|
|
22
|
+
|
|
23
|
+
- `flarelink.auth.*` — email/password, magic-link, OAuth (Google + GitHub), email verification, password reset. Browser + server safe.
|
|
24
|
+
- `flarelink.storage.*` — file storage on R2. Every SDK call runs **server-side** (it needs the service key), but presigned URLs let the **browser** PUT/GET R2 directly — your bytes never go through Flarelink or your own server. See "Service key" below.
|
|
25
|
+
- `flarelink.from(...)` + `flarelink.sql\`...\`` — typed query builder for D1, plus a raw-SQL escape hatch. **Server-only.** Browser-side queries with row-level security policies are deferred to a later release.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
### 1. Create a Flarelink project
|
|
30
|
+
|
|
31
|
+
Sign up at [flarelink.jaan-f97.workers.dev](https://flarelink.jaan-f97.workers.dev), connect your Cloudflare account, and click **Provision new project** in the dashboard. Flarelink uploads an auth Worker to your CF account and gives you back its URL.
|
|
32
|
+
|
|
33
|
+
### 2. Add the deployment URL to your env
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# .env / .dev.vars / process.env / Cloudflare env binding
|
|
37
|
+
FLARELINK_AUTH_URL=https://myapp-auth.your-subdomain.workers.dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Configure trusted origins
|
|
41
|
+
|
|
42
|
+
In the Flarelink dashboard's Authentication page, add every URL your app runs on — production, staging, `http://localhost:3000` for dev. The Worker rejects requests from anywhere else with a 403. **This is the most common misconfiguration.**
|
|
43
|
+
|
|
44
|
+
### 4. Create the client and use it
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createFlarelink } from '@flarelink/client';
|
|
48
|
+
|
|
49
|
+
const flarelink = createFlarelink({ url: process.env.FLARELINK_AUTH_URL! });
|
|
50
|
+
|
|
51
|
+
// In a sign-up form:
|
|
52
|
+
const result = await flarelink.auth.signUp({
|
|
53
|
+
email: 'jane@example.com',
|
|
54
|
+
password: 'correct horse battery staple',
|
|
55
|
+
name: 'Jane',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// On a protected route:
|
|
59
|
+
const user = await flarelink.auth.getMe(); // null if not signed in
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Auth API
|
|
63
|
+
|
|
64
|
+
Every method sends `credentials: 'include'`, so the browser carries the session cookie automatically. On the server, you'll need to forward the `Cookie` header yourself (see SSR section below).
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// Sign up
|
|
68
|
+
await flarelink.auth.signUp({ email, password, name });
|
|
69
|
+
|
|
70
|
+
// Sign in
|
|
71
|
+
await flarelink.auth.signIn({ email, password });
|
|
72
|
+
|
|
73
|
+
// Magic link
|
|
74
|
+
await flarelink.auth.signInWithMagicLink('user@example.com');
|
|
75
|
+
|
|
76
|
+
// OAuth
|
|
77
|
+
await flarelink.auth.signInWithSocial('google'); // redirects to provider
|
|
78
|
+
await flarelink.auth.signInWithSocial('github', { noRedirect: true }); // returns URL
|
|
79
|
+
|
|
80
|
+
// Sign out
|
|
81
|
+
await flarelink.auth.signOut();
|
|
82
|
+
|
|
83
|
+
// Who's signed in?
|
|
84
|
+
const user = await flarelink.auth.getMe(); // User | null
|
|
85
|
+
const session = await flarelink.auth.getSession(); // Session | null
|
|
86
|
+
|
|
87
|
+
// Password reset (two steps)
|
|
88
|
+
await flarelink.auth.requestPasswordReset({
|
|
89
|
+
email: 'user@example.com',
|
|
90
|
+
redirectTo: 'https://myapp.com/reset', // your app's reset page
|
|
91
|
+
});
|
|
92
|
+
// ...user clicks the link, your page reads ?token=
|
|
93
|
+
await flarelink.auth.resetPassword({ token, newPassword });
|
|
94
|
+
|
|
95
|
+
// Email verification (manual trigger; v0.1 deployments default to auto-on-signup)
|
|
96
|
+
await flarelink.auth.sendVerificationEmail({ email });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Error handling
|
|
100
|
+
|
|
101
|
+
Auth failures are `AuthError` instances with BetterAuth's machine-readable `code`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { AuthError } from '@flarelink/client';
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await flarelink.auth.signIn({ email, password });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof AuthError && err.code === 'INVALID_PASSWORD') {
|
|
110
|
+
// …
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Server-only: storage + database
|
|
116
|
+
|
|
117
|
+
`flarelink.storage.*` and `flarelink.from(...)` require a per-project **service key**. The key grants full DB + R2 access — **never include it in a client-side bundle.** Read it from server env only.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// In server-side code (Next.js server action, SvelteKit +server.ts, a CF Worker, etc.)
|
|
121
|
+
const flarelink = createFlarelink({
|
|
122
|
+
url: process.env.FLARELINK_AUTH_URL!,
|
|
123
|
+
serviceKey: process.env.FLARELINK_SERVICE_KEY!, // never ship this to the browser
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The service key is shown once after project provisioning (in the Flarelink dashboard's secret-bundle modal). If you lose it, hit **Rotate service key** on the Authentication page — that invalidates the old one and surfaces a new one. Apps using the old key will get `INVALID_SERVICE_KEY` (401) immediately.
|
|
128
|
+
|
|
129
|
+
### Storage
|
|
130
|
+
|
|
131
|
+
The SDK methods always run **server-side** — every call needs the service key, which must never reach the browser. But uploads and downloads themselves go **browser → R2 directly**, with no server in the byte path. That's the whole point: your server hands out short-lived presigned URLs; the browser uses them to talk to R2.
|
|
132
|
+
|
|
133
|
+
Two patterns, depending on whether bytes need to move:
|
|
134
|
+
|
|
135
|
+
**Presigning** (server mints URL → browser uses it):
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// SERVER (Next.js route handler, SvelteKit +server.ts, Express, …):
|
|
139
|
+
const { url, signedHeaders } = await flarelink.storage
|
|
140
|
+
.from('uploads')
|
|
141
|
+
.createSignedUploadUrl('avatars/jane.png', { contentType: 'image/png' });
|
|
142
|
+
// Return `url` (and signedHeaders) to the browser via your API.
|
|
143
|
+
|
|
144
|
+
// BROWSER (anywhere):
|
|
145
|
+
await fetch(url, {
|
|
146
|
+
method: 'PUT',
|
|
147
|
+
headers: signedHeaders,
|
|
148
|
+
body: file, // a File, Blob, ArrayBuffer, etc.
|
|
149
|
+
});
|
|
150
|
+
// File is now on R2 — your server saw zero bytes.
|
|
151
|
+
|
|
152
|
+
// Same pattern for downloads — mint server-side, embed in browser:
|
|
153
|
+
const { url: dl } = await flarelink.storage
|
|
154
|
+
.from('uploads')
|
|
155
|
+
.createSignedDownloadUrl('avatars/jane.png');
|
|
156
|
+
// Return `dl` to the browser; <img src={dl} />, window.open(dl), etc.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Server-only** (no browser involvement):
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Delete an object
|
|
163
|
+
await flarelink.storage.from('uploads').remove(['avatars/old.png']);
|
|
164
|
+
|
|
165
|
+
// List objects under a prefix
|
|
166
|
+
const { objects, prefixes } = await flarelink.storage
|
|
167
|
+
.from('uploads')
|
|
168
|
+
.list({ prefix: 'avatars/' });
|
|
169
|
+
|
|
170
|
+
// All buckets on the customer's R2 account
|
|
171
|
+
const buckets = await flarelink.storage.listBuckets();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Throws `StorageError` (with `.code`: `INVALID_SERVICE_KEY` / `SERVICE_KEY_NOT_PROVISIONED` / `R2_NOT_CONFIGURED`).
|
|
175
|
+
|
|
176
|
+
### Database
|
|
177
|
+
|
|
178
|
+
`flarelink.from(table)` returns a chainable that resolves on `await`. The query builder is intentionally small — equality + AND in `where`, `orderBy`, `limit`, `offset`, plus `insert` / `update` / `delete` / `returning`. Anything more dynamic goes through `flarelink.sql\`…\``.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// SELECT
|
|
182
|
+
const users = await flarelink
|
|
183
|
+
.from('users')
|
|
184
|
+
.select(['id', 'email', 'active'])
|
|
185
|
+
.where({ active: true })
|
|
186
|
+
.orderBy('created_at', 'desc')
|
|
187
|
+
.limit(20);
|
|
188
|
+
// users.rows is typed if you parameterize: flarelink.from<{ id: string; ... }>('users')
|
|
189
|
+
|
|
190
|
+
// SELECT * is the default; .select() is only for narrowing
|
|
191
|
+
const everyone = await flarelink.from('users');
|
|
192
|
+
|
|
193
|
+
// IS NULL semantics
|
|
194
|
+
const unverified = await flarelink.from('users').where({ verified_at: null });
|
|
195
|
+
|
|
196
|
+
// INSERT (single row, multi-row, with RETURNING)
|
|
197
|
+
await flarelink.from('users').insert({ email: 'a@b.com', name: 'A' });
|
|
198
|
+
const created = await flarelink
|
|
199
|
+
.from('users')
|
|
200
|
+
.insert([{ email: 'a@b.com' }, { email: 'c@d.com' }])
|
|
201
|
+
.returning('*');
|
|
202
|
+
|
|
203
|
+
// UPDATE + DELETE
|
|
204
|
+
await flarelink.from('users').update({ active: false }).where({ id: 42 });
|
|
205
|
+
await flarelink.from('users').delete().where({ id: 99 });
|
|
206
|
+
|
|
207
|
+
// Raw SQL escape hatch — interpolated values are SAFE bind params,
|
|
208
|
+
// never concatenated into the SQL string.
|
|
209
|
+
const top = await flarelink.sql`
|
|
210
|
+
SELECT email, count(*) AS n
|
|
211
|
+
FROM events
|
|
212
|
+
WHERE created_at > ${cutoff}
|
|
213
|
+
GROUP BY email
|
|
214
|
+
ORDER BY n DESC
|
|
215
|
+
LIMIT 10
|
|
216
|
+
`;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Identifiers (table + column names) must match `/^[A-Za-z_][A-Za-z0-9_]*$/` — anything else throws `DatabaseError` with code `INVALID_IDENTIFIER` before the request is sent. There's no way for an interpolated value to inject SQL — all values go through bind params.
|
|
220
|
+
|
|
221
|
+
All results have shape `{ rows: T[], meta: { duration, rows_read?, rows_written?, last_row_id?, changes? } }`. Throws `DatabaseError` on failure with `.code` set to the underlying D1 error category (`D1_QUERY_FAILED`, `INVALID_IDENTIFIER`, `UNSUPPORTED_FILTER`, etc.).
|
|
222
|
+
|
|
223
|
+
The customer's D1 also holds Flarelink's auth tables (`user`, `account`, `verification`, `flarelink_config`) — `flarelink.from('user')` reads those just like any other table. Avoid creating customer tables with those names.
|
|
224
|
+
|
|
225
|
+
**Not yet supported** (use `flarelink.sql\`…\``): `IN (…)`, `>` / `<` / `LIKE`, `OR`, joins, transactions. These land in a later release alongside browser-side queries with row-level security policies.
|
|
226
|
+
|
|
227
|
+
## SSR cookie forwarding
|
|
228
|
+
|
|
229
|
+
In server frameworks (Next.js, SvelteKit, etc.), browser cookies aren't on the server `fetch` by default. Forward them manually with a custom fetch:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// Next.js example
|
|
233
|
+
import { cookies } from 'next/headers';
|
|
234
|
+
import { createFlarelink } from '@flarelink/client';
|
|
235
|
+
|
|
236
|
+
const flarelink = createFlarelink({
|
|
237
|
+
url: process.env.FLARELINK_AUTH_URL!,
|
|
238
|
+
fetch: (input, init) => {
|
|
239
|
+
const cookieHeader = cookies()
|
|
240
|
+
.getAll()
|
|
241
|
+
.map((c) => `${c.name}=${c.value}`)
|
|
242
|
+
.join('; ');
|
|
243
|
+
return fetch(input, {
|
|
244
|
+
...init,
|
|
245
|
+
headers: { ...init?.headers, cookie: cookieHeader },
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## AGENTS.md / Claude / Cursor
|
|
252
|
+
|
|
253
|
+
Paste this into your project's `AGENTS.md` / `CLAUDE.md` so AI agents helping you integrate Flarelink know what to do:
|
|
254
|
+
|
|
255
|
+
````markdown
|
|
256
|
+
# Flarelink integration (`@flarelink/client`)
|
|
257
|
+
|
|
258
|
+
This project uses `@flarelink/client` for auth, file storage, and database access against a Cloudflare-hosted Flarelink project.
|
|
259
|
+
|
|
260
|
+
## Setup
|
|
261
|
+
- `process.env.FLARELINK_AUTH_URL` — the project's auth Worker URL.
|
|
262
|
+
- `process.env.FLARELINK_SERVICE_KEY` — **server-only.** Required for storage + database. NEVER include in browser bundles.
|
|
263
|
+
- Trusted origins are configured in the Flarelink dashboard — every origin this app runs on must be listed there or requests come back 403.
|
|
264
|
+
|
|
265
|
+
## Client
|
|
266
|
+
Browser-safe (auth only):
|
|
267
|
+
```ts
|
|
268
|
+
import { createFlarelink } from '@flarelink/client';
|
|
269
|
+
const flarelink = createFlarelink({ url: process.env.FLARELINK_AUTH_URL! });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Server-side (auth + storage + db):
|
|
273
|
+
```ts
|
|
274
|
+
const flarelink = createFlarelink({
|
|
275
|
+
url: process.env.FLARELINK_AUTH_URL!,
|
|
276
|
+
serviceKey: process.env.FLARELINK_SERVICE_KEY!,
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Auth (browser + server)
|
|
281
|
+
- `flarelink.auth.signUp({ email, password, name })`
|
|
282
|
+
- `flarelink.auth.signIn({ email, password })`
|
|
283
|
+
- `flarelink.auth.signInWithMagicLink(email)`
|
|
284
|
+
- `flarelink.auth.signInWithSocial('google' | 'github')`
|
|
285
|
+
- `flarelink.auth.signOut()`
|
|
286
|
+
- `flarelink.auth.getMe()` → User | null
|
|
287
|
+
- `flarelink.auth.getSession()` → Session | null
|
|
288
|
+
- `flarelink.auth.requestPasswordReset({ email, redirectTo })`
|
|
289
|
+
- `flarelink.auth.resetPassword({ token, newPassword })`
|
|
290
|
+
- `flarelink.auth.sendVerificationEmail({ email })`
|
|
291
|
+
|
|
292
|
+
All send `credentials: 'include'`. On the server, forward cookies via the `fetch` option.
|
|
293
|
+
|
|
294
|
+
## Storage
|
|
295
|
+
All SDK calls run server-side (service key required). Two patterns:
|
|
296
|
+
|
|
297
|
+
**Presign + browser-direct** — server mints a short-lived URL, browser PUTs/GETs R2 directly:
|
|
298
|
+
```ts
|
|
299
|
+
// Server: mint a URL and return it to the client
|
|
300
|
+
const { url, signedHeaders } = await flarelink.storage
|
|
301
|
+
.from('bucket-name')
|
|
302
|
+
.createSignedUploadUrl('path/key.png', { contentType: 'image/png' });
|
|
303
|
+
// Then in the browser:
|
|
304
|
+
// await fetch(url, { method: 'PUT', headers: signedHeaders, body: file });
|
|
305
|
+
|
|
306
|
+
const { url: dl } = await flarelink.storage
|
|
307
|
+
.from('bucket-name')
|
|
308
|
+
.createSignedDownloadUrl('path/key.png');
|
|
309
|
+
// Use `dl` as <img src>, fetch it, redirect to it — anywhere a URL works.
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Server-only** — no browser path; the SDK call IS the operation:
|
|
313
|
+
```ts
|
|
314
|
+
await flarelink.storage.from('bucket-name').remove(['path/key.png']);
|
|
315
|
+
const { objects, prefixes } = await flarelink.storage
|
|
316
|
+
.from('bucket-name')
|
|
317
|
+
.list({ prefix: 'path/' });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Database (server-only)
|
|
321
|
+
```ts
|
|
322
|
+
// SELECT
|
|
323
|
+
const users = await flarelink
|
|
324
|
+
.from('users')
|
|
325
|
+
.select(['id', 'email'])
|
|
326
|
+
.where({ active: true })
|
|
327
|
+
.orderBy('created_at', 'desc')
|
|
328
|
+
.limit(20);
|
|
329
|
+
// → { rows: [...], meta: { duration, rows_read, ... } }
|
|
330
|
+
|
|
331
|
+
// INSERT (single, multi, with RETURNING)
|
|
332
|
+
await flarelink.from('users').insert({ email: 'a@b.com', name: 'A' });
|
|
333
|
+
const inserted = await flarelink
|
|
334
|
+
.from('users')
|
|
335
|
+
.insert([{ email: 'a' }, { email: 'b' }])
|
|
336
|
+
.returning('*');
|
|
337
|
+
|
|
338
|
+
// UPDATE / DELETE
|
|
339
|
+
await flarelink.from('users').update({ active: false }).where({ id: 42 });
|
|
340
|
+
await flarelink.from('users').delete().where({ id: 99 });
|
|
341
|
+
|
|
342
|
+
// Raw SQL escape hatch — interpolated values are SAFE bind params.
|
|
343
|
+
// Use this for: IN, range, LIKE, OR, joins, anything else.
|
|
344
|
+
const top = await flarelink.sql`
|
|
345
|
+
SELECT email, count(*) AS n FROM events
|
|
346
|
+
WHERE created_at > ${cutoff}
|
|
347
|
+
GROUP BY email ORDER BY n DESC LIMIT 10
|
|
348
|
+
`;
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The query builder only supports equality + AND in `where`. For `IN`, ranges, joins, etc., use `flarelink.sql\`…\`` — interpolated values are bind params, not concatenated strings, so SQL injection is impossible through that surface.
|
|
352
|
+
|
|
353
|
+
Flarelink's auth tables live in the same D1: `user`, `account`, `verification`, `flarelink_config`. Avoid naming customer tables with those names.
|
|
354
|
+
|
|
355
|
+
## Errors
|
|
356
|
+
- `AuthError` — auth failures (check `err.code`: `INVALID_PASSWORD`, `USER_NOT_FOUND`, `TOO_MANY_REQUESTS`, etc.)
|
|
357
|
+
- `StorageError` — storage failures (`INVALID_SERVICE_KEY`, `R2_NOT_CONFIGURED`)
|
|
358
|
+
- `DatabaseError` — db failures (`INVALID_IDENTIFIER`, `D1_QUERY_FAILED`, `UNSUPPORTED_FILTER`)
|
|
359
|
+
- `MissingServiceKeyError` — `serviceKey` not provided to `createFlarelink` but you tried to use storage/db
|
|
360
|
+
|
|
361
|
+
## Don't
|
|
362
|
+
- Don't include `FLARELINK_SERVICE_KEY` in client-side bundles. There is no "scoped" or "read-only" service key — leaking it = full DB + R2 access.
|
|
363
|
+
- Don't roll a custom session cookie — use `flarelink.auth.getMe()`.
|
|
364
|
+
- Don't store auth tokens manually — cookies are handled by the Worker.
|
|
365
|
+
- Don't concatenate user input into `flarelink.sql\`…\`` template parts. Always pass values as `${interpolations}` so they become bind params.
|
|
366
|
+
- Don't use `flarelink.from(...)` for anything more complex than `=` filters with AND. Use `flarelink.sql\`…\`` instead — it's the same `await`, just more flexible.
|
|
367
|
+
````
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT.
|