js-bao-wss-client 2.1.0-alpha.9 → 2.1.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 +59 -49
- package/dist/JsBaoClient.d.ts +101 -11
- package/dist/JsBaoClient.js +137 -8
- package/dist/api/cronTriggersApi.d.ts +24 -13
- package/dist/api/cronTriggersApi.js +11 -11
- package/dist/api/documentsApi.d.ts +5 -1
- package/dist/api/documentsApi.js +36 -3
- package/dist/browser.umd.js +391 -41
- package/dist/errors.d.ts +1 -1
- package/dist/internal/authController.d.ts +86 -5
- package/dist/internal/authController.js +94 -11
- package/dist/internal/webSocketManager.d.ts +17 -0
- package/dist/internal/webSocketManager.js +114 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -921,10 +921,17 @@ const all = await client.documents.list({ includeRoot: true });
|
|
|
921
921
|
|
|
922
922
|
## OAuth Authentication
|
|
923
923
|
|
|
924
|
+
Google registers a separate OAuth client per platform, so the server publishes a
|
|
925
|
+
client MAP keyed by client type (`web`, `ios`, `android`, `desktop`,
|
|
926
|
+
`chrome-extension`). A browser reads the `web` entry; there is no single
|
|
927
|
+
"is Google available" flag, because one could only ever be right for one
|
|
928
|
+
platform.
|
|
929
|
+
|
|
924
930
|
```typescript
|
|
925
|
-
// Check if
|
|
926
|
-
|
|
927
|
-
|
|
931
|
+
// Check if Google sign-in is available for THIS (browser) client: the provider
|
|
932
|
+
// is enabled and the web entry is usable.
|
|
933
|
+
const googleAvailable = await client.checkOAuthAvailable();
|
|
934
|
+
if (googleAvailable) {
|
|
928
935
|
// Start OAuth flow (redirects to Google)
|
|
929
936
|
await client.startOAuthFlow();
|
|
930
937
|
}
|
|
@@ -975,18 +982,45 @@ if (client.isAuthenticated()) {
|
|
|
975
982
|
client.setToken("new-jwt-token");
|
|
976
983
|
```
|
|
977
984
|
|
|
978
|
-
##
|
|
985
|
+
## Email Sign-In
|
|
979
986
|
|
|
980
|
-
|
|
987
|
+
One request sends ONE email carrying a 6-digit code and — when the redirect
|
|
988
|
+
target is on the app's `[auth].emailRedirectUris` allow-list — a sign-in link.
|
|
989
|
+
The user finishes with whichever suits them, and consuming either one retires
|
|
990
|
+
both: one email signs a user in once. Email sign-in must be enabled for the app
|
|
991
|
+
(`[auth].emailSignInEnabled`, on by default).
|
|
981
992
|
|
|
982
|
-
### Request
|
|
993
|
+
### Request the email
|
|
983
994
|
|
|
984
995
|
```typescript
|
|
985
|
-
//
|
|
986
|
-
|
|
996
|
+
// `redirectUri` defaults to the client's oauthRedirectUri. With no target at
|
|
997
|
+
// all the email carries the code alone, from the same template. A target that
|
|
998
|
+
// IS sent must match the app's non-empty allow-list, or the request is
|
|
999
|
+
// rejected 400 `Invalid redirect URI` — it never degrades to code-only.
|
|
1000
|
+
await client.emailSignInRequest("user@example.com", {
|
|
1001
|
+
redirectUri: "https://app.example.com/auth/callback",
|
|
1002
|
+
});
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
Both credentials expire together, 15 minutes after the request. Rate limits
|
|
1006
|
+
apply (5 requests per email per hour, 20 per IP per hour).
|
|
1007
|
+
|
|
1008
|
+
`magicLinkRequest` and `otpRequest` still work and are **deprecated**: both are
|
|
1009
|
+
aliases of the same issuance path and send the same email.
|
|
1010
|
+
|
|
1011
|
+
### Finish with the code
|
|
1012
|
+
|
|
1013
|
+
```typescript
|
|
1014
|
+
const { user, isNewUser } = await client.otpVerify("user@example.com", "123456");
|
|
1015
|
+
console.log("Logged in as:", user.email);
|
|
1016
|
+
|
|
1017
|
+
// isNewUser is true if this is the user's first sign-in (account was just created)
|
|
1018
|
+
if (isNewUser) {
|
|
1019
|
+
// Show onboarding flow for new users
|
|
1020
|
+
}
|
|
987
1021
|
```
|
|
988
1022
|
|
|
989
|
-
###
|
|
1023
|
+
### Finish with the link
|
|
990
1024
|
|
|
991
1025
|
```typescript
|
|
992
1026
|
// In your callback page (e.g., /oauth/callback)
|
|
@@ -998,7 +1032,6 @@ if (magicToken) {
|
|
|
998
1032
|
const { user, promptAddPasskey, isNewUser } = await client.magicLinkVerify(magicToken);
|
|
999
1033
|
console.log("Logged in as:", user.email);
|
|
1000
1034
|
|
|
1001
|
-
// isNewUser is true if this is the user's first sign-in (account was just created)
|
|
1002
1035
|
if (isNewUser) {
|
|
1003
1036
|
// Show onboarding flow for new users
|
|
1004
1037
|
}
|
|
@@ -1010,56 +1043,32 @@ if (magicToken) {
|
|
|
1010
1043
|
}
|
|
1011
1044
|
```
|
|
1012
1045
|
|
|
1013
|
-
## OTP (Email Code) Authentication
|
|
1014
|
-
|
|
1015
|
-
The client supports passwordless authentication via one-time 6-digit codes sent by email. OTP authentication must be enabled in the admin console for your app.
|
|
1016
|
-
|
|
1017
|
-
### Request OTP Code
|
|
1018
|
-
|
|
1019
|
-
```typescript
|
|
1020
|
-
// Send a 6-digit code to the user's email
|
|
1021
|
-
await client.otpRequest("user@example.com");
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
The code is valid for 10 minutes. Rate limits apply (5 codes per email per hour, 20 per IP per hour).
|
|
1025
|
-
|
|
1026
|
-
### Verify OTP Code
|
|
1027
|
-
|
|
1028
|
-
```typescript
|
|
1029
|
-
// Verify the code and complete authentication
|
|
1030
|
-
const { user, isNewUser } = await client.otpVerify("user@example.com", "123456");
|
|
1031
|
-
console.log("Logged in as:", user.email);
|
|
1032
|
-
|
|
1033
|
-
// isNewUser is true if this is the user's first sign-in (account was just created)
|
|
1034
|
-
if (isNewUser) {
|
|
1035
|
-
// Show onboarding flow for new users
|
|
1036
|
-
}
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
1046
|
### Error Handling
|
|
1040
1047
|
|
|
1041
1048
|
```typescript
|
|
1042
1049
|
try {
|
|
1043
1050
|
await client.otpVerify("user@example.com", "123456");
|
|
1044
1051
|
} catch (error) {
|
|
1045
|
-
if (error.code === "
|
|
1046
|
-
// OTP auth not enabled for this app
|
|
1047
|
-
} else if (error.code === "RATE_LIMITED") {
|
|
1052
|
+
if (error.code === "RATE_LIMITED") {
|
|
1048
1053
|
// Too many attempts, try again later
|
|
1049
1054
|
} else if (error.code === "OTP_MAX_ATTEMPTS") {
|
|
1050
|
-
// Maximum verification attempts exceeded, request a new
|
|
1055
|
+
// Maximum verification attempts exceeded, request a new email
|
|
1051
1056
|
} else if (error.code === "INVALID_TOKEN") {
|
|
1052
1057
|
// Invalid or expired code
|
|
1053
1058
|
}
|
|
1054
1059
|
}
|
|
1055
1060
|
```
|
|
1056
1061
|
|
|
1062
|
+
When email sign-in is disabled the request endpoints answer a plain 400 with
|
|
1063
|
+
`"Email sign-in is not enabled for this app"` and no error code — gate the UI
|
|
1064
|
+
on `getAuthConfig()`'s `emailSignInEnabled` instead.
|
|
1065
|
+
|
|
1057
1066
|
## Passkey Authentication
|
|
1058
1067
|
|
|
1059
1068
|
The client supports WebAuthn/passkey authentication for passwordless sign-in. Passkeys must be enabled in the admin console for your app.
|
|
1060
1069
|
|
|
1061
|
-
Note: Passkeys can only be added to existing accounts (created via OAuth or
|
|
1062
|
-
1. User creates account via OAuth or
|
|
1070
|
+
Note: Passkeys can only be added to existing accounts (created via OAuth or email sign-in). To use passkey authentication:
|
|
1071
|
+
1. User creates account via OAuth or email sign-in
|
|
1063
1072
|
2. User adds a passkey to their account
|
|
1064
1073
|
3. User can then sign in with the passkey on future visits
|
|
1065
1074
|
|
|
@@ -1073,14 +1082,15 @@ const config = await client.getAuthConfig();
|
|
|
1073
1082
|
if (config.hasPasskey) {
|
|
1074
1083
|
console.log("Passkeys are available");
|
|
1075
1084
|
}
|
|
1076
|
-
if (config.
|
|
1077
|
-
console.log("
|
|
1078
|
-
}
|
|
1079
|
-
if (config.otpEnabled) {
|
|
1080
|
-
console.log("OTP (email code) sign-in is available");
|
|
1085
|
+
if (config.emailSignInEnabled) {
|
|
1086
|
+
console.log("Email sign-in is available");
|
|
1081
1087
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1088
|
+
// Per-platform Google configuration. `usable` is the server's shape verdict for
|
|
1089
|
+
// one entry — a client id, at least one redirect URI, and a client secret
|
|
1090
|
+
// exactly when that client type takes one. Availability is that AND the
|
|
1091
|
+
// provider switch, which is what `checkOAuthAvailable()` computes.
|
|
1092
|
+
if (config.googleOAuthEnabled && config.googleClients.clients.web?.usable) {
|
|
1093
|
+
console.log("Google OAuth is available in the browser");
|
|
1084
1094
|
}
|
|
1085
1095
|
```
|
|
1086
1096
|
|
package/dist/JsBaoClient.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type SubscribeOptions } from "./internal/databaseSubscriptions";
|
|
|
8
8
|
import { type StorageConfig, type YjsPersistenceFactory } from "./internal/storage/index.js";
|
|
9
9
|
import { BlobManager } from "./internal/blobManager";
|
|
10
10
|
import { type AnalyticsEventInput } from "./internal/analyticsQueue";
|
|
11
|
+
import { type GoogleClientsConfig } from "./internal/authController";
|
|
11
12
|
import { type RequestOptions } from "./internal/httpClient";
|
|
12
13
|
import { type DocumentPermission as DocumentAccessLevel, type LocalDocumentEntry, type LocalMetadataEntry, type DocumentDebugSnapshot } from "./internal/documentManager";
|
|
13
14
|
import { DocumentsAPI, DocumentContext, type DocumentInfo, type ResolveAliasParams, type CreateDocumentOptions } from "./api/documentsApi";
|
|
@@ -32,8 +33,8 @@ import { InvitationsAPI } from "./api/invitationsApi";
|
|
|
32
33
|
import { NotificationsAPI } from "./api/notificationsApi";
|
|
33
34
|
export { JsBaoError, isJsBaoError, LockTimeoutError, JsBaoApiError, isJsBaoApiError, JsBaoNetworkError, isJsBaoNetworkError, } from "./errors";
|
|
34
35
|
export type { JsBaoErrorCode } from "./errors";
|
|
35
|
-
export { AuthError, AUTH_CODES } from "./internal/authController";
|
|
36
|
-
export type { AuthCode } from "./internal/authController";
|
|
36
|
+
export { AuthError, AUTH_CODES, googleWebClientAvailable, } from "./internal/authController";
|
|
37
|
+
export type { AuthCode, GoogleClientConfig, GoogleClientsConfig, } from "./internal/authController";
|
|
37
38
|
export type { DocumentInfo, DocumentPermissionEntry, DocumentInvitation, DocumentInvitationResponse, DocumentAccessResult, DocumentGroupPermissionEntry, DocumentAliasInfo, DocumentAliasScope, PermissionUpdateResult, DirectPermissionGrant, DeferredPermissionGrant, PendingInvitationEntry, PendingGroupInvitationEntry, LinkAccessResult, } from "./api/documentsApi";
|
|
38
39
|
export type { UserProfile, SharedDocument, SharedDocumentListResult, SharedDocumentsOptions, OwnedDocumentsOptions, } from "./api/meApi";
|
|
39
40
|
export type { SessionInfo } from "./api/sessionApi";
|
|
@@ -69,7 +70,7 @@ export type { GroupTypeConfigsAPI } from "./api/groupTypeConfigsApi";
|
|
|
69
70
|
export type { CollectionTypeConfigsAPI } from "./api/collectionTypeConfigsApi";
|
|
70
71
|
export type { DatabaseTypeConfigsAPI } from "./api/databaseTypeConfigsApi";
|
|
71
72
|
export type { CronTriggersAPI } from "./api/cronTriggersApi";
|
|
72
|
-
export type { CronTriggerInfo, CreateCronTriggerParams, UpdateCronTriggerParams, CronTriggerListResult, } from "./api/cronTriggersApi";
|
|
73
|
+
export type { CronTriggerInfo, CreateCronTriggerParams, UpdateCronTriggerParams, CronTriggerListResult, ObjectStatus, } from "./api/cronTriggersApi";
|
|
73
74
|
export type { CreateDocumentOptions, CreateOfflineOptions, CreateWithAliasOptions, GetOrCreateWithAliasOptions, DeleteDocumentOptions, DocumentAccessRequest, DocumentAccessRequestResponse, DocumentListOptions, DocumentListPage, EvictAllDocumentsOptions, EvictDocumentOptions, GrantGroupPermissionParams, ResolveAliasParams, SetAliasParams, UpdateDocumentData, } from "./api/documentsApi";
|
|
74
75
|
export type { CreateDatabaseParams, UpdateDatabaseParams } from "./api/databasesApi";
|
|
75
76
|
export type { CreateCollectionParams, UpdateCollectionParams, GrantCollectionGroupPermissionParams, ListCollectionsOptions, } from "./api/collectionsApi";
|
|
@@ -325,7 +326,10 @@ export interface PromptsAPI {
|
|
|
325
326
|
* refusal rejects with a `JsBaoApiError` whose `status` is `403` and whose
|
|
326
327
|
* `code` is `"PROMPT_ACCESS_DENIED"` — including when the prompt has no rule
|
|
327
328
|
* stored at all, which denies every caller who is not an app admin or owner.
|
|
328
|
-
* A prompt that is
|
|
329
|
+
* A prompt that is not in service rejects with a `400` whose `code` is
|
|
330
|
+
* `"PROMPT_NOT_EXECUTABLE"`. Availability is a single server-owned `status`:
|
|
331
|
+
* only an `active` prompt executes here, and `primitive prompts enable`
|
|
332
|
+
* puts an inactive one back in service.
|
|
329
333
|
*/
|
|
330
334
|
execute<V extends object = Record<string, unknown>, R = unknown>(promptKey: string, options: ExecutePromptOptions<V>): Promise<ExecutePromptResult<R>>;
|
|
331
335
|
}
|
|
@@ -384,6 +388,16 @@ export interface JsBaoClientOptions {
|
|
|
384
388
|
};
|
|
385
389
|
sync?: {
|
|
386
390
|
outboundDebounceMs?: number;
|
|
391
|
+
/**
|
|
392
|
+
* How long a handshake may take before the client gives up on it and lets
|
|
393
|
+
* the reconnect logic retry, in milliseconds. Defaults to 10000, and is
|
|
394
|
+
* also settable through `CLIENT_SYNC_HANDSHAKE_TIMEOUT_MS`.
|
|
395
|
+
*
|
|
396
|
+
* The budget covers both halves: opening the WebSocket (an endpoint that
|
|
397
|
+
* accepts the connection but never answers the upgrade is abandoned here,
|
|
398
|
+
* rather than holding the connect open forever) and the sync handshake
|
|
399
|
+
* that follows it.
|
|
400
|
+
*/
|
|
387
401
|
handshakeTimeoutMs?: number;
|
|
388
402
|
};
|
|
389
403
|
commitRetryBackoff?: {
|
|
@@ -1938,9 +1952,41 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
1938
1952
|
* only the union of a completed walk evicts. Anything short of that — a
|
|
1939
1953
|
* failed request, a server that ignores `limit` and answers with a bare
|
|
1940
1954
|
* array, a page that claims more rows with no cursor to ask for them, a
|
|
1941
|
-
* cursor that stops advancing, the page cap — evicts nothing.
|
|
1955
|
+
* cursor that stops advancing, the page cap — evicts nothing. Nor does a
|
|
1956
|
+
* completed walk whose union is empty: the request asks `includeRoot=true`,
|
|
1957
|
+
* so zero rows is an anomalous answer rather than a user with no documents
|
|
1958
|
+
* (#2859).
|
|
1942
1959
|
*/
|
|
1943
1960
|
private reconcileDocumentScope;
|
|
1961
|
+
/**
|
|
1962
|
+
* The hand-off from an unpaged `documents.list()` to the scope walk (#2919).
|
|
1963
|
+
*
|
|
1964
|
+
* `documentsApi` used to reconcile the caller's own listing response
|
|
1965
|
+
* `authoritative: true` whenever the request was not paged. But the unpaged
|
|
1966
|
+
* `GET /documents` is a bare array the server silently truncated at
|
|
1967
|
+
* dynamo-bao's 100-row `defaultQueryLimit`, so a truncated listing and a
|
|
1968
|
+
* complete one are the same bytes: reading one as the server's whole view of
|
|
1969
|
+
* the scope deleted the metadata row — and the cached CRDT state, unflushed
|
|
1970
|
+
* offline edits included — of every cached document past the truncation
|
|
1971
|
+
* point. So the response now merges only, and the eviction decision is left
|
|
1972
|
+
* to `reconcileDocumentScope`, whose paged walk is the only read that can
|
|
1973
|
+
* say where the scope ends. Mirrors the swift-client's
|
|
1974
|
+
* `reconcileByWalkingScope(seenIds:)` (#2827 / PR #2841).
|
|
1975
|
+
*
|
|
1976
|
+
* `seenIds` is what the caller's response already accounted for. When no
|
|
1977
|
+
* cached document is missing from it, an authoritative pass would have
|
|
1978
|
+
* nothing to delete and the walk is skipped — an app whose documents fit in
|
|
1979
|
+
* one response issues no extra request. The root is exempt from that
|
|
1980
|
+
* question: the listing filters it out of its own response, so it is always
|
|
1981
|
+
* "missing", and treating it as a truncation would put a scope walk behind
|
|
1982
|
+
* every listing.
|
|
1983
|
+
*
|
|
1984
|
+
* The walk is NOT forced. Above the truncation threshold every unpaged
|
|
1985
|
+
* listing looks truncated, so forcing would make each `documents.list()` pay
|
|
1986
|
+
* a full N/100-request walk; it shares the connect-time walk's staleness
|
|
1987
|
+
* window instead.
|
|
1988
|
+
*/
|
|
1989
|
+
private _reconcileScopeAfterListing;
|
|
1944
1990
|
private _runScopeReconcile;
|
|
1945
1991
|
/** Update local metadata cache with server document data.
|
|
1946
1992
|
* @param items - Array of server document records to merge into the local cache
|
|
@@ -2673,12 +2719,49 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
2673
2719
|
name: string;
|
|
2674
2720
|
mode: "public" | "invite-only" | "domain";
|
|
2675
2721
|
waitlistEnabled: boolean;
|
|
2676
|
-
|
|
2722
|
+
/**
|
|
2723
|
+
* Whether THIS (browser) client can start Google sign-in: the provider is
|
|
2724
|
+
* enabled and the `web` entry is usable. A single server-side flag could
|
|
2725
|
+
* not tell a web-configured app from a native-configured one, so this is
|
|
2726
|
+
* computed per platform from the client map.
|
|
2727
|
+
*/
|
|
2728
|
+
googleAvailable: boolean;
|
|
2677
2729
|
hasPasskey: boolean;
|
|
2730
|
+
/**
|
|
2731
|
+
* Is email sign-in available? ONE flag: one request sends one email
|
|
2732
|
+
* carrying both credentials.
|
|
2733
|
+
*/
|
|
2734
|
+
emailSignInEnabled: boolean;
|
|
2735
|
+
/** @deprecated Always equal to `emailSignInEnabled`. */
|
|
2678
2736
|
magicLinkEnabled: boolean;
|
|
2679
2737
|
}>;
|
|
2738
|
+
/**
|
|
2739
|
+
* Request one sign-in email. It carries a 6-digit code and — when a redirect
|
|
2740
|
+
* target is available AND the app allow-lists it — a sign-in link, so the
|
|
2741
|
+
* user can finish on the device they started on or on whichever device has
|
|
2742
|
+
* their mail. Nothing selects a method up front.
|
|
2743
|
+
*
|
|
2744
|
+
* Finish with `otpVerify(email, code)` for the code, or `magicLinkVerify(token)`
|
|
2745
|
+
* for the link. Consuming either one retires both: one email, one session.
|
|
2746
|
+
*
|
|
2747
|
+
* @param email - The email address to sign in
|
|
2748
|
+
* @param options - Optional configuration
|
|
2749
|
+
* @param options.redirectUri - Where the link should land. Defaults to the
|
|
2750
|
+
* client's `oauthRedirectUri`. With neither, the email carries the code
|
|
2751
|
+
* alone rather than failing.
|
|
2752
|
+
* @group Authentication
|
|
2753
|
+
*/
|
|
2754
|
+
emailSignInRequest(email: string, options?: {
|
|
2755
|
+
redirectUri?: string;
|
|
2756
|
+
}): Promise<{
|
|
2757
|
+
success: boolean;
|
|
2758
|
+
}>;
|
|
2680
2759
|
/**
|
|
2681
2760
|
* Request a magic link email for passwordless authentication.
|
|
2761
|
+
*
|
|
2762
|
+
* @deprecated Use {@link emailSignInRequest}. This is an alias of it — the
|
|
2763
|
+
* server issues through the same path and sends the same email, which now
|
|
2764
|
+
* also carries a 6-digit code.
|
|
2682
2765
|
* @param email - The email address to send the magic link to
|
|
2683
2766
|
* @param options - Optional configuration
|
|
2684
2767
|
* @param options.redirectUri - Override the default OAuth redirect URI for the magic link callback
|
|
@@ -2712,6 +2795,9 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
2712
2795
|
/**
|
|
2713
2796
|
* Request a one-time password (OTP) code to be sent to the specified email.
|
|
2714
2797
|
* The code can be verified using `otpVerify()`.
|
|
2798
|
+
*
|
|
2799
|
+
* @deprecated Use {@link emailSignInRequest}. This is an alias of it that
|
|
2800
|
+
* carries no redirect target, so the email it sends is code-only.
|
|
2715
2801
|
* @param email - The email address to send the OTP code to
|
|
2716
2802
|
* @group Authentication
|
|
2717
2803
|
*/
|
|
@@ -2839,17 +2925,21 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
2839
2925
|
mode: string;
|
|
2840
2926
|
waitlistEnabled: boolean;
|
|
2841
2927
|
googleOAuthEnabled: boolean;
|
|
2842
|
-
|
|
2843
|
-
hasOAuth: boolean;
|
|
2844
|
-
redirectUris: string[] | null;
|
|
2928
|
+
googleClients: GoogleClientsConfig;
|
|
2845
2929
|
passkeyEnabled: boolean;
|
|
2846
|
-
passkeyRpId: string | null;
|
|
2847
|
-
passkeyRpName: string | null;
|
|
2848
2930
|
passkeyRpConfig: Record<string, {
|
|
2849
2931
|
name: string;
|
|
2850
2932
|
}> | null;
|
|
2851
2933
|
hasPasskey: boolean;
|
|
2934
|
+
/**
|
|
2935
|
+
* Is email sign-in available at all (#2884)? ONE flag — one request sends
|
|
2936
|
+
* one email carrying both credentials, so there is no per-method
|
|
2937
|
+
* availability to report.
|
|
2938
|
+
*/
|
|
2939
|
+
emailSignInEnabled: boolean;
|
|
2940
|
+
/** @deprecated (#2884) Always equal to `emailSignInEnabled`. */
|
|
2852
2941
|
magicLinkEnabled: boolean;
|
|
2942
|
+
/** @deprecated (#2884) Always equal to `emailSignInEnabled`. */
|
|
2853
2943
|
otpEnabled: boolean;
|
|
2854
2944
|
hasApple: boolean;
|
|
2855
2945
|
appleSignInEnabled: boolean;
|
package/dist/JsBaoClient.js
CHANGED
|
@@ -11,7 +11,7 @@ import { deleteIdbByName } from "./internal/storage/idbUtils.js";
|
|
|
11
11
|
import { BlobManager } from "./internal/blobManager";
|
|
12
12
|
import { BrowserConnectivityMonitor, CONNECTIVITY_LOST, CONNECTIVITY_RESTORED, USER_SET, } from "./internal/connectivityMonitor";
|
|
13
13
|
import { createAnalyticsQueue, } from "./internal/analyticsQueue";
|
|
14
|
-
import { AuthController, exchangeOAuthCode, parseJwtPayload, } from "./internal/authController";
|
|
14
|
+
import { AuthController, exchangeOAuthCode, googleWebClientAvailable, parseJwtPayload, } from "./internal/authController";
|
|
15
15
|
import { HttpClient, } from "./internal/httpClient";
|
|
16
16
|
import { DocumentManager, } from "./internal/documentManager";
|
|
17
17
|
import { ensureArrayBuffer, toBase64, fromBase64, decompressGzip, } from "./utils/binary";
|
|
@@ -38,7 +38,7 @@ import { CollectionsAPI } from "./api/collectionsApi";
|
|
|
38
38
|
import { InvitationsAPI } from "./api/invitationsApi";
|
|
39
39
|
import { NotificationsAPI } from "./api/notificationsApi";
|
|
40
40
|
export { JsBaoError, isJsBaoError, LockTimeoutError, JsBaoApiError, isJsBaoApiError, JsBaoNetworkError, isJsBaoNetworkError, } from "./errors";
|
|
41
|
-
export { AuthError, AUTH_CODES } from "./internal/authController";
|
|
41
|
+
export { AuthError, AUTH_CODES, googleWebClientAvailable, } from "./internal/authController";
|
|
42
42
|
export { ANALYTICS_UNAUTHENTICATED_USER } from "./internal/analyticsQueue";
|
|
43
43
|
const DEFAULT_RETURN_ACTIVE_MIN_MS = 5 * 60 * 1000;
|
|
44
44
|
const DEFAULT_SYNC_ERROR_MIN_MS = 30 * 1000;
|
|
@@ -771,6 +771,10 @@ export class JsBaoClient extends Observable {
|
|
|
771
771
|
this.wsManager = new WebSocketManager({
|
|
772
772
|
logger,
|
|
773
773
|
maxReconnectDelayMs: this.maxReconnectDelay,
|
|
774
|
+
// The handshake budget covers the socket coming up as well as the sync
|
|
775
|
+
// handshake that follows it: an endpoint that accepts the connection and
|
|
776
|
+
// never answers the upgrade must not hold connect() open forever (#2923).
|
|
777
|
+
connectTimeoutMs: this.syncWatchdogTimeoutMs,
|
|
774
778
|
hasAccessToken: () => !!this.token,
|
|
775
779
|
buildConnectionRequest: (connectionId) => this.buildWebSocketRequest(connectionId),
|
|
776
780
|
createWebSocket: (url, headers) => this.createWebSocketInstance(url, headers),
|
|
@@ -1384,7 +1388,10 @@ export class JsBaoClient extends Observable {
|
|
|
1384
1388
|
* only the union of a completed walk evicts. Anything short of that — a
|
|
1385
1389
|
* failed request, a server that ignores `limit` and answers with a bare
|
|
1386
1390
|
* array, a page that claims more rows with no cursor to ask for them, a
|
|
1387
|
-
* cursor that stops advancing, the page cap — evicts nothing.
|
|
1391
|
+
* cursor that stops advancing, the page cap — evicts nothing. Nor does a
|
|
1392
|
+
* completed walk whose union is empty: the request asks `includeRoot=true`,
|
|
1393
|
+
* so zero rows is an anomalous answer rather than a user with no documents
|
|
1394
|
+
* (#2859).
|
|
1388
1395
|
*/
|
|
1389
1396
|
reconcileDocumentScope(options) {
|
|
1390
1397
|
if (this.scopeReconcileInFlight)
|
|
@@ -1418,6 +1425,62 @@ export class JsBaoClient extends Observable {
|
|
|
1418
1425
|
this.scopeReconcileInFlight = run;
|
|
1419
1426
|
return run;
|
|
1420
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* The hand-off from an unpaged `documents.list()` to the scope walk (#2919).
|
|
1430
|
+
*
|
|
1431
|
+
* `documentsApi` used to reconcile the caller's own listing response
|
|
1432
|
+
* `authoritative: true` whenever the request was not paged. But the unpaged
|
|
1433
|
+
* `GET /documents` is a bare array the server silently truncated at
|
|
1434
|
+
* dynamo-bao's 100-row `defaultQueryLimit`, so a truncated listing and a
|
|
1435
|
+
* complete one are the same bytes: reading one as the server's whole view of
|
|
1436
|
+
* the scope deleted the metadata row — and the cached CRDT state, unflushed
|
|
1437
|
+
* offline edits included — of every cached document past the truncation
|
|
1438
|
+
* point. So the response now merges only, and the eviction decision is left
|
|
1439
|
+
* to `reconcileDocumentScope`, whose paged walk is the only read that can
|
|
1440
|
+
* say where the scope ends. Mirrors the swift-client's
|
|
1441
|
+
* `reconcileByWalkingScope(seenIds:)` (#2827 / PR #2841).
|
|
1442
|
+
*
|
|
1443
|
+
* `seenIds` is what the caller's response already accounted for. When no
|
|
1444
|
+
* cached document is missing from it, an authoritative pass would have
|
|
1445
|
+
* nothing to delete and the walk is skipped — an app whose documents fit in
|
|
1446
|
+
* one response issues no extra request. The root is exempt from that
|
|
1447
|
+
* question: the listing filters it out of its own response, so it is always
|
|
1448
|
+
* "missing", and treating it as a truncation would put a scope walk behind
|
|
1449
|
+
* every listing.
|
|
1450
|
+
*
|
|
1451
|
+
* The walk is NOT forced. Above the truncation threshold every unpaged
|
|
1452
|
+
* listing looks truncated, so forcing would make each `documents.list()` pay
|
|
1453
|
+
* a full N/100-request walk; it shares the connect-time walk's staleness
|
|
1454
|
+
* window instead.
|
|
1455
|
+
*/
|
|
1456
|
+
async _reconcileScopeAfterListing(seenIds) {
|
|
1457
|
+
// The index is what says which documents are cached, and at launch it is
|
|
1458
|
+
// filled asynchronously from persistence. Asking before that read lands
|
|
1459
|
+
// finds nothing unaccounted for and skips a walk that was owed.
|
|
1460
|
+
try {
|
|
1461
|
+
await this._loadAllMetadataFromIdb();
|
|
1462
|
+
}
|
|
1463
|
+
catch { }
|
|
1464
|
+
const rootDocId = this.getRootDocId();
|
|
1465
|
+
let unaccounted = false;
|
|
1466
|
+
for (const [documentId, meta] of this.docManager.metadataIndex) {
|
|
1467
|
+
if (!documentId || seenIds.has(documentId))
|
|
1468
|
+
continue;
|
|
1469
|
+
if (rootDocId && documentId === rootDocId)
|
|
1470
|
+
continue;
|
|
1471
|
+
// Local-only documents and pending creates the server has never heard of
|
|
1472
|
+
// are absent from every listing, and say nothing about truncation.
|
|
1473
|
+
if (meta?.localOnly === true)
|
|
1474
|
+
continue;
|
|
1475
|
+
if (this.isPendingCreate(documentId))
|
|
1476
|
+
continue;
|
|
1477
|
+
unaccounted = true;
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
if (!unaccounted)
|
|
1481
|
+
return;
|
|
1482
|
+
void this.reconcileDocumentScope({ reason: "list-truncated" });
|
|
1483
|
+
}
|
|
1421
1484
|
async _runScopeReconcile(reason, generation) {
|
|
1422
1485
|
// The index is what says which documents are cached, and at launch it is
|
|
1423
1486
|
// filled asynchronously from persistence. Walking before that read lands
|
|
@@ -1504,11 +1567,29 @@ export class JsBaoClient extends Observable {
|
|
|
1504
1567
|
// A scope walked as one user says nothing about the one signed in now.
|
|
1505
1568
|
if (generation !== this.scopeReconcileGeneration)
|
|
1506
1569
|
return;
|
|
1570
|
+
// A completed walk that found nothing at all. The request asks
|
|
1571
|
+
// `includeRoot=true`, so the server's answer to it always carries at
|
|
1572
|
+
// least the caller's root: zero rows is an anomalous answer, not a user
|
|
1573
|
+
// with no documents, and reading it as the whole scope evicts every
|
|
1574
|
+
// cached document on the device (#2859). One row — the root, and
|
|
1575
|
+
// nothing else — is the user whose documents really were all deleted,
|
|
1576
|
+
// and that page still evicts the rest of the cache.
|
|
1577
|
+
if (union.length === 0) {
|
|
1578
|
+
logger.debug("[metadata] scope reconcile declined (walk found no documents)", {
|
|
1579
|
+
reason,
|
|
1580
|
+
pages: page + 1,
|
|
1581
|
+
});
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1507
1584
|
logger.debug("[metadata] scope reconcile walked the scope", {
|
|
1508
1585
|
reason,
|
|
1509
1586
|
documents: union.length,
|
|
1510
1587
|
pages: page + 1,
|
|
1511
1588
|
});
|
|
1589
|
+
// The walk asks for the root, so the union carries it — retaining it by
|
|
1590
|
+
// id as well means the cache does not depend on the server having said
|
|
1591
|
+
// so. Mirrors the swift client's `reconcileScopeAtConnect`.
|
|
1592
|
+
const rootDocId = this.getRootDocId();
|
|
1512
1593
|
await this.syncMetadata({
|
|
1513
1594
|
scope: "all",
|
|
1514
1595
|
payloadType: "full",
|
|
@@ -1516,6 +1597,7 @@ export class JsBaoClient extends Observable {
|
|
|
1516
1597
|
authoritative: true,
|
|
1517
1598
|
includeRoot: true,
|
|
1518
1599
|
background: true,
|
|
1600
|
+
retainIds: rootDocId ? [rootDocId] : undefined,
|
|
1519
1601
|
shouldRetain: (documentId) => !isEvictable(documentId),
|
|
1520
1602
|
});
|
|
1521
1603
|
return;
|
|
@@ -4614,7 +4696,13 @@ export class JsBaoClient extends Observable {
|
|
|
4614
4696
|
return new WebSocketImpl(finalUrl);
|
|
4615
4697
|
}
|
|
4616
4698
|
logger.log(`[DIAGNOSTIC] Node.js environment - using headers option`);
|
|
4617
|
-
|
|
4699
|
+
// `handshakeTimeout` makes the transport itself give up on an upgrade that
|
|
4700
|
+
// is never answered; the manager's own connect deadline covers the socket
|
|
4701
|
+
// implementations (browser included) that have no such option.
|
|
4702
|
+
return new WebSocketImpl(wsUrl, {
|
|
4703
|
+
headers,
|
|
4704
|
+
handshakeTimeout: this.syncWatchdogTimeoutMs,
|
|
4705
|
+
});
|
|
4618
4706
|
}
|
|
4619
4707
|
shouldTriggerHandshakeRecovery() {
|
|
4620
4708
|
// A close the client asked for is not a handshake the server refused. A
|
|
@@ -4625,6 +4713,15 @@ export class JsBaoClient extends Observable {
|
|
|
4625
4713
|
// close.
|
|
4626
4714
|
if (this.wsLastCloseInitiator === "forceReconnect")
|
|
4627
4715
|
return false;
|
|
4716
|
+
// An endpoint that never answered the upgrade refused nothing either: the
|
|
4717
|
+
// connect deadline abandoned the attempt without the server ever ruling on
|
|
4718
|
+
// the token (#2923). Treating that as an auth failure would refresh
|
|
4719
|
+
// against an endpoint that is not answering, and — because recovery
|
|
4720
|
+
// suppresses `shouldReconnect` — swallow the one reconnect the manager
|
|
4721
|
+
// would otherwise schedule, leaving the retry to a refresh that may never
|
|
4722
|
+
// succeed. A transport timeout belongs to the backoff loop.
|
|
4723
|
+
if (this.wsLastCloseInitiator === "connectTimeout")
|
|
4724
|
+
return false;
|
|
4628
4725
|
if (this.wsHandshakeCompleted)
|
|
4629
4726
|
return false;
|
|
4630
4727
|
if (this.wsHandshakeRecoveryInFlight)
|
|
@@ -7104,14 +7201,43 @@ export class JsBaoClient extends Observable {
|
|
|
7104
7201
|
name: config.name,
|
|
7105
7202
|
mode: config.mode,
|
|
7106
7203
|
waitlistEnabled: config.waitlistEnabled,
|
|
7107
|
-
|
|
7204
|
+
googleAvailable: googleWebClientAvailable(config),
|
|
7108
7205
|
hasPasskey: config.hasPasskey,
|
|
7109
|
-
|
|
7206
|
+
emailSignInEnabled: config.emailSignInEnabled,
|
|
7207
|
+
magicLinkEnabled: config.emailSignInEnabled,
|
|
7110
7208
|
};
|
|
7111
7209
|
}
|
|
7210
|
+
// ============ Email Sign-In (#2884) ============
|
|
7211
|
+
/**
|
|
7212
|
+
* Request one sign-in email. It carries a 6-digit code and — when a redirect
|
|
7213
|
+
* target is available AND the app allow-lists it — a sign-in link, so the
|
|
7214
|
+
* user can finish on the device they started on or on whichever device has
|
|
7215
|
+
* their mail. Nothing selects a method up front.
|
|
7216
|
+
*
|
|
7217
|
+
* Finish with `otpVerify(email, code)` for the code, or `magicLinkVerify(token)`
|
|
7218
|
+
* for the link. Consuming either one retires both: one email, one session.
|
|
7219
|
+
*
|
|
7220
|
+
* @param email - The email address to sign in
|
|
7221
|
+
* @param options - Optional configuration
|
|
7222
|
+
* @param options.redirectUri - Where the link should land. Defaults to the
|
|
7223
|
+
* client's `oauthRedirectUri`. With neither, the email carries the code
|
|
7224
|
+
* alone rather than failing.
|
|
7225
|
+
* @group Authentication
|
|
7226
|
+
*/
|
|
7227
|
+
async emailSignInRequest(email, options) {
|
|
7228
|
+
const redirectUri = options?.redirectUri || this.options.oauthRedirectUri;
|
|
7229
|
+
return this.auth.emailSignInRequest({
|
|
7230
|
+
email,
|
|
7231
|
+
...(redirectUri ? { redirectUri } : {}),
|
|
7232
|
+
});
|
|
7233
|
+
}
|
|
7112
7234
|
// ============ Magic Link Methods ============
|
|
7113
7235
|
/**
|
|
7114
7236
|
* Request a magic link email for passwordless authentication.
|
|
7237
|
+
*
|
|
7238
|
+
* @deprecated Use {@link emailSignInRequest}. This is an alias of it — the
|
|
7239
|
+
* server issues through the same path and sends the same email, which now
|
|
7240
|
+
* also carries a 6-digit code.
|
|
7115
7241
|
* @param email - The email address to send the magic link to
|
|
7116
7242
|
* @param options - Optional configuration
|
|
7117
7243
|
* @param options.redirectUri - Override the default OAuth redirect URI for the magic link callback
|
|
@@ -7142,6 +7268,9 @@ export class JsBaoClient extends Observable {
|
|
|
7142
7268
|
/**
|
|
7143
7269
|
* Request a one-time password (OTP) code to be sent to the specified email.
|
|
7144
7270
|
* The code can be verified using `otpVerify()`.
|
|
7271
|
+
*
|
|
7272
|
+
* @deprecated Use {@link emailSignInRequest}. This is an alias of it that
|
|
7273
|
+
* carries no redirect target, so the email it sends is code-only.
|
|
7145
7274
|
* @param email - The email address to send the OTP code to
|
|
7146
7275
|
* @group Authentication
|
|
7147
7276
|
*/
|
|
@@ -7247,8 +7376,8 @@ export class JsBaoClient extends Observable {
|
|
|
7247
7376
|
if (!this.token &&
|
|
7248
7377
|
this.options.autoOAuth &&
|
|
7249
7378
|
this.options.oauthRedirectUri) {
|
|
7250
|
-
const
|
|
7251
|
-
if (
|
|
7379
|
+
const googleAvailable = await this.auth.checkOAuthAvailable();
|
|
7380
|
+
if (googleAvailable) {
|
|
7252
7381
|
await this.auth.startOAuthFlow({
|
|
7253
7382
|
redirectUri: this.options.oauthRedirectUri,
|
|
7254
7383
|
});
|