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