@finueva/auth 0.8.0 → 0.8.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 +393 -90
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,157 +1,460 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@finueva/auth`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official TypeScript client for Finueva Auth.
|
|
4
|
+
|
|
5
|
+
Use it to:
|
|
6
|
+
|
|
7
|
+
- sign users in with email and password;
|
|
8
|
+
- read the current session;
|
|
9
|
+
- manage profile and password details;
|
|
10
|
+
- list organizations 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
|
+
## Install
|
|
4
17
|
|
|
5
18
|
```sh
|
|
6
19
|
pnpm add @finueva/auth
|
|
7
20
|
```
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
Node.js applications require Node.js 22 or later.
|
|
23
|
+
|
|
24
|
+
## Pick The Right Client
|
|
25
|
+
|
|
26
|
+
| Runtime | Import | How it authenticates |
|
|
27
|
+
| ------------------------- | ---------------------- | ------------------------------------ |
|
|
28
|
+
| Browser | `@finueva/auth` | Finueva Auth's HTTP-only cookie |
|
|
29
|
+
| Server or SSR handler | `@finueva/auth/server` | Cookies or one incoming bearer token |
|
|
30
|
+
| CLI or native application | `@finueva/auth/device` | A device bearer session |
|
|
31
|
+
|
|
32
|
+
Create one browser client for the application, one server client per request, or one device client per application process.
|
|
33
|
+
|
|
34
|
+
Before you start, ask the Auth operator to register your exact application origin. Device applications also need a registered client ID. Do not copy values from another application or environment.
|
|
35
|
+
|
|
36
|
+
## Browser Quick Start
|
|
10
37
|
|
|
11
38
|
```ts
|
|
12
|
-
import { createAuthClient } from "@finueva/auth";
|
|
39
|
+
import { AuthError, createAuthClient } from "@finueva/auth";
|
|
13
40
|
|
|
14
41
|
const auth = createAuthClient({
|
|
15
42
|
authOrigin: "https://auth.finueva.com",
|
|
16
43
|
appOrigin: window.location.origin,
|
|
17
44
|
});
|
|
18
45
|
|
|
19
|
-
|
|
46
|
+
const session = await auth.getSession();
|
|
47
|
+
|
|
48
|
+
if (session) {
|
|
49
|
+
console.log(`Signed in as ${session.user.email}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log("Not signed in");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const nextSession = await auth.signIn({
|
|
56
|
+
email: "ada@example.com",
|
|
57
|
+
password: "correct-horse-battery-staple",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log(`Welcome ${nextSession.user.name}`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof AuthError && error.code === "invalid_credentials") {
|
|
63
|
+
console.log("Check the email and password.");
|
|
64
|
+
} else {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Auth keeps the session token in an HTTP-only cookie. The browser client never returns that token.
|
|
20
71
|
|
|
72
|
+
### React To Session Changes
|
|
73
|
+
|
|
74
|
+
Use `onAuthStateChange()` to update the interface:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
21
77
|
const stop = auth.onAuthStateChange((state) => {
|
|
22
|
-
|
|
23
|
-
|
|
78
|
+
switch (state.status) {
|
|
79
|
+
case "loading":
|
|
80
|
+
showLoading();
|
|
81
|
+
break;
|
|
82
|
+
case "cached":
|
|
83
|
+
showAccountHint(state.session.user);
|
|
84
|
+
break;
|
|
85
|
+
case "authenticated":
|
|
86
|
+
showAccount(state.session.user);
|
|
87
|
+
break;
|
|
88
|
+
case "unauthenticated":
|
|
89
|
+
showSignIn();
|
|
90
|
+
break;
|
|
91
|
+
case "unavailable":
|
|
92
|
+
showAuthUnavailable();
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
24
95
|
});
|
|
25
96
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
97
|
+
window.addEventListener("pagehide", stop, { once: true });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`cached` state is only a display hint. Never use cached browser state to authorize an operation. Check access on the server.
|
|
101
|
+
|
|
102
|
+
Browser snapshots use local storage by default. Choose another mode when you create the client:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const auth = createAuthClient({
|
|
106
|
+
authOrigin: "https://auth.finueva.com",
|
|
107
|
+
appOrigin: window.location.origin,
|
|
108
|
+
persistence: "session", // "local", "session", or "memory"
|
|
109
|
+
timeoutMs: 10_000,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The snapshot contains normalized display data, not cookies or Drive credentials.
|
|
114
|
+
|
|
115
|
+
## Common Browser Tasks
|
|
116
|
+
|
|
117
|
+
### Create An Account
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const session = await auth.signUp("ada@example.com", "correct-horse-battery-staple", "Ada Lovelace");
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
New passwords must contain 10 to 128 characters.
|
|
124
|
+
|
|
125
|
+
### Update The Profile
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const session = await auth.updateProfile({
|
|
129
|
+
name: "Ada Lovelace",
|
|
130
|
+
image: "https://images.example.com/ada.png",
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Pass `image: null` to remove the image.
|
|
135
|
+
|
|
136
|
+
### Change The Password
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
await auth.changePassword(currentPassword, newPassword);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This revokes the user's other sessions by default. Pass `false` as the third argument only when the product deliberately keeps those sessions.
|
|
143
|
+
|
|
144
|
+
### Reset A Password
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
await auth.requestPasswordReset("ada@example.com", `${window.location.origin}/account/reset-password`);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
On the reset page, remove the token from the address bar before loading other resources. Use it once:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const url = new URL(window.location.href);
|
|
154
|
+
let token = url.searchParams.get("token");
|
|
155
|
+
|
|
156
|
+
url.searchParams.delete("token");
|
|
157
|
+
window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
|
|
158
|
+
|
|
159
|
+
if (!token) throw new Error("Reset token is missing");
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await auth.resetPassword(token, newPassword);
|
|
163
|
+
} finally {
|
|
164
|
+
token = null;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Serve this page with `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. Do not load analytics or third-party content on it.
|
|
169
|
+
|
|
170
|
+
### Verify An Email
|
|
171
|
+
|
|
172
|
+
```ts
|
|
34
173
|
await auth.sendVerificationEmail({
|
|
35
|
-
email,
|
|
174
|
+
email: "ada@example.com",
|
|
36
175
|
callbackURL: `${window.location.origin}/account/email-verified`,
|
|
37
176
|
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The callback must use the application's exact origin. Omit it to use the default Auth result page.
|
|
180
|
+
|
|
181
|
+
### List Organizations
|
|
182
|
+
|
|
183
|
+
```ts
|
|
38
184
|
const organizations = await auth.listOrganizations();
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
185
|
+
|
|
186
|
+
for (const organization of organizations) {
|
|
187
|
+
console.log(organization.id, organization.name, organization.slug);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Accept An Invitation
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const result = await auth.acceptInvitation({
|
|
195
|
+
invitationId: "invitation_01J8R7M6N5P4Q3S2T1",
|
|
45
196
|
});
|
|
197
|
+
|
|
198
|
+
console.log(result.organizationId);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Sign Out
|
|
202
|
+
|
|
203
|
+
```ts
|
|
46
204
|
await auth.signOut();
|
|
47
|
-
stop();
|
|
48
205
|
```
|
|
49
206
|
|
|
50
|
-
|
|
207
|
+
## Request A Drive Credential
|
|
51
208
|
|
|
52
|
-
|
|
209
|
+
Ask Auth for a credential for one workspace:
|
|
53
210
|
|
|
54
|
-
|
|
211
|
+
```ts
|
|
212
|
+
const credential = await auth.getDriveCredential({
|
|
213
|
+
workspace: { type: "personal" },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const authorization = `${credential.tokenType} ${credential.token}`;
|
|
217
|
+
```
|
|
55
218
|
|
|
56
|
-
|
|
219
|
+
For an organization workspace:
|
|
57
220
|
|
|
58
|
-
|
|
221
|
+
```ts
|
|
222
|
+
const credential = await auth.getDriveCredential({
|
|
223
|
+
workspace: {
|
|
224
|
+
type: "organization",
|
|
225
|
+
organizationId: "org_01J8R7M6N5P4Q3S2T1",
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
```
|
|
59
229
|
|
|
60
|
-
|
|
230
|
+
Drive credentials last about 14 minutes. Browser and device clients keep them in memory only and refresh them when two minutes remain. The server client does not cache them.
|
|
61
231
|
|
|
62
|
-
|
|
232
|
+
If Drive rejects a credential, request a replacement:
|
|
63
233
|
|
|
64
|
-
|
|
234
|
+
```ts
|
|
235
|
+
const replacement = await auth.getDriveCredential({
|
|
236
|
+
workspace: { type: "personal" },
|
|
237
|
+
forceRefresh: true,
|
|
238
|
+
});
|
|
239
|
+
```
|
|
65
240
|
|
|
66
|
-
|
|
241
|
+
The SDK does not replay the failed Drive request. Replay it only when the operation is safe to repeat.
|
|
67
242
|
|
|
68
|
-
|
|
243
|
+
## Server And SSR
|
|
69
244
|
|
|
70
|
-
|
|
245
|
+
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.
|
|
71
246
|
|
|
72
247
|
```ts
|
|
73
|
-
import { createServerAuth } from "@finueva/auth/server";
|
|
248
|
+
import { AuthError, createServerAuth } from "@finueva/auth/server";
|
|
249
|
+
|
|
250
|
+
export async function handleAccountRequest(request: Request): Promise<Response> {
|
|
251
|
+
const auth = createServerAuth({
|
|
252
|
+
authOrigin: "https://auth.finueva.com",
|
|
253
|
+
appOrigin: "https://files.finueva.com",
|
|
254
|
+
request,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const session = await auth.getSession();
|
|
259
|
+
|
|
260
|
+
if (!session) {
|
|
261
|
+
return Response.json(
|
|
262
|
+
{ authenticated: false },
|
|
263
|
+
{
|
|
264
|
+
status: 401,
|
|
265
|
+
headers: { "cache-control": "no-store" },
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Response.json(
|
|
271
|
+
{ user: session.user },
|
|
272
|
+
{
|
|
273
|
+
headers: { "cache-control": "no-store" },
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error instanceof AuthError) {
|
|
278
|
+
return Response.json(
|
|
279
|
+
{ code: error.code },
|
|
280
|
+
{
|
|
281
|
+
status: error.status,
|
|
282
|
+
headers: { "cache-control": "no-store" },
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
74
291
|
|
|
75
|
-
|
|
76
|
-
authOrigin: "https://auth.finueva.com",
|
|
77
|
-
appOrigin: "https://files.finueva.com",
|
|
78
|
-
request,
|
|
79
|
-
});
|
|
292
|
+
Some server mutations return cookies. Apply them to the outgoing response:
|
|
80
293
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
294
|
+
```ts
|
|
295
|
+
const result = await auth.signOut();
|
|
296
|
+
const headers = new Headers({
|
|
297
|
+
"cache-control": "no-store",
|
|
298
|
+
location: "/signed-out",
|
|
86
299
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
300
|
+
|
|
301
|
+
result.applyCookies(headers);
|
|
302
|
+
|
|
303
|
+
return new Response(null, { status: 303, headers });
|
|
90
304
|
```
|
|
91
305
|
|
|
92
|
-
|
|
306
|
+
Call `applyCookies()` after server `signOut()`, `acceptInvitation()`, and `signInWithSocialIdToken()`. If you omit it, the browser does not receive the session change.
|
|
93
307
|
|
|
94
|
-
|
|
308
|
+
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.
|
|
95
309
|
|
|
96
|
-
|
|
310
|
+
## CLI And Native Applications
|
|
311
|
+
|
|
312
|
+
Use the device flow when a CLI or device can open a system browser:
|
|
97
313
|
|
|
98
314
|
```ts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
signUp: false,
|
|
315
|
+
import { AuthError, DeviceAuthorizationError, createDeviceAuth } from "@finueva/auth/device";
|
|
316
|
+
|
|
317
|
+
const auth = createDeviceAuth({
|
|
318
|
+
authOrigin: "https://auth.finueva.com",
|
|
319
|
+
clientId: "finueva-cli",
|
|
105
320
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
321
|
+
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
process.once("SIGINT", () => controller.abort());
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const authorization = await auth.requestAuthorization({
|
|
327
|
+
signal: controller.signal,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
console.log(`Open ${authorization.verificationUriComplete}`);
|
|
331
|
+
console.log(`Code: ${authorization.userCode}`);
|
|
332
|
+
|
|
333
|
+
await auth.pollForToken(authorization, {
|
|
334
|
+
signal: controller.signal,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const session = await auth.getSession();
|
|
338
|
+
console.log(session ? `Signed in as ${session.user.email}` : "Not signed in");
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (error instanceof DeviceAuthorizationError || error instanceof AuthError) {
|
|
341
|
+
console.error(error.code);
|
|
342
|
+
} else {
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
109
346
|
```
|
|
110
347
|
|
|
111
|
-
|
|
348
|
+
`clientId` is a public registered identifier, not a secret. Never display or log `authorization.deviceCode`.
|
|
112
349
|
|
|
113
|
-
|
|
350
|
+
Device sessions are memory-only unless you provide `DeviceTokenPersistence`:
|
|
114
351
|
|
|
115
352
|
```ts
|
|
116
|
-
import { createDeviceAuth } from "@finueva/auth/device";
|
|
353
|
+
import { createDeviceAuth, type DeviceTokenPersistence } from "@finueva/auth/device";
|
|
354
|
+
|
|
355
|
+
const persistence: DeviceTokenPersistence = {
|
|
356
|
+
read: (signal) => vault.read("finueva-auth", signal),
|
|
357
|
+
replace: (token, signal) => vault.replace("finueva-auth", token, signal),
|
|
358
|
+
delete: (signal) => vault.delete("finueva-auth", signal),
|
|
359
|
+
};
|
|
117
360
|
|
|
118
361
|
const auth = createDeviceAuth({
|
|
119
362
|
authOrigin: "https://auth.finueva.com",
|
|
120
|
-
clientId: "finueva-
|
|
363
|
+
clientId: "finueva-desktop",
|
|
364
|
+
fetch: nativeFetch,
|
|
365
|
+
persistence,
|
|
121
366
|
});
|
|
122
|
-
if (auth._type !== "device") throw new Error("Expected a device Auth client");
|
|
123
|
-
const authorization = await auth.requestAuthorization();
|
|
124
|
-
showCode(authorization.userCode, authorization.verificationUri);
|
|
125
|
-
const token = await auth.pollForToken(authorization, { signal });
|
|
126
|
-
const session = await auth.getSession();
|
|
127
|
-
await auth.signUpWithEmail(email, password, name); // direct first-party mobile flow
|
|
128
|
-
await auth.signInWithEmail(email, password);
|
|
129
|
-
await auth.signInWithSocialIdToken({ provider: "apple", idToken, nonce: rawNonce, signUp: false });
|
|
130
|
-
await auth.updateProfile({ name, image });
|
|
131
|
-
await auth.changePassword(currentPassword, newPassword);
|
|
132
|
-
const credential = await auth.getDriveCredential({ workspace: { type: "personal" } });
|
|
133
|
-
await auth.revokeCurrentDevice(); // or revokeAllSessions()
|
|
134
367
|
```
|
|
135
368
|
|
|
136
|
-
|
|
369
|
+
Use the operating system's credential vault. `replace()` must replace the complete token atomically. Do not use local storage, a WebView cookie jar, or a plaintext file. A native application must also supply a reviewed native HTTPS transport that omits ambient WebView cookies.
|
|
137
370
|
|
|
138
|
-
|
|
371
|
+
Direct first-party native email sign-in is available to registered applications:
|
|
139
372
|
|
|
140
|
-
|
|
373
|
+
```ts
|
|
374
|
+
const session = await auth.signInWithEmail("ada@example.com", "correct-horse-battery-staple");
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
End the current device session with `revokeCurrentDevice()`. End every browser and device session for the account with `revokeAllSessions()`.
|
|
378
|
+
|
|
379
|
+
## Social ID Tokens
|
|
380
|
+
|
|
381
|
+
Browser, server, and device clients accept Google and Apple ID tokens from a first-party provider integration:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const session = await auth.signInWithSocialIdToken({
|
|
385
|
+
provider: "google",
|
|
386
|
+
idToken,
|
|
387
|
+
nonce,
|
|
388
|
+
signUp: false,
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
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.
|
|
141
393
|
|
|
142
|
-
|
|
394
|
+
The generic Google and Apple redirect flow is not exposed by this package.
|
|
143
395
|
|
|
144
|
-
|
|
396
|
+
## Errors
|
|
397
|
+
|
|
398
|
+
Invalid local arguments throw `TypeError`. Auth failures throw `AuthError`:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
try {
|
|
402
|
+
await auth.getSession();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (!(error instanceof AuthError)) throw error;
|
|
405
|
+
|
|
406
|
+
if (error.code === "auth_request_rate_limited") {
|
|
407
|
+
scheduleRetry(error.retryAfterSeconds ?? 60);
|
|
408
|
+
} else if (error.code === "identity_authority_unavailable") {
|
|
409
|
+
showAuthUnavailable();
|
|
410
|
+
} else {
|
|
411
|
+
showAuthError(error.code);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
145
415
|
|
|
146
|
-
|
|
416
|
+
Use `error.code` for application behavior. Do not parse `error.message`.
|
|
417
|
+
|
|
418
|
+
| Code | What the application should do |
|
|
419
|
+
| -------------------------------- | --------------------------------------------------------------------- |
|
|
420
|
+
| `invalid_credentials` | Ask the user to check the credential. |
|
|
421
|
+
| `invalid_auth_input` | Correct the account input. |
|
|
422
|
+
| `account_conflict` | Stop the account action. Do not expose account-existence details. |
|
|
423
|
+
| `authentication_required` | Start sign-in. |
|
|
424
|
+
| `email_verification_required` | Ask the user to verify the current email. |
|
|
425
|
+
| `invitation_unavailable` | Ask for a new invitation. Do not retry the old one. |
|
|
426
|
+
| `invalid_workspace_selection` | Send one exact personal or organization workspace. |
|
|
427
|
+
| `state_conflict` | Wait for the active authority operation, then read the session again. |
|
|
428
|
+
| `access_denied` | Stop the operation. |
|
|
429
|
+
| `credential_rate_limited` | Wait for `retryAfterSeconds` when present. |
|
|
430
|
+
| `auth_request_rate_limited` | Wait for `retryAfterSeconds` when present. |
|
|
431
|
+
| `identity_authority_unavailable` | Keep state unverified and try again later. |
|
|
432
|
+
|
|
433
|
+
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`.
|
|
434
|
+
|
|
435
|
+
The SDK does not automatically retry requests.
|
|
436
|
+
|
|
437
|
+
## Security Notes
|
|
438
|
+
|
|
439
|
+
- Use HTTPS. Plain HTTP is accepted only on localhost.
|
|
440
|
+
- Use exact registered origins. Do not build an Auth origin from the current request host.
|
|
441
|
+
- Keep browser session snapshots display-only.
|
|
442
|
+
- Create server clients per request. Do not keep one in a module singleton.
|
|
443
|
+
- Apply returned cookies before sending a server response.
|
|
444
|
+
- Keep device bearer tokens in an operating-system credential vault.
|
|
445
|
+
- Keep Drive credentials in memory and send them only to Drive.
|
|
446
|
+
- Do not log cookies, bearer tokens, ID tokens, reset tokens, device codes, or complete Auth responses.
|
|
447
|
+
- Protect application mutations against CSRF before calling the server client.
|
|
147
448
|
|
|
148
449
|
## Current Limits
|
|
149
450
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
451
|
+
- The public API is experimental and can change before `1.0.0`.
|
|
452
|
+
- Requests time out after 10 seconds by default. `timeoutMs` accepts 1 to 60,000 milliseconds.
|
|
453
|
+
- Auth JSON responses are limited to 64 KiB.
|
|
454
|
+
- Browser snapshots and bearer authorization values are limited to 16 KiB.
|
|
455
|
+
- Organization lists fail closed at 100 records.
|
|
456
|
+
- Organization Drive context supports at most 50 current teams.
|
|
457
|
+
- Account deletion and organization management are not exposed.
|
|
458
|
+
- Cross-root SSO and custom refresh tokens are not implemented.
|
|
459
|
+
- Hosted mobile transport and operating-system vault qualification are incomplete.
|
|
460
|
+
- Production support gates are not complete.
|