@archlast/client 0.1.1 → 0.1.2
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 +69 -855
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,713 +1,149 @@
|
|
|
1
1
|
# @archlast/client
|
|
2
2
|
|
|
3
|
-
The Archlast client SDK provides
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- [Installation](#installation)
|
|
8
|
-
- [Quick Start](#quick-start)
|
|
9
|
-
- [Client Configuration](#client-configuration)
|
|
10
|
-
- [React Hooks](#react-hooks)
|
|
11
|
-
- [useQuery](#usequery)
|
|
12
|
-
- [useMutation](#usemutation)
|
|
13
|
-
- [usePagination](#usepagination)
|
|
14
|
-
- [useUpload](#useupload)
|
|
15
|
-
- [useAuth](#useauth)
|
|
16
|
-
- [Direct Client Usage](#direct-client-usage)
|
|
17
|
-
- [Authentication](#authentication)
|
|
18
|
-
- [File Storage](#file-storage)
|
|
19
|
-
- [Admin APIs](#admin-apis)
|
|
20
|
-
- [tRPC Integration](#trpc-integration)
|
|
21
|
-
- [Subpath Exports](#subpath-exports)
|
|
22
|
-
- [TypeScript Support](#typescript-support)
|
|
23
|
-
- [Error Handling](#error-handling)
|
|
24
|
-
- [Advanced Patterns](#advanced-patterns)
|
|
3
|
+
The Archlast client SDK provides WebSocket-based queries and mutations, React
|
|
4
|
+
hooks with cache integration, authentication helpers, storage utilities, admin
|
|
5
|
+
APIs, and optional tRPC access.
|
|
25
6
|
|
|
26
7
|
## Installation
|
|
27
8
|
|
|
28
9
|
```bash
|
|
29
10
|
npm install @archlast/client
|
|
30
11
|
|
|
31
|
-
#
|
|
12
|
+
# Other package managers
|
|
32
13
|
pnpm add @archlast/client
|
|
33
14
|
yarn add @archlast/client
|
|
34
15
|
bun add @archlast/client
|
|
35
16
|
```
|
|
36
17
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Install required peer dependencies in your application:
|
|
18
|
+
Peer dependencies:
|
|
40
19
|
|
|
41
20
|
```bash
|
|
42
21
|
npm install react react-dom @tanstack/react-query
|
|
43
22
|
```
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
For tRPC support:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
npm install @trpc/client @trpc/react-query
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Quick Start
|
|
54
|
-
|
|
55
|
-
### 1. Create the Client
|
|
24
|
+
## Quick start
|
|
56
25
|
|
|
57
26
|
```ts
|
|
58
27
|
import { ArchlastClient } from "@archlast/client";
|
|
28
|
+
import { ArchlastProvider, useQuery, useMutation } from "@archlast/client/react";
|
|
29
|
+
import { api } from "./_generated/api";
|
|
59
30
|
|
|
60
31
|
const client = new ArchlastClient(
|
|
61
|
-
"ws://localhost:4000/ws",
|
|
62
|
-
"http://localhost:4000",
|
|
63
|
-
"web"
|
|
64
|
-
undefined, // Optional: auth URL (for same-origin proxy)
|
|
65
|
-
{
|
|
66
|
-
autoConnect: true, // Auto-connect WebSocket (default: true)
|
|
67
|
-
isAdmin: false, // Admin mode (default: false)
|
|
68
|
-
apiKey: "arch_key", // Optional: Better-Auth API key
|
|
69
|
-
betterAuthCookies: {}, // Optional: Better-Auth session cookies
|
|
70
|
-
}
|
|
32
|
+
"ws://localhost:4000/ws",
|
|
33
|
+
"http://localhost:4000",
|
|
34
|
+
"web"
|
|
71
35
|
);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### 2. Set Up the Provider
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
import { ArchlastProvider } from "@archlast/client/react";
|
|
78
36
|
|
|
79
37
|
function App() {
|
|
80
38
|
return (
|
|
81
39
|
<ArchlastProvider client={client}>
|
|
82
|
-
<
|
|
40
|
+
<TodoList />
|
|
83
41
|
</ArchlastProvider>
|
|
84
42
|
);
|
|
85
43
|
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### 3. Use Hooks in Components
|
|
89
|
-
|
|
90
|
-
```tsx
|
|
91
|
-
import { useQuery, useMutation } from "@archlast/client/react";
|
|
92
|
-
import { api } from "./_generated/api";
|
|
93
44
|
|
|
94
45
|
function TodoList() {
|
|
95
|
-
// Subscribe to live updates
|
|
96
46
|
const tasks = useQuery(api.tasks.list, {});
|
|
97
|
-
|
|
98
|
-
// Mutation with automatic cache invalidation
|
|
99
47
|
const createTask = useMutation(api.tasks.create);
|
|
100
48
|
|
|
101
|
-
const handleAdd = async () => {
|
|
102
|
-
await createTask({ text: "New task" });
|
|
103
|
-
// Cache automatically invalidated, UI updates reactively
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (!tasks) return <div>Loading...</div>;
|
|
107
|
-
|
|
108
49
|
return (
|
|
109
50
|
<div>
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
</ul>
|
|
115
|
-
<button onClick={handleAdd}>Add Task</button>
|
|
51
|
+
<pre>{JSON.stringify(tasks, null, 2)}</pre>
|
|
52
|
+
<button onClick={() => createTask({ text: "New task" })}>
|
|
53
|
+
Add
|
|
54
|
+
</button>
|
|
116
55
|
</div>
|
|
117
56
|
);
|
|
118
57
|
}
|
|
119
58
|
```
|
|
120
59
|
|
|
121
|
-
|
|
60
|
+
The `api` object is generated by the CLI in `_generated/api.ts`.
|
|
122
61
|
|
|
123
|
-
|
|
62
|
+
## Client configuration
|
|
124
63
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
### Constructor Signature
|
|
64
|
+
Constructor signature:
|
|
128
65
|
|
|
129
66
|
```ts
|
|
130
|
-
new ArchlastClient(wsUrl, httpUrl
|
|
67
|
+
new ArchlastClient(wsUrl, httpUrl?, appId?, authUrl?, options?)
|
|
131
68
|
```
|
|
132
69
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
| `httpUrl` | `string` | HTTP base URL (e.g., `http://localhost:4000`) |
|
|
139
|
-
| `appId` | `string` | Application identifier (e.g., `"web"`, `"mobile"`) |
|
|
140
|
-
| `authUrl` | `string?` | Optional custom auth URL (defaults to `httpUrl`) |
|
|
141
|
-
| `options` | `object?` | Additional configuration options |
|
|
142
|
-
|
|
143
|
-
### Options
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
interface ClientOptions {
|
|
147
|
-
// Auto-connect WebSocket on instantiation (default: true)
|
|
148
|
-
autoConnect?: boolean;
|
|
149
|
-
|
|
150
|
-
// API key for authentication (arch_ prefix)
|
|
151
|
-
apiKey?: string;
|
|
152
|
-
|
|
153
|
-
// Enable admin WebSocket subscriptions
|
|
154
|
-
isAdmin?: boolean;
|
|
155
|
-
|
|
156
|
-
// Better Auth session cookies for SSR
|
|
157
|
-
betterAuthCookies?: Record<string, string>;
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### Full Example
|
|
70
|
+
Options:
|
|
71
|
+
- `autoConnect` (default true)
|
|
72
|
+
- `apiKey` Better Auth API key (`arch_` prefix)
|
|
73
|
+
- `isAdmin` subscribe to admin streams
|
|
74
|
+
- `betterAuthCookies` for session cookies
|
|
162
75
|
|
|
163
76
|
```ts
|
|
164
77
|
const client = new ArchlastClient(
|
|
165
|
-
"
|
|
166
|
-
"
|
|
78
|
+
"ws://localhost:4000/ws",
|
|
79
|
+
"http://localhost:4000",
|
|
167
80
|
"web",
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
autoConnect: true,
|
|
171
|
-
apiKey: "arch_your_api_key",
|
|
172
|
-
isAdmin: false,
|
|
173
|
-
betterAuthCookies: {
|
|
174
|
-
"better-auth.session": "session_token_here"
|
|
175
|
-
}
|
|
176
|
-
}
|
|
81
|
+
undefined,
|
|
82
|
+
{ apiKey: "arch_example" }
|
|
177
83
|
);
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Runtime Credential Updates
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
// Rotate API key
|
|
184
|
-
client.setApiKey("arch_new_api_key");
|
|
185
|
-
|
|
186
|
-
// Update session cookies (for SSR)
|
|
187
|
-
client.setBetterAuthCookies({
|
|
188
|
-
"better-auth.session": "new_session_token"
|
|
189
|
-
});
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## React Hooks
|
|
195
|
-
|
|
196
|
-
### useQuery
|
|
197
|
-
|
|
198
|
-
Subscribe to real-time query updates with automatic caching via TanStack Query.
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
import { useQuery } from "@archlast/client/react";
|
|
202
|
-
import { api } from "./_generated/api";
|
|
203
84
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const tasks = useQuery(api.tasks.list, {});
|
|
207
|
-
|
|
208
|
-
// With arguments
|
|
209
|
-
const task = useQuery(api.tasks.get, { id: "123" });
|
|
210
|
-
|
|
211
|
-
// With options
|
|
212
|
-
const { data, isLoading, error, refetch } = useQuery(
|
|
213
|
-
api.tasks.list,
|
|
214
|
-
{ completed: false },
|
|
215
|
-
{
|
|
216
|
-
// TanStack Query options
|
|
217
|
-
staleTime: 5000,
|
|
218
|
-
refetchOnWindowFocus: true,
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
if (isLoading) return <Spinner />;
|
|
223
|
-
if (error) return <Error message={error.message} />;
|
|
224
|
-
|
|
225
|
-
return <TaskGrid tasks={data} />;
|
|
226
|
-
}
|
|
85
|
+
client.setApiKey("arch_new_key");
|
|
86
|
+
client.setBetterAuthCookies({ "better-auth.session": "..." });
|
|
227
87
|
```
|
|
228
88
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Execute mutations with automatic cache invalidation.
|
|
89
|
+
## React hooks
|
|
232
90
|
|
|
233
|
-
|
|
234
|
-
import { useMutation } from "@archlast/client/react";
|
|
91
|
+
Available from `@archlast/client/react`:
|
|
235
92
|
|
|
236
|
-
|
|
237
|
-
|
|
93
|
+
- `useQuery` / `useMutation` for live queries and mutations
|
|
94
|
+
- `usePagination` for cursor based queries
|
|
95
|
+
- `useAuth` for Better Auth session state
|
|
96
|
+
- `useUpload`, `useDownload` for storage
|
|
97
|
+
- `useStorageList`, `useStorageMetadata`, `useStorageDeleteFile`
|
|
238
98
|
|
|
239
|
-
|
|
240
|
-
e.preventDefault();
|
|
241
|
-
const formData = new FormData(e.target as HTMLFormElement);
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const task = await createTask({
|
|
245
|
-
text: formData.get("text") as string,
|
|
246
|
-
priority: "medium"
|
|
247
|
-
});
|
|
248
|
-
console.log("Created:", task._id);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.error("Failed:", error);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<form onSubmit={handleSubmit}>
|
|
256
|
-
<input name="text" required />
|
|
257
|
-
<button type="submit">Create</button>
|
|
258
|
-
</form>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### usePagination
|
|
264
|
-
|
|
265
|
-
Handle cursor-based pagination with infinite scroll support.
|
|
99
|
+
Example:
|
|
266
100
|
|
|
267
101
|
```ts
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
function InfiniteTaskList() {
|
|
271
|
-
const {
|
|
272
|
-
items, // Accumulated items across all pages
|
|
273
|
-
cursor, // Current cursor for next page
|
|
274
|
-
isDone, // Whether pagination is complete
|
|
275
|
-
hasMore, // Convenience flag: !isDone && cursor exists
|
|
276
|
-
isLoading, // Loading state
|
|
277
|
-
loadMore, // Function to load next page
|
|
278
|
-
refresh, // Function to reset and reload from first page
|
|
279
|
-
page, // Optional: current page number (if server provides)
|
|
280
|
-
pageSize, // Optional: page size (if server provides)
|
|
281
|
-
total, // Optional: total count (if server provides)
|
|
282
|
-
} = usePagination(
|
|
283
|
-
api.tasks.listPaginated,
|
|
284
|
-
{ completed: false }, // Base args (no cursor/limit)
|
|
285
|
-
{ limit: 20 } // Options
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
return (
|
|
289
|
-
<div>
|
|
290
|
-
<ul>
|
|
291
|
-
{items.map(task => (
|
|
292
|
-
<li key={task._id}>{task.text}</li>
|
|
293
|
-
))}
|
|
294
|
-
</ul>
|
|
295
|
-
|
|
296
|
-
{hasMore && (
|
|
297
|
-
<button
|
|
298
|
-
onClick={loadMore}
|
|
299
|
-
disabled={isLoading}
|
|
300
|
-
>
|
|
301
|
-
{isLoading ? "Loading..." : "Load More"}
|
|
302
|
-
</button>
|
|
303
|
-
)}
|
|
304
|
-
|
|
305
|
-
<button onClick={refresh}>Refresh</button>
|
|
306
|
-
</div>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
102
|
+
const items = useQuery(api.items.list, { limit: 10 });
|
|
103
|
+
const createItem = useMutation(api.items.create);
|
|
309
104
|
```
|
|
310
105
|
|
|
311
|
-
|
|
106
|
+
## Direct client usage
|
|
312
107
|
|
|
313
108
|
```ts
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
continueCursor: string | null;
|
|
317
|
-
isDone: boolean;
|
|
318
|
-
page?: number; // Optional
|
|
319
|
-
pageSize?: number; // Optional
|
|
320
|
-
total?: number; // Optional
|
|
321
|
-
}
|
|
322
|
-
continueCursor: string | null;
|
|
323
|
-
isDone: boolean;
|
|
324
|
-
}
|
|
109
|
+
const data = await client.fetch("tasks.get", { id: "123" });
|
|
110
|
+
const updated = await client.mutate("tasks.update", { id: "123", text: "Hi" });
|
|
325
111
|
```
|
|
326
112
|
|
|
327
|
-
### useUpload
|
|
328
|
-
|
|
329
|
-
Upload files with progress tracking.
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
import { useUpload } from "@archlast/client/react";
|
|
333
|
-
|
|
334
|
-
function FileUploader() {
|
|
335
|
-
const {
|
|
336
|
-
upload,
|
|
337
|
-
progress,
|
|
338
|
-
isUploading,
|
|
339
|
-
storageId,
|
|
340
|
-
error
|
|
341
|
-
} = useUpload();
|
|
342
|
-
|
|
343
|
-
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
|
344
|
-
const file = e.target.files?.[0];
|
|
345
|
-
if (!file) return;
|
|
346
|
-
|
|
347
|
-
const id = await upload(file, "images");
|
|
348
|
-
console.log("Uploaded with ID:", id);
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
return (
|
|
352
|
-
<div>
|
|
353
|
-
<input type="file" onChange={handleFileChange} disabled={isUploading} />
|
|
354
|
-
|
|
355
|
-
{isUploading && (
|
|
356
|
-
<div>
|
|
357
|
-
<progress value={progress} max={100} />
|
|
358
|
-
<span>{progress}%</span>
|
|
359
|
-
</div>
|
|
360
|
-
)}
|
|
361
|
-
|
|
362
|
-
{storageId && <p>Uploaded: {storageId}</p>}
|
|
363
|
-
{error && <p className="error">{error.message}</p>}
|
|
364
|
-
</div>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### useAuth
|
|
370
|
-
|
|
371
|
-
Authentication state and actions via the `ArchlastAuthClient` wrapper.
|
|
372
|
-
|
|
373
|
-
```ts
|
|
374
|
-
import { useAuth } from "@archlast/client/react";
|
|
375
|
-
|
|
376
|
-
function AuthButton() {
|
|
377
|
-
const {
|
|
378
|
-
isAuthenticated,
|
|
379
|
-
user,
|
|
380
|
-
isLoading,
|
|
381
|
-
signIn,
|
|
382
|
-
signOut
|
|
383
|
-
} = useAuth();
|
|
384
|
-
|
|
385
|
-
if (isLoading) return <Spinner />;
|
|
386
|
-
|
|
387
|
-
if (isAuthenticated) {
|
|
388
|
-
return (
|
|
389
|
-
<div>
|
|
390
|
-
<span>Welcome, {user?.name}</span>
|
|
391
|
-
<button onClick={() => signOut()}>
|
|
392
|
-
Sign Out
|
|
393
|
-
</button>
|
|
394
|
-
</div>
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return (
|
|
399
|
-
<button onClick={() => signIn({ email: "user@example.com", password: "..." })}>
|
|
400
|
-
Sign In
|
|
401
|
-
</button>
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### Using Better-Auth React Hooks
|
|
407
|
-
|
|
408
|
-
For full-featured auth in React, use Better-Auth's `useSession` hook:
|
|
409
|
-
|
|
410
|
-
```tsx
|
|
411
|
-
import { authClient } from "./lib/better-auth"; // Your Better-Auth client
|
|
412
|
-
|
|
413
|
-
function AuthButton() {
|
|
414
|
-
const { data: session, isPending } = authClient.useSession();
|
|
415
|
-
|
|
416
|
-
if (isPending) return <Spinner />;
|
|
417
|
-
|
|
418
|
-
if (session?.user) {
|
|
419
|
-
return (
|
|
420
|
-
<div>
|
|
421
|
-
<span>Welcome, {session.user.name}</span>
|
|
422
|
-
<button onClick={() => authClient.signOut()}>
|
|
423
|
-
Sign Out
|
|
424
|
-
</button>
|
|
425
|
-
</div>
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return (
|
|
430
|
-
<button onClick={() => authClient.signIn.email({ email, password })}>
|
|
431
|
-
Sign In
|
|
432
|
-
</button>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
---
|
|
438
|
-
|
|
439
|
-
## Direct Client Usage
|
|
440
|
-
|
|
441
|
-
Use the client directly without React for Node.js, scripts, or non-React frameworks.
|
|
442
|
-
|
|
443
|
-
### Fetch (Query)
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
// Fetch a single query
|
|
447
|
-
const tasks = await client.fetch("tasks.list", {});
|
|
448
|
-
|
|
449
|
-
// With arguments
|
|
450
|
-
const task = await client.fetch("tasks.get", { id: "123" });
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Mutate
|
|
454
|
-
|
|
455
|
-
```ts
|
|
456
|
-
// Execute a mutation
|
|
457
|
-
const created = await client.mutate("tasks.create", {
|
|
458
|
-
text: "New task",
|
|
459
|
-
priority: "high"
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// Update
|
|
463
|
-
await client.mutate("tasks.update", {
|
|
464
|
-
id: "123",
|
|
465
|
-
completed: true
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
// Delete
|
|
469
|
-
await client.mutate("tasks.delete", { id: "123" });
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### Subscribe
|
|
473
|
-
|
|
474
|
-
```ts
|
|
475
|
-
// Manual WebSocket subscription
|
|
476
|
-
const unsubscribe = client.subscribe("tasks.list", {}, (data) => {
|
|
477
|
-
console.log("Tasks updated:", data);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// Later: unsubscribe
|
|
481
|
-
unsubscribe();
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
---
|
|
485
|
-
|
|
486
113
|
## Authentication
|
|
487
114
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
### Using the Auth Client
|
|
491
|
-
|
|
492
|
-
The `ArchlastAuthClient` provides a wrapper around Better-Auth endpoints:
|
|
493
|
-
|
|
494
|
-
```ts
|
|
495
|
-
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
496
|
-
|
|
497
|
-
const auth = new ArchlastAuthClient({
|
|
498
|
-
baseUrl: "http://localhost:4000",
|
|
499
|
-
apiKey: "arch_your_api_key" // Optional: for programmatic access
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// Sign up with email/password
|
|
503
|
-
// POST /api/auth/sign-up/email
|
|
504
|
-
await auth.signUp({
|
|
505
|
-
email: "user@example.com",
|
|
506
|
-
password: "secure_password",
|
|
507
|
-
name: "John Doe"
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Sign in with email/password
|
|
511
|
-
// POST /api/auth/sign-in/email
|
|
512
|
-
await auth.signIn({
|
|
513
|
-
email: "user@example.com",
|
|
514
|
-
password: "secure_password"
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// Sign in with username (if username plugin enabled)
|
|
518
|
-
await auth.signIn({
|
|
519
|
-
username: "johndoe",
|
|
520
|
-
password: "secure_password"
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// Get current session
|
|
524
|
-
// GET /api/auth/get-session
|
|
525
|
-
const state = await auth.getState();
|
|
526
|
-
console.log(state.isAuthenticated, state.user);
|
|
527
|
-
|
|
528
|
-
// Sign out
|
|
529
|
-
await auth.signOut();
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
### Using Better-Auth Client Directly
|
|
533
|
-
|
|
534
|
-
For full Better-Auth features (OAuth, organization, admin), use the Better-Auth client directly:
|
|
115
|
+
Auth uses Better Auth endpoints under `/api/auth/*`.
|
|
535
116
|
|
|
536
117
|
```ts
|
|
537
|
-
import { createAuthClient } from "better-auth/react";
|
|
538
|
-
import {
|
|
539
|
-
adminClient,
|
|
540
|
-
usernameClient,
|
|
541
|
-
organizationClient,
|
|
542
|
-
// Optional plugins (add as needed):
|
|
543
|
-
// anonymousClient, // For guest users
|
|
544
|
-
// apiKeyClient, // For API key management
|
|
545
|
-
} from "better-auth/client/plugins";
|
|
546
|
-
|
|
547
|
-
const authClient = createAuthClient({
|
|
548
|
-
baseURL: "http://localhost:4000/api/auth",
|
|
549
|
-
fetchOptions: { credentials: "include" },
|
|
550
|
-
plugins: [
|
|
551
|
-
adminClient(),
|
|
552
|
-
usernameClient(),
|
|
553
|
-
organizationClient(),
|
|
554
|
-
],
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// Email sign in (typical for user-facing apps)
|
|
558
|
-
await authClient.signIn.email({
|
|
559
|
-
email: "user@example.com",
|
|
560
|
-
password: "secure_password",
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// Username sign in (used by dashboard)
|
|
564
|
-
await authClient.signIn.username({
|
|
565
|
-
username: "admin",
|
|
566
|
-
password: "secure_password",
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Session management
|
|
570
|
-
const { data: session } = authClient.useSession();
|
|
571
|
-
|
|
572
|
-
// Organization management
|
|
573
|
-
await authClient.organization.create({ name: "My Team" });
|
|
574
|
-
|
|
575
|
-
// Admin operations (requires admin role)
|
|
576
|
-
await authClient.admin.listUsers({ limit: 10 });
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
### Auth State in Components
|
|
580
|
-
|
|
581
|
-
```tsx
|
|
582
118
|
import { useAuth } from "@archlast/client/react";
|
|
583
119
|
|
|
584
|
-
|
|
585
|
-
const { isAuthenticated, isLoading } = useAuth();
|
|
586
|
-
|
|
587
|
-
if (isLoading) return <LoadingScreen />;
|
|
588
|
-
if (!isAuthenticated) return <Navigate to="/login" />;
|
|
589
|
-
|
|
590
|
-
return children;
|
|
591
|
-
}
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
---
|
|
595
|
-
|
|
596
|
-
## File Storage
|
|
597
|
-
|
|
598
|
-
### Storage Client
|
|
599
|
-
|
|
600
|
-
```ts
|
|
601
|
-
// Access via main client
|
|
602
|
-
const storage = client.storage;
|
|
603
|
-
|
|
604
|
-
// Or import directly
|
|
605
|
-
import { StorageClient } from "@archlast/client/storage";
|
|
606
|
-
const storage = new StorageClient(httpUrl, getAuthHeaders);
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
### Upload Files
|
|
610
|
-
|
|
611
|
-
```ts
|
|
612
|
-
const file = new File(["Hello, World!"], "hello.txt", { type: "text/plain" });
|
|
613
|
-
|
|
614
|
-
// Basic upload
|
|
615
|
-
const result = await client.storage.upload(file);
|
|
616
|
-
console.log("Storage ID:", result.id);
|
|
617
|
-
console.log("URL:", result.url);
|
|
618
|
-
|
|
619
|
-
// Upload with explicit content type
|
|
620
|
-
const blob = new Blob(["data"], { type: "application/octet-stream" });
|
|
621
|
-
const binaryResult = await client.storage.upload(blob, "application/octet-stream");
|
|
120
|
+
const { signIn, signOut, isAuthenticated } = useAuth();
|
|
622
121
|
```
|
|
623
122
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
### List Files
|
|
123
|
+
Or use the auth client directly:
|
|
627
124
|
|
|
628
125
|
```ts
|
|
629
|
-
|
|
630
|
-
const { items } = await client.storage.list(20, 0); // limit, offset
|
|
631
|
-
|
|
632
|
-
// Next page
|
|
633
|
-
const page2 = await client.storage.list(20, 20);
|
|
634
|
-
|
|
635
|
-
// Items structure
|
|
636
|
-
items.forEach(file => {
|
|
637
|
-
console.log(file.id); // Storage ID
|
|
638
|
-
console.log(file.fileName); // Original filename
|
|
639
|
-
console.log(file.contentType); // MIME type
|
|
640
|
-
console.log(file.size); // Size in bytes
|
|
641
|
-
console.log(file.url); // Public URL
|
|
642
|
-
});
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
### Get Presigned URLs
|
|
646
|
-
|
|
647
|
-
```ts
|
|
648
|
-
// Get temporary signed URL for download
|
|
649
|
-
const { url, expiresAt } = await client.storage.presign(storageId);
|
|
126
|
+
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
650
127
|
|
|
651
|
-
|
|
652
|
-
<img src={url} alt="Uploaded file" />
|
|
128
|
+
const auth = new ArchlastAuthClient({ baseUrl: "http://localhost:4000" });
|
|
653
129
|
```
|
|
654
130
|
|
|
655
|
-
|
|
131
|
+
## Storage
|
|
656
132
|
|
|
657
133
|
```ts
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
console.log(metadata.createdAt); // Date
|
|
134
|
+
const file = new File(["hello"], "hello.txt", { type: "text/plain" });
|
|
135
|
+
const uploaded = await client.storage.upload(file);
|
|
136
|
+
const list = await client.storage.list();
|
|
137
|
+
const { url } = await client.storage.presign(uploaded.id);
|
|
663
138
|
```
|
|
664
139
|
|
|
665
|
-
---
|
|
666
|
-
|
|
667
140
|
## Admin APIs
|
|
668
141
|
|
|
669
|
-
Access admin functionality (requires admin privileges).
|
|
670
|
-
|
|
671
|
-
### Admin Auth Client
|
|
672
|
-
|
|
673
|
-
The `AdminClient` currently provides authentication management:
|
|
674
|
-
|
|
675
142
|
```ts
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const admin = new AdminClient("http://localhost:4000", {
|
|
679
|
-
apiKey: "arch_your_api_key" // Optional: Better-Auth API key
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
// Admin authentication
|
|
683
|
-
await admin.auth.signIn(email, password);
|
|
684
|
-
const { user } = await admin.auth.getProfile();
|
|
685
|
-
await admin.auth.signOut();
|
|
686
|
-
|
|
687
|
-
// Session management
|
|
688
|
-
const { sessions } = await admin.auth.getSessions();
|
|
689
|
-
await admin.auth.revokeSession(sessionId);
|
|
690
|
-
await admin.auth.signOutAll();
|
|
143
|
+
const profile = await client.admin.auth.getProfile();
|
|
691
144
|
```
|
|
692
145
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
- `admin.auth.signIn(email, password)` - Admin sign in
|
|
696
|
-
- `admin.auth.signOut()` - Sign out current session
|
|
697
|
-
- `admin.auth.getProfile()` - Get admin profile
|
|
698
|
-
- `admin.auth.getSessions()` - List all sessions
|
|
699
|
-
- `admin.auth.revokeSession(sessionId)` - Revoke specific session
|
|
700
|
-
- `admin.auth.signOutAll()` - Sign out from all sessions
|
|
701
|
-
|
|
702
|
-
**Note:** Additional admin clients (users, data, API keys) may be added in future releases.
|
|
703
|
-
|
|
704
|
-
---
|
|
705
|
-
|
|
706
|
-
## tRPC Integration
|
|
707
|
-
|
|
708
|
-
For type-safe RPC with full TypeScript inference.
|
|
709
|
-
|
|
710
|
-
### Setup
|
|
146
|
+
## tRPC integration
|
|
711
147
|
|
|
712
148
|
```ts
|
|
713
149
|
import { createArchlastTRPCClient } from "@archlast/client/trpc";
|
|
@@ -715,246 +151,24 @@ import type { AppRouter } from "./_generated/rpc";
|
|
|
715
151
|
|
|
716
152
|
const trpc = createArchlastTRPCClient<AppRouter>({
|
|
717
153
|
baseUrl: "http://localhost:4000",
|
|
718
|
-
apiKey: "
|
|
154
|
+
apiKey: "arch_example"
|
|
719
155
|
});
|
|
720
156
|
```
|
|
721
157
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
```ts
|
|
725
|
-
// Queries
|
|
726
|
-
const tasks = await trpc.tasks.list.query({});
|
|
727
|
-
const task = await trpc.tasks.get.query({ id: "123" });
|
|
728
|
-
|
|
729
|
-
// Mutations
|
|
730
|
-
const created = await trpc.tasks.create.mutate({ text: "New task" });
|
|
731
|
-
await trpc.tasks.update.mutate({ id: "123", completed: true });
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
### React Query Integration
|
|
735
|
-
|
|
736
|
-
```tsx
|
|
737
|
-
import { createTRPCReact } from "@trpc/react-query";
|
|
738
|
-
import type { AppRouter } from "./_generated/rpc";
|
|
739
|
-
|
|
740
|
-
export const trpc = createTRPCReact<AppRouter>();
|
|
741
|
-
|
|
742
|
-
// In component
|
|
743
|
-
function TaskList() {
|
|
744
|
-
const { data, isLoading } = trpc.tasks.list.useQuery({});
|
|
745
|
-
const createTask = trpc.tasks.create.useMutation();
|
|
746
|
-
|
|
747
|
-
// ...
|
|
748
|
-
}
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
---
|
|
752
|
-
|
|
753
|
-
## Subpath Exports
|
|
754
|
-
|
|
755
|
-
Import specific functionality to reduce bundle size:
|
|
756
|
-
|
|
757
|
-
| Import | Description |
|
|
758
|
-
|--------|-------------|
|
|
759
|
-
| `@archlast/client` | Core `ArchlastClient` class |
|
|
760
|
-
| `@archlast/client/react` | React hooks and provider |
|
|
761
|
-
| `@archlast/client/auth` | Authentication client |
|
|
762
|
-
| `@archlast/client/storage` | File storage client |
|
|
763
|
-
| `@archlast/client/admin` | Admin API client |
|
|
764
|
-
| `@archlast/client/trpc` | tRPC client factory |
|
|
765
|
-
|
|
766
|
-
```ts
|
|
767
|
-
// Minimal import for core functionality
|
|
768
|
-
import { ArchlastClient } from "@archlast/client";
|
|
769
|
-
|
|
770
|
-
// React-specific imports
|
|
771
|
-
import {
|
|
772
|
-
ArchlastProvider,
|
|
773
|
-
useQuery,
|
|
774
|
-
useMutation,
|
|
775
|
-
usePagination,
|
|
776
|
-
useUpload,
|
|
777
|
-
useAuth
|
|
778
|
-
} from "@archlast/client/react";
|
|
779
|
-
|
|
780
|
-
// Auth-only import
|
|
781
|
-
import { ArchlastAuthClient } from "@archlast/client/auth";
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
---
|
|
785
|
-
|
|
786
|
-
## TypeScript Support
|
|
787
|
-
|
|
788
|
-
The client is fully typed. Types are generated by the CLI based on your schema.
|
|
789
|
-
|
|
790
|
-
### Generated Types
|
|
791
|
-
|
|
792
|
-
```ts
|
|
793
|
-
// _generated/api.ts
|
|
794
|
-
export const api = {
|
|
795
|
-
tasks: {
|
|
796
|
-
list: { _path: "tasks.list", _returnType: {} as Task[] },
|
|
797
|
-
get: { _path: "tasks.get", _returnType: {} as Task | null },
|
|
798
|
-
create: { _path: "tasks.create", _returnType: {} as Task },
|
|
799
|
-
// ...
|
|
800
|
-
}
|
|
801
|
-
};
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
### Usage with Types
|
|
805
|
-
|
|
806
|
-
```ts
|
|
807
|
-
import { useQuery } from "@archlast/client/react";
|
|
808
|
-
import { api } from "./_generated/api";
|
|
809
|
-
|
|
810
|
-
// TypeScript knows `tasks` is Task[]
|
|
811
|
-
const tasks = useQuery(api.tasks.list, {});
|
|
812
|
-
|
|
813
|
-
// TypeScript knows `task` is Task | null
|
|
814
|
-
const task = useQuery(api.tasks.get, { id: "123" });
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
---
|
|
818
|
-
|
|
819
|
-
## Error Handling
|
|
820
|
-
|
|
821
|
-
### Query Errors
|
|
822
|
-
|
|
823
|
-
```tsx
|
|
824
|
-
function TaskList() {
|
|
825
|
-
const { data, error, isError } = useQuery(api.tasks.list, {});
|
|
826
|
-
|
|
827
|
-
if (isError) {
|
|
828
|
-
return (
|
|
829
|
-
<div className="error">
|
|
830
|
-
<h3>Failed to load tasks</h3>
|
|
831
|
-
<p>{error.message}</p>
|
|
832
|
-
<button onClick={() => refetch()}>Retry</button>
|
|
833
|
-
</div>
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return <TaskGrid tasks={data} />;
|
|
838
|
-
}
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
### Mutation Errors
|
|
842
|
-
|
|
843
|
-
```ts
|
|
844
|
-
const createTask = useMutation(api.tasks.create);
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
await createTask({ text: "" }); // Invalid
|
|
848
|
-
} catch (error) {
|
|
849
|
-
if (error instanceof ValidationError) {
|
|
850
|
-
console.log("Validation failed:", error.issues);
|
|
851
|
-
} else if (error instanceof AuthenticationError) {
|
|
852
|
-
console.log("Not authenticated");
|
|
853
|
-
} else {
|
|
854
|
-
console.log("Unknown error:", error);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
### Global Error Boundary
|
|
860
|
-
|
|
861
|
-
```tsx
|
|
862
|
-
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
|
863
|
-
import { ErrorBoundary } from "react-error-boundary";
|
|
864
|
-
|
|
865
|
-
function App() {
|
|
866
|
-
return (
|
|
867
|
-
<QueryErrorResetBoundary>
|
|
868
|
-
{({ reset }) => (
|
|
869
|
-
<ErrorBoundary
|
|
870
|
-
onReset={reset}
|
|
871
|
-
fallbackRender={({ error, resetErrorBoundary }) => (
|
|
872
|
-
<div>
|
|
873
|
-
<p>Something went wrong: {error.message}</p>
|
|
874
|
-
<button onClick={resetErrorBoundary}>Try again</button>
|
|
875
|
-
</div>
|
|
876
|
-
)}
|
|
877
|
-
>
|
|
878
|
-
<ArchlastProvider client={client}>
|
|
879
|
-
<YourApp />
|
|
880
|
-
</ArchlastProvider>
|
|
881
|
-
</ErrorBoundary>
|
|
882
|
-
)}
|
|
883
|
-
</QueryErrorResetBoundary>
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
---
|
|
889
|
-
|
|
890
|
-
## Advanced Patterns
|
|
891
|
-
|
|
892
|
-
### Optimistic Updates
|
|
893
|
-
|
|
894
|
-
```ts
|
|
895
|
-
const queryClient = useQueryClient();
|
|
896
|
-
|
|
897
|
-
const updateTask = useMutation(api.tasks.update, {
|
|
898
|
-
onMutate: async (newData) => {
|
|
899
|
-
// Cancel outgoing refetches
|
|
900
|
-
await queryClient.cancelQueries(["tasks.list"]);
|
|
901
|
-
|
|
902
|
-
// Snapshot previous value
|
|
903
|
-
const previous = queryClient.getQueryData(["tasks.list"]);
|
|
904
|
-
|
|
905
|
-
// Optimistically update
|
|
906
|
-
queryClient.setQueryData(["tasks.list"], (old: Task[]) =>
|
|
907
|
-
old.map(t => t._id === newData.id ? { ...t, ...newData } : t)
|
|
908
|
-
);
|
|
909
|
-
|
|
910
|
-
return { previous };
|
|
911
|
-
},
|
|
912
|
-
onError: (err, newData, context) => {
|
|
913
|
-
// Rollback on error
|
|
914
|
-
queryClient.setQueryData(["tasks.list"], context.previous);
|
|
915
|
-
}
|
|
916
|
-
});
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
### SSR / Server Components
|
|
920
|
-
|
|
921
|
-
```ts
|
|
922
|
-
// For Next.js SSR
|
|
923
|
-
const client = new ArchlastClient(
|
|
924
|
-
wsUrl,
|
|
925
|
-
httpUrl,
|
|
926
|
-
"web",
|
|
927
|
-
undefined,
|
|
928
|
-
{
|
|
929
|
-
autoConnect: false, // Don't connect on server
|
|
930
|
-
betterAuthCookies: cookies() // Pass request cookies
|
|
931
|
-
}
|
|
932
|
-
);
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
### Multiple Clients
|
|
936
|
-
|
|
937
|
-
```tsx
|
|
938
|
-
const mainClient = new ArchlastClient(mainWsUrl, mainHttpUrl, "main");
|
|
939
|
-
const analyticsClient = new ArchlastClient(analyticsWsUrl, analyticsHttpUrl, "analytics");
|
|
940
|
-
|
|
941
|
-
function App() {
|
|
942
|
-
return (
|
|
943
|
-
<ArchlastProvider client={mainClient}>
|
|
944
|
-
<ArchlastProvider client={analyticsClient} contextKey="analytics">
|
|
945
|
-
<YourApp />
|
|
946
|
-
</ArchlastProvider>
|
|
947
|
-
</ArchlastProvider>
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
```
|
|
158
|
+
## Subpath exports
|
|
951
159
|
|
|
952
|
-
|
|
160
|
+
- `@archlast/client/react`
|
|
161
|
+
- `@archlast/client/trpc`
|
|
162
|
+
- `@archlast/client/auth`
|
|
163
|
+
- `@archlast/client/storage`
|
|
164
|
+
- `@archlast/client/admin`
|
|
953
165
|
|
|
954
|
-
##
|
|
166
|
+
## Troubleshooting
|
|
955
167
|
|
|
956
|
-
|
|
168
|
+
- WebSocket errors: use `ws://<host>/ws` and confirm CORS settings.
|
|
169
|
+
- Auth errors: ensure cookies or API key are configured.
|
|
170
|
+
- Storage uploads: use `File` or `Blob` for browser uploads.
|
|
957
171
|
|
|
958
|
-
##
|
|
172
|
+
## Publishing (maintainers)
|
|
959
173
|
|
|
960
|
-
|
|
174
|
+
See `docs/npm-publishing.md` for release and publish steps.
|