@finueva/auth 0.10.0 → 0.10.1
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 +132 -400
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Finueva Auth SDK for TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `@finueva/auth` to add Finueva sign-in, organization access, invitations, and Drive credentials to a browser, server, CLI, or native application.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- sign users in with email and password;
|
|
8
|
-
- read the current session;
|
|
9
|
-
- manage profile and password details;
|
|
10
|
-
- read organizations, create ordinary teams, replace display data, and accept invitations;
|
|
11
|
-
- request short-lived Drive credentials;
|
|
12
|
-
- authenticate CLIs and native applications.
|
|
13
|
-
|
|
14
|
-
The package is ESM-only and has no runtime dependencies. Its API is experimental while the package is below `1.0.0`.
|
|
15
|
-
|
|
16
|
-
Sections marked **Unreleased** describe repository source and are not available in the currently published `@finueva/auth@0.9.0` package.
|
|
5
|
+
The current published version is `0.10.0`. The package is ESM-only. Node.js consumers require Node.js 22 or later. Other server runtimes must provide compatible Fetch, Web Crypto, URL, and Web Streams APIs. Its interface is Experimental until version `1.0.0`.
|
|
17
6
|
|
|
18
7
|
## Install
|
|
19
8
|
|
|
@@ -21,22 +10,30 @@ Sections marked **Unreleased** describe repository source and are not available
|
|
|
21
10
|
pnpm add @finueva/auth
|
|
22
11
|
```
|
|
23
12
|
|
|
24
|
-
|
|
13
|
+
For a browser or server application, ask your Auth operator for:
|
|
25
14
|
|
|
26
|
-
|
|
15
|
+
- the Auth origin for your environment;
|
|
16
|
+
- your registered application origin.
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
| ------------------------- | ---------------------- | ------------------------------------ |
|
|
30
|
-
| Browser | `@finueva/auth` | Finueva Auth's HTTP-only cookie |
|
|
31
|
-
| Server or SSR handler | `@finueva/auth/server` | Cookies or one incoming bearer token |
|
|
32
|
-
| CLI or native application | `@finueva/auth/device` | A device bearer session |
|
|
18
|
+
For a CLI or native application, ask for:
|
|
33
19
|
|
|
34
|
-
|
|
20
|
+
- the Auth origin for your environment;
|
|
21
|
+
- a registered device client ID.
|
|
35
22
|
|
|
36
|
-
|
|
23
|
+
Do not reuse configuration from another application or environment.
|
|
24
|
+
|
|
25
|
+
## Choose A Client
|
|
26
|
+
|
|
27
|
+
| Runtime | Import | Session authority | Create it |
|
|
28
|
+
| ------------------------- | ---------------------- | ------------------------------ | ------------------------ |
|
|
29
|
+
| Browser | `@finueva/auth` | Auth HTTP-only cookie | Once for the application |
|
|
30
|
+
| Server or SSR handler | `@finueva/auth/server` | Incoming Auth cookie or bearer | Once per request |
|
|
31
|
+
| CLI or native application | `@finueva/auth/device` | Device bearer session | Once per process |
|
|
37
32
|
|
|
38
33
|
## Browser Quick Start
|
|
39
34
|
|
|
35
|
+
Create the client with the exact Auth and application origins:
|
|
36
|
+
|
|
40
37
|
```ts
|
|
41
38
|
import { AuthError, createAuthClient } from "@finueva/auth";
|
|
42
39
|
|
|
@@ -44,189 +41,60 @@ const auth = createAuthClient({
|
|
|
44
41
|
authOrigin: "https://auth.finueva.com",
|
|
45
42
|
appOrigin: window.location.origin,
|
|
46
43
|
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Read the current session:
|
|
47
47
|
|
|
48
|
+
```ts
|
|
48
49
|
const session = await auth.getSession();
|
|
49
50
|
|
|
50
51
|
if (session) {
|
|
51
52
|
console.log(`Signed in as ${session.user.email}`);
|
|
52
|
-
} else {
|
|
53
|
-
console.log("Not signed in");
|
|
54
53
|
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Sign in with email and password:
|
|
55
57
|
|
|
58
|
+
```ts
|
|
56
59
|
try {
|
|
57
|
-
const
|
|
60
|
+
const session = await auth.signIn({
|
|
58
61
|
email: "ada@example.com",
|
|
59
62
|
password: "correct-horse-battery-staple",
|
|
60
63
|
});
|
|
61
64
|
|
|
62
|
-
console.log(`Welcome ${
|
|
65
|
+
console.log(`Welcome ${session.user.name}`);
|
|
63
66
|
} catch (error) {
|
|
64
67
|
if (error instanceof AuthError && error.code === "invalid_credentials") {
|
|
65
|
-
|
|
68
|
+
showInvalidCredentials();
|
|
66
69
|
} else {
|
|
67
70
|
throw error;
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### React To Session Changes
|
|
75
|
-
|
|
76
|
-
Use `onAuthStateChange()` to update the interface:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const stop = auth.onAuthStateChange((state) => {
|
|
80
|
-
switch (state.status) {
|
|
81
|
-
case "loading":
|
|
82
|
-
showLoading();
|
|
83
|
-
break;
|
|
84
|
-
case "cached":
|
|
85
|
-
showAccountHint(state.session.user);
|
|
86
|
-
break;
|
|
87
|
-
case "authenticated":
|
|
88
|
-
showAccount(state.session.user);
|
|
89
|
-
break;
|
|
90
|
-
case "unauthenticated":
|
|
91
|
-
showSignIn();
|
|
92
|
-
break;
|
|
93
|
-
case "unavailable":
|
|
94
|
-
showAuthUnavailable();
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
window.addEventListener("pagehide", stop, { once: true });
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
`cached` state is only a display hint. Never use cached browser state to authorize an operation. Check access on the server.
|
|
103
|
-
|
|
104
|
-
Browser snapshots use local storage by default. Choose another mode when you create the client:
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
const auth = createAuthClient({
|
|
108
|
-
authOrigin: "https://auth.finueva.com",
|
|
109
|
-
appOrigin: window.location.origin,
|
|
110
|
-
persistence: "session", // "local", "session", or "memory"
|
|
111
|
-
timeoutMs: 10_000,
|
|
112
|
-
});
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The snapshot contains normalized display data, not cookies or Drive credentials.
|
|
116
|
-
|
|
117
|
-
## Common Browser Tasks
|
|
118
|
-
|
|
119
|
-
### Create An Account
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
const session = await auth.signUp("ada@example.com", "correct-horse-battery-staple", "Ada Lovelace");
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
New passwords must contain 10 to 128 characters.
|
|
75
|
+
The browser owns the session cookie. The SDK does not expose or persist it.
|
|
126
76
|
|
|
127
|
-
|
|
77
|
+
## Common Tasks
|
|
128
78
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
79
|
+
| Task | Browser | Server | Device |
|
|
80
|
+
| ------------------------- | ------------------------------------------- | -------------------------------------- | -------------------------------------- |
|
|
81
|
+
| Create an account | `signUp()` | Not available | `signUpWithEmail()` |
|
|
82
|
+
| Read the current session | `getSession()` | `getSession()` | `getSession()` |
|
|
83
|
+
| Observe session state | `onAuthStateChange()` | Not available | Not available |
|
|
84
|
+
| Update a profile | `updateProfile()` | Not available | `updateProfile()` |
|
|
85
|
+
| Change a password | `changePassword()` | Not available | `changePassword()` |
|
|
86
|
+
| Reset a password | `requestPasswordReset()`, `resetPassword()` | Not available | Not available |
|
|
87
|
+
| Verify an email | `sendVerificationEmail()` | Not available | Not available |
|
|
88
|
+
| List organizations | `listOrganizations()` | `listOrganizations()` | `listOrganizations()` |
|
|
89
|
+
| Create an ordinary team | `createOrganizationTeam()` | `createOrganizationTeam()` | `createOrganizationTeam()` |
|
|
90
|
+
| Invite an ordinary member | `createOrganizationMemberInvitation()` | `createOrganizationMemberInvitation()` | `createOrganizationMemberInvitation()` |
|
|
91
|
+
| Accept an invitation | `acceptInvitation()` | `acceptInvitation()` | Not available |
|
|
92
|
+
| Request Drive access | `getDriveCredential()` | `getDriveCredential()` | `getDriveCredential()` |
|
|
93
|
+
| End the current session | `signOut()` | `signOut()` | `revokeCurrentDevice()` |
|
|
143
94
|
|
|
144
|
-
|
|
95
|
+
## Create An Organization Invitation
|
|
145
96
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
await auth.requestPasswordReset("ada@example.com", `${window.location.origin}/account/reset-password`);
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
On the reset page, remove the token from the address bar before loading other resources. Use it once:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
const url = new URL(window.location.href);
|
|
156
|
-
let token = url.searchParams.get("token");
|
|
157
|
-
|
|
158
|
-
url.searchParams.delete("token");
|
|
159
|
-
window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
|
|
160
|
-
|
|
161
|
-
if (!token) throw new Error("Reset token is missing");
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
await auth.resetPassword(token, newPassword);
|
|
165
|
-
} finally {
|
|
166
|
-
token = null;
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Serve this page with `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. Do not load analytics or third-party content on it.
|
|
171
|
-
|
|
172
|
-
### Verify An Email
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
await auth.sendVerificationEmail({
|
|
176
|
-
email: "ada@example.com",
|
|
177
|
-
callbackURL: `${window.location.origin}/account/email-verified`,
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
The callback must use the application's exact origin. Omit it to use the default Auth result page.
|
|
182
|
-
|
|
183
|
-
### List Organizations
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
let page = await auth.listOrganizations({ limit: 50 });
|
|
187
|
-
|
|
188
|
-
for (const organization of page.organizations) {
|
|
189
|
-
console.log(organization.id, organization.name, organization.slug);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (page.nextCursor) {
|
|
193
|
-
page = await auth.listOrganizations({ limit: 50, cursor: page.nextCursor });
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
All organization collections use an opaque cursor and a page size from 1 through 100. Use
|
|
198
|
-
`getOrganization()`, `listOrganizationMembers()`, `listOrganizationTeams()`, and
|
|
199
|
-
`listOrganizationTeamMembers()` with an explicit organization ID. Manual owners and admins can use
|
|
200
|
-
`listOrganizationInvitations()` and `getOrganizationInvitation()`. A verified recipient can use
|
|
201
|
-
`listReceivedInvitations()` and `getReceivedInvitation()`.
|
|
202
|
-
|
|
203
|
-
Pagination is deterministic while a collection does not change. During concurrent mutation, an
|
|
204
|
-
insert at or before the cursor can be absent, an insert after it can appear once, and a delete,
|
|
205
|
-
update, status change, or expiry can change later pages. Restart pagination when you need a current
|
|
206
|
-
view.
|
|
207
|
-
|
|
208
|
-
### Create A Team
|
|
209
|
-
|
|
210
|
-
**Unreleased:** this method will ship in a later package version and is not in `@finueva/auth@0.9.0`.
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
const idempotencyKey = crypto.randomUUID();
|
|
214
|
-
const team = await auth.createOrganizationTeam("org_01J8R7M6N5P4Q3S2T1", "Platform Engineering", idempotencyKey);
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
The name and caller-owned idempotency key must each contain from 1 through 255 UTF-8 bytes. Use a
|
|
218
|
-
new key for each intended team. If a response is lost, send the same organization ID, name, and key
|
|
219
|
-
again. Auth returns the original creation result for a completed same-key request, even if a later
|
|
220
|
-
operation changed the team display. A conflicting or unresolved same-key request returns
|
|
221
|
-
`state_conflict`. The SDK never retries automatically.
|
|
222
|
-
|
|
223
|
-
For an ordinary organization, a current effective owner or administrator can use the cookie or
|
|
224
|
-
bearer lane. For a managed organization, a current manual owner or manual administrator must use the
|
|
225
|
-
cookie lane. The method creates only an ordinary team.
|
|
226
|
-
|
|
227
|
-
### Create A Member Invitation
|
|
228
|
-
|
|
229
|
-
**Unreleased:** this method will ship in a later package version and is not in `@finueva/auth@0.9.0`.
|
|
97
|
+
Use one new idempotency key for each invitation that you intend to create:
|
|
230
98
|
|
|
231
99
|
```ts
|
|
232
100
|
const invitation = await auth.createOrganizationMemberInvitation("org_01J8R7M6N5P4Q3S2T1", {
|
|
@@ -238,68 +106,22 @@ const invitation = await auth.createOrganizationMemberInvitation("org_01J8R7M6N5
|
|
|
238
106
|
console.log(invitation.id, invitation.expiresAt);
|
|
239
107
|
```
|
|
240
108
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
Use a new key for each intended invitation. If a response is lost, send the same organization ID and input again. The method returns only the invitation ID and expiry. It does not report email delivery. The SDK never retries automatically.
|
|
244
|
-
|
|
245
|
-
For an ordinary organization, a current effective owner or administrator can use the cookie or bearer lane. Managed organizations are denied.
|
|
246
|
-
|
|
247
|
-
### Accept An Invitation
|
|
248
|
-
|
|
249
|
-
```ts
|
|
250
|
-
const result = await auth.acceptInvitation({
|
|
251
|
-
invitationId: "invitation_01J8R7M6N5P4Q3S2T1",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
console.log(result.organizationId);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Replace Organization Display
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
const display = await auth.updateOrganizationDisplay("org_01J8R7M6N5P4Q3S2T1", {
|
|
261
|
-
name: "Example Group",
|
|
262
|
-
slug: "example-group",
|
|
263
|
-
logo: null,
|
|
264
|
-
});
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Send all three fields. `name` and `slug` can contain no more than 255 UTF-8 bytes. Use `logo: null` to remove the logo. A non-null logo must be an exact canonical HTTPS URL of no more than 2048 UTF-8 bytes. The method supports manual owners and manual administrators of ordinary organizations. Provider-only administrators are denied. It does not update metadata, lifecycle, membership, teams, invitations, or active-organization state.
|
|
268
|
-
|
|
269
|
-
Browser and cookie-based server calls require the exact configured application Origin. The SDK reports a denied or missing Origin as `request_origin_denied`. Other organization authority denials use `access_denied`.
|
|
270
|
-
|
|
271
|
-
The method does not retry. If a response is lost, submit the same complete desired state again. Concurrent different updates use last-write-wins behavior.
|
|
272
|
-
|
|
273
|
-
### Replace Team Display
|
|
274
|
-
|
|
275
|
-
**Unreleased:** this method will ship in a later package version and is not in `@finueva/auth@0.9.0`.
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
const display = await auth.updateOrganizationTeamDisplay("org_01J8R7M6N5P4Q3S2T1", "team_01J8R7M6N5P4Q3S2T1", {
|
|
279
|
-
name: "Platform Engineering",
|
|
280
|
-
});
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Send the complete `name`. It can contain no more than 255 UTF-8 bytes. For an ordinary organization, a current effective owner or administrator can use the cookie or bearer lane. For a managed organization, a current manual owner or manual administrator must use the cookie lane. Managed teams are denied.
|
|
284
|
-
|
|
285
|
-
The method does not retry. If a response is lost, submit the same complete desired name again. Concurrent different updates use last-write-wins behavior.
|
|
286
|
-
|
|
287
|
-
### Sign Out
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
await auth.signOut();
|
|
291
|
-
```
|
|
109
|
+
If the response is lost, repeat the same request with the same key. The SDK does not retry automatically. The result confirms invitation creation. It does not confirm email delivery.
|
|
292
110
|
|
|
293
111
|
## Request A Drive Credential
|
|
294
112
|
|
|
295
|
-
|
|
113
|
+
Request one short-lived credential for one workspace:
|
|
296
114
|
|
|
297
115
|
```ts
|
|
298
116
|
const credential = await auth.getDriveCredential({
|
|
299
117
|
workspace: { type: "personal" },
|
|
300
118
|
});
|
|
301
119
|
|
|
302
|
-
|
|
120
|
+
await fetch("https://drive.finueva.com/api/files", {
|
|
121
|
+
headers: {
|
|
122
|
+
authorization: `${credential.tokenType} ${credential.token}`,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
303
125
|
```
|
|
304
126
|
|
|
305
127
|
For an organization workspace:
|
|
@@ -313,184 +135,87 @@ const credential = await auth.getDriveCredential({
|
|
|
313
135
|
});
|
|
314
136
|
```
|
|
315
137
|
|
|
316
|
-
|
|
138
|
+
Browser and device clients cache one completed credential in memory. The server client does not cache credentials. The SDK never replays a failed Drive operation.
|
|
317
139
|
|
|
318
|
-
|
|
140
|
+
## Server Quick Start
|
|
319
141
|
|
|
320
|
-
|
|
321
|
-
const replacement = await auth.getDriveCredential({
|
|
322
|
-
workspace: { type: "personal" },
|
|
323
|
-
forceRefresh: true,
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
The SDK does not replay the failed Drive request. Replay it only when the operation is safe to repeat.
|
|
328
|
-
|
|
329
|
-
## Server And SSR
|
|
330
|
-
|
|
331
|
-
Create a server client inside each request handler. It forwards only Finueva Auth cookies. If the incoming request has a bearer token, that token takes precedence and the client forwards no cookies.
|
|
142
|
+
Create one client inside each request handler:
|
|
332
143
|
|
|
333
144
|
```ts
|
|
334
|
-
import {
|
|
145
|
+
import { createServerAuth } from "@finueva/auth/server";
|
|
335
146
|
|
|
336
|
-
export async function
|
|
147
|
+
export async function handleRequest(request: Request): Promise<Response> {
|
|
337
148
|
const auth = createServerAuth({
|
|
338
149
|
authOrigin: "https://auth.finueva.com",
|
|
339
150
|
appOrigin: "https://files.finueva.com",
|
|
340
151
|
request,
|
|
341
152
|
});
|
|
342
153
|
|
|
343
|
-
|
|
344
|
-
const session = await auth.getSession();
|
|
345
|
-
|
|
346
|
-
if (!session) {
|
|
347
|
-
return Response.json(
|
|
348
|
-
{ authenticated: false },
|
|
349
|
-
{
|
|
350
|
-
status: 401,
|
|
351
|
-
headers: { "cache-control": "no-store" },
|
|
352
|
-
},
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return Response.json(
|
|
357
|
-
{ user: session.user },
|
|
358
|
-
{
|
|
359
|
-
headers: { "cache-control": "no-store" },
|
|
360
|
-
},
|
|
361
|
-
);
|
|
362
|
-
} catch (error) {
|
|
363
|
-
if (error instanceof AuthError) {
|
|
364
|
-
return Response.json(
|
|
365
|
-
{ code: error.code },
|
|
366
|
-
{
|
|
367
|
-
status: error.status,
|
|
368
|
-
headers: { "cache-control": "no-store" },
|
|
369
|
-
},
|
|
370
|
-
);
|
|
371
|
-
}
|
|
154
|
+
const session = await auth.getSession();
|
|
372
155
|
|
|
373
|
-
|
|
374
|
-
|
|
156
|
+
return Response.json(
|
|
157
|
+
{ authenticated: session !== null, user: session?.user ?? null },
|
|
158
|
+
{ headers: { "cache-control": "no-store" } },
|
|
159
|
+
);
|
|
375
160
|
}
|
|
376
161
|
```
|
|
377
162
|
|
|
378
|
-
|
|
163
|
+
If the incoming request has `Authorization`, the server client uses that bearer and forwards no cookies. Otherwise, it forwards only Auth cookies.
|
|
164
|
+
|
|
165
|
+
Some server methods return cookie changes. Apply them before you send the response:
|
|
379
166
|
|
|
380
167
|
```ts
|
|
381
168
|
const result = await auth.signOut();
|
|
382
|
-
const headers = new Headers({
|
|
383
|
-
"cache-control": "no-store",
|
|
384
|
-
location: "/signed-out",
|
|
385
|
-
});
|
|
169
|
+
const headers = new Headers({ location: "/signed-out" });
|
|
386
170
|
|
|
387
171
|
result.applyCookies(headers);
|
|
388
172
|
|
|
389
173
|
return new Response(null, { status: 303, headers });
|
|
390
174
|
```
|
|
391
175
|
|
|
392
|
-
Call `applyCookies()` after server `signOut()`, `acceptInvitation()`, and `signInWithSocialIdToken()`.
|
|
393
|
-
|
|
394
|
-
Check `Origin`, Fetch Metadata, or a server-generated CSRF token before a server-side mutation. The SDK forwards authority; it does not replace your application's CSRF protection.
|
|
176
|
+
Call `applyCookies()` after server `signOut()`, `acceptInvitation()`, and `signInWithSocialIdToken()`.
|
|
395
177
|
|
|
396
|
-
##
|
|
178
|
+
## Device Quick Start
|
|
397
179
|
|
|
398
|
-
Use the device flow
|
|
180
|
+
Use the device flow for a CLI or native application that can open a system browser:
|
|
399
181
|
|
|
400
182
|
```ts
|
|
401
|
-
import {
|
|
183
|
+
import { createDeviceAuth } from "@finueva/auth/device";
|
|
402
184
|
|
|
403
185
|
const auth = createDeviceAuth({
|
|
404
186
|
authOrigin: "https://auth.finueva.com",
|
|
405
187
|
clientId: "finueva-cli",
|
|
406
188
|
});
|
|
407
189
|
|
|
408
|
-
const
|
|
409
|
-
process.once("SIGINT", () => controller.abort());
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
const authorization = await auth.requestAuthorization({
|
|
413
|
-
signal: controller.signal,
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
console.log(`Open ${authorization.verificationUriComplete}`);
|
|
417
|
-
console.log(`Code: ${authorization.userCode}`);
|
|
418
|
-
|
|
419
|
-
await auth.pollForToken(authorization, {
|
|
420
|
-
signal: controller.signal,
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const session = await auth.getSession();
|
|
424
|
-
console.log(session ? `Signed in as ${session.user.email}` : "Not signed in");
|
|
425
|
-
} catch (error) {
|
|
426
|
-
if (error instanceof DeviceAuthorizationError || error instanceof AuthError) {
|
|
427
|
-
console.error(error.code);
|
|
428
|
-
} else {
|
|
429
|
-
throw error;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
`clientId` is a public registered identifier, not a secret. Never display or log `authorization.deviceCode`.
|
|
435
|
-
|
|
436
|
-
Device sessions are memory-only unless you provide `DeviceTokenPersistence`:
|
|
190
|
+
const authorization = await auth.requestAuthorization();
|
|
437
191
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const persistence: DeviceTokenPersistence = {
|
|
442
|
-
read: (signal) => vault.read("finueva-auth", signal),
|
|
443
|
-
replace: (token, signal) => vault.replace("finueva-auth", token, signal),
|
|
444
|
-
delete: (signal) => vault.delete("finueva-auth", signal),
|
|
445
|
-
};
|
|
192
|
+
console.log(`Open ${authorization.verificationUriComplete}`);
|
|
193
|
+
console.log(`Code: ${authorization.userCode}`);
|
|
446
194
|
|
|
447
|
-
|
|
448
|
-
authOrigin: "https://auth.finueva.com",
|
|
449
|
-
clientId: "finueva-desktop",
|
|
450
|
-
fetch: nativeFetch,
|
|
451
|
-
persistence,
|
|
452
|
-
});
|
|
195
|
+
await auth.pollForToken(authorization);
|
|
453
196
|
```
|
|
454
197
|
|
|
455
|
-
|
|
198
|
+
`clientId` is public. `deviceCode` is not. Never log or display the device code.
|
|
456
199
|
|
|
457
|
-
|
|
200
|
+
Device tokens are memory-only by default. For persistence, supply a `DeviceTokenPersistence` adapter backed by an operating-system credential vault. Do not use local storage, a WebView cookie jar, or a plaintext file.
|
|
458
201
|
|
|
459
|
-
|
|
460
|
-
const session = await auth.signInWithEmail("ada@example.com", "correct-horse-battery-staple");
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
End the current device session with `revokeCurrentDevice()`. End every browser and device session for the account with `revokeAllSessions()`.
|
|
464
|
-
|
|
465
|
-
## Social ID Tokens
|
|
202
|
+
## Handle Errors
|
|
466
203
|
|
|
467
|
-
|
|
204
|
+
Constructor and selected account-input errors can throw `TypeError`. Validated SDK request failures throw `AuthError`. Device authorization can throw `DeviceAuthorizationError`:
|
|
468
205
|
|
|
469
206
|
```ts
|
|
470
|
-
|
|
471
|
-
provider: "google",
|
|
472
|
-
idToken,
|
|
473
|
-
nonce,
|
|
474
|
-
signUp: false,
|
|
475
|
-
});
|
|
476
|
-
```
|
|
207
|
+
import { AuthError } from "@finueva/auth";
|
|
477
208
|
|
|
478
|
-
Set `signUp: true` only after the user chooses to create an account. The application owns the provider authorization flow, state, nonce, and PKCE transaction. Pass only the ID token to Auth. Do not pass provider access or refresh tokens.
|
|
479
|
-
|
|
480
|
-
The generic Google and Apple redirect flow is not exposed by this package.
|
|
481
|
-
|
|
482
|
-
## Errors
|
|
483
|
-
|
|
484
|
-
Invalid local arguments throw `TypeError`. Auth failures throw `AuthError`:
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
209
|
try {
|
|
488
|
-
await auth.
|
|
210
|
+
await auth.signIn({
|
|
211
|
+
email: "ada@example.com",
|
|
212
|
+
password: "correct-horse-battery-staple",
|
|
213
|
+
});
|
|
489
214
|
} catch (error) {
|
|
490
215
|
if (!(error instanceof AuthError)) throw error;
|
|
491
216
|
|
|
492
|
-
if (error.code === "
|
|
493
|
-
|
|
217
|
+
if (error.code === "invalid_credentials") {
|
|
218
|
+
showInvalidCredentials();
|
|
494
219
|
} else if (error.code === "identity_authority_unavailable") {
|
|
495
220
|
showAuthUnavailable();
|
|
496
221
|
} else {
|
|
@@ -499,48 +224,55 @@ try {
|
|
|
499
224
|
}
|
|
500
225
|
```
|
|
501
226
|
|
|
502
|
-
Use `error.code` for application behavior. Do not parse `error.message`.
|
|
503
|
-
|
|
504
|
-
| Code | What the application should do |
|
|
505
|
-
| -------------------------------- | --------------------------------------------------------------------- |
|
|
506
|
-
| `invalid_credentials` | Ask the user to check the credential. |
|
|
507
|
-
| `invalid_auth_input` | Correct the account input. |
|
|
508
|
-
| `account_conflict` | Stop the account action. Do not expose account-existence details. |
|
|
509
|
-
| `authentication_required` | Start sign-in. |
|
|
510
|
-
| `email_verification_required` | Ask the user to verify the current email. |
|
|
511
|
-
| `invitation_unavailable` | Ask for a new invitation. Do not retry the old one. |
|
|
512
|
-
| `invalid_workspace_selection` | Send one exact personal or organization workspace. |
|
|
513
|
-
| `state_conflict` | Wait for the active authority operation, then read the session again. |
|
|
514
|
-
| `access_denied` | Stop the operation. |
|
|
515
|
-
| `credential_rate_limited` | Wait for `retryAfterSeconds` when present. |
|
|
516
|
-
| `auth_request_rate_limited` | Wait for `retryAfterSeconds` when present. |
|
|
517
|
-
| `identity_authority_unavailable` | Keep state unverified and try again later. |
|
|
518
|
-
|
|
519
|
-
Device authorization can also throw `DeviceAuthorizationError`. Its codes are `device_access_denied`, `device_authorization_cancelled`, `device_code_expired`, `invalid_device_authorization`, and `identity_authority_unavailable`.
|
|
520
|
-
|
|
521
|
-
The SDK does not automatically retry requests.
|
|
227
|
+
Use `error.code` for application behavior. Do not parse `error.message`. Except for RFC 8628 device polling, the SDK does not automatically repeat requests.
|
|
522
228
|
|
|
523
|
-
## Security
|
|
229
|
+
## Security Checklist
|
|
524
230
|
|
|
525
231
|
- Use HTTPS. Plain HTTP is accepted only on localhost.
|
|
526
|
-
-
|
|
232
|
+
- Register exact application origins.
|
|
233
|
+
- Create server clients per request.
|
|
234
|
+
- Apply returned server cookies before you send the response.
|
|
527
235
|
- Keep browser session snapshots display-only.
|
|
528
|
-
-
|
|
529
|
-
-
|
|
530
|
-
-
|
|
531
|
-
- Keep Drive credentials in memory and send them only to Drive.
|
|
236
|
+
- Keep device sessions in an operating-system credential vault.
|
|
237
|
+
- Keep Drive credentials in memory.
|
|
238
|
+
- Protect server-side mutations against CSRF.
|
|
532
239
|
- Do not log cookies, bearer tokens, ID tokens, reset tokens, device codes, or complete Auth responses.
|
|
533
|
-
- Protect application mutations against CSRF before calling the server client.
|
|
534
240
|
|
|
535
241
|
## Current Limits
|
|
536
242
|
|
|
537
|
-
- The
|
|
538
|
-
- Requests time out after 10 seconds by default.
|
|
539
|
-
-
|
|
243
|
+
- The package is Experimental until `1.0.0`.
|
|
244
|
+
- Requests time out after 10 seconds by default.
|
|
245
|
+
- `timeoutMs` accepts 1 through 60,000 milliseconds.
|
|
246
|
+
- Most responses are limited to 64 KiB. Organization read responses can use up to 256 KiB.
|
|
540
247
|
- Browser snapshots and bearer authorization values are limited to 16 KiB.
|
|
541
|
-
- Organization
|
|
248
|
+
- Organization collections use page sizes from 1 through 100.
|
|
542
249
|
- Organization Drive context supports at most 50 current teams.
|
|
543
|
-
-
|
|
544
|
-
-
|
|
545
|
-
-
|
|
546
|
-
- Production support gates are
|
|
250
|
+
- Invitation creation supports only the `member` role in ordinary organizations.
|
|
251
|
+
- Invitation resend and cancellation are not public SDK methods.
|
|
252
|
+
- Cross-root SSO, custom refresh tokens, and account deletion are not implemented.
|
|
253
|
+
- Production support gates are incomplete. Ask the Auth operator for the current environment status before adoption.
|
|
254
|
+
|
|
255
|
+
## Package Exports
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { AuthError, createAuthClient } from "@finueva/auth";
|
|
259
|
+
import { createServerAuth } from "@finueva/auth/server";
|
|
260
|
+
import { DeviceAuthorizationError, createDeviceAuth } from "@finueva/auth/device";
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
TypeScript declarations ship with the package. The public declarations do not expose Better Auth types.
|
|
264
|
+
|
|
265
|
+
## Documentation
|
|
266
|
+
|
|
267
|
+
Long-form task guides are maintained with the Auth source:
|
|
268
|
+
|
|
269
|
+
- [Get started](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/index.md)
|
|
270
|
+
- [Browser applications](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/browser.md)
|
|
271
|
+
- [Server and SSR applications](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/server.md)
|
|
272
|
+
- [CLI and native applications](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/device.md)
|
|
273
|
+
- [Social ID-token sign-in](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/social-sign-in.md)
|
|
274
|
+
- [Organizations and invitations](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/organizations.md)
|
|
275
|
+
- [Drive credentials](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/drive-credentials.md)
|
|
276
|
+
- [Errors and recovery](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/errors-and-recovery.md)
|
|
277
|
+
|
|
278
|
+
These links track current source and can describe a newer SDK version. Check the version at the start of the guide. Repository access is required until these guides have a public, versioned documentation deployment.
|