@basictech/react 0.8.0-beta.4 → 0.9.0-beta.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/changelog.md +65 -0
- package/dist/index.d.mts +1258 -285
- package/dist/index.d.ts +1258 -285
- package/dist/index.js +2898 -1425
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2858 -1418
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -8
- package/readme.md +331 -214
- package/.turbo/turbo-build.log +0 -24
- package/AUTH_IMPLEMENTATION_GUIDE.md +0 -2011
- package/src/AuthContext.tsx +0 -591
- package/src/config.ts +0 -9
- package/src/context.tsx +0 -122
- package/src/core/auth/AuthManager.ts +0 -1371
- package/src/core/db/RemoteCollection.ts +0 -308
- package/src/core/db/RemoteDB.ts +0 -40
- package/src/core/db/index.ts +0 -7
- package/src/core/db/types.ts +0 -140
- package/src/dev/BasicDevToolbar.tsx +0 -665
- package/src/index.ts +0 -36
- package/src/sync/index.ts +0 -288
- package/src/sync/syncProtocol.js +0 -291
- package/src/sync/tokenRegistry.ts +0 -20
- package/src/updater/updateMigrations.ts +0 -22
- package/src/updater/versionUpdater.ts +0 -153
- package/src/utils/network.ts +0 -135
- package/src/utils/normalizeClientId.ts +0 -22
- package/src/utils/resolveDid.ts +0 -101
- package/src/utils/schema.ts +0 -119
- package/src/utils/storage.ts +0 -67
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,395 +1,1357 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
export { useLiveQuery as useQuery } from 'dexie-react-hooks';
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Dexie, { Table } from 'dexie';
|
|
4
3
|
|
|
4
|
+
interface BasicStorage {
|
|
5
|
+
get(key: string): Promise<string | null>;
|
|
6
|
+
set(key: string, value: string): Promise<void>;
|
|
7
|
+
remove(key: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
declare class LocalStorageAdapter implements BasicStorage {
|
|
10
|
+
get(key: string): Promise<string | null>;
|
|
11
|
+
set(key: string, value: string): Promise<void>;
|
|
12
|
+
remove(key: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
declare const STORAGE_KEYS: {
|
|
15
|
+
readonly REFRESH_TOKEN: "basic_refresh_token";
|
|
16
|
+
readonly USER_INFO: "basic_user_info";
|
|
17
|
+
readonly AUTH_STATE: "basic_auth_state";
|
|
18
|
+
readonly REDIRECT_URI: "basic_redirect_uri";
|
|
19
|
+
readonly SERVER_URL: "basic_server_url";
|
|
20
|
+
readonly PDS_ENDPOINTS: "basic_pds_endpoints";
|
|
21
|
+
readonly LAST_CONNECT_REPORT: "basic_last_connect_report";
|
|
22
|
+
readonly DEBUG: "basic_debug";
|
|
23
|
+
readonly CODE_VERIFIER: "basic_code_verifier";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Token = {
|
|
27
|
+
access_token: string;
|
|
28
|
+
token_type: string;
|
|
29
|
+
expires_in: number;
|
|
30
|
+
refresh_token: string;
|
|
31
|
+
};
|
|
32
|
+
type User = {
|
|
33
|
+
sub?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
email?: string;
|
|
36
|
+
picture?: string;
|
|
37
|
+
};
|
|
5
38
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
39
|
+
* High-level auth lifecycle state.
|
|
40
|
+
*
|
|
41
|
+
* - `bootstrapping` — SDK is initializing; not yet determined if a session exists.
|
|
42
|
+
* - `authenticated` — User has a valid access token and active session.
|
|
43
|
+
* - `recovering` — A session likely exists (refresh token / cached user) but
|
|
44
|
+
* the SDK hasn't confirmed it yet (e.g. offline, mid-refresh).
|
|
45
|
+
* - `reauth_required` — The session is definitively invalid (revoked, expired
|
|
46
|
+
* refresh token, etc.). The user must sign in again.
|
|
47
|
+
* NOTE: `isSignedIn` remains `true` in this state so the UI
|
|
48
|
+
* can display user info while prompting re-authentication.
|
|
49
|
+
* Use `authStatus === 'reauth_required'` to distinguish
|
|
50
|
+
* this from a healthy signed-in state.
|
|
51
|
+
* - `signed_out` — No session. User is not authenticated.
|
|
52
|
+
*
|
|
53
|
+
* TODO: revisit naming and ergonomics — consider adding a `needsReauth` or
|
|
54
|
+
* `shouldPromptSignIn` convenience getter so consumers don't need to inspect
|
|
55
|
+
* the raw status to decide between "sign in" vs "sign out" UI.
|
|
8
56
|
*/
|
|
57
|
+
type AuthStatus = 'bootstrapping' | 'authenticated' | 'recovering' | 'reauth_required' | 'signed_out';
|
|
58
|
+
type AuthResult = {
|
|
59
|
+
success: boolean;
|
|
60
|
+
error?: string;
|
|
61
|
+
code?: string;
|
|
62
|
+
};
|
|
63
|
+
type GetTokenOptions$1 = {
|
|
64
|
+
forceRefresh?: boolean;
|
|
65
|
+
};
|
|
66
|
+
type PdsEndpoints = {
|
|
67
|
+
pds_url: string;
|
|
68
|
+
authorization_endpoint: string;
|
|
69
|
+
token_endpoint: string;
|
|
70
|
+
userinfo_endpoint: string;
|
|
71
|
+
};
|
|
72
|
+
type AuthManagerConfig = {
|
|
73
|
+
projectId: string | undefined;
|
|
74
|
+
scopes: string;
|
|
75
|
+
pdsUrl: string;
|
|
76
|
+
adminUrl: string;
|
|
77
|
+
debug: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Identifies which local user profile this manager belongs to (multi-user).
|
|
80
|
+
* Cross-tab events are scoped to it so a tab active on another profile
|
|
81
|
+
* ignores them. Defaults to '' (single-user / legacy profile).
|
|
82
|
+
*/
|
|
83
|
+
instanceKey?: string;
|
|
84
|
+
};
|
|
9
85
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
86
|
+
* Framework-agnostic auth manager. Holds token state, handles OAuth flow,
|
|
87
|
+
* token refresh (with mutex), and user info fetching.
|
|
88
|
+
*
|
|
89
|
+
* React integration: pass a state-setter as `notify` so the component
|
|
90
|
+
* re-renders whenever auth state changes.
|
|
12
91
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
92
|
+
declare class AuthManager {
|
|
93
|
+
token: Token | null;
|
|
94
|
+
user: User | null;
|
|
95
|
+
isSignedIn: boolean;
|
|
96
|
+
isAuthReady: boolean;
|
|
97
|
+
authStatus: AuthStatus;
|
|
98
|
+
authErrorCode: string | null;
|
|
99
|
+
did: string | null;
|
|
100
|
+
/** Space-separated scopes granted in the current access token */
|
|
101
|
+
tokenScope: string | null;
|
|
102
|
+
/** Space-separated scopes originally requested in the auth config */
|
|
103
|
+
requestedScopes: string;
|
|
104
|
+
readonly config: AuthManagerConfig;
|
|
105
|
+
readonly storage: BasicStorage;
|
|
106
|
+
/** True only during a user-initiated OAuth code exchange (not session restore) */
|
|
107
|
+
private freshSignIn;
|
|
108
|
+
private notify;
|
|
109
|
+
private refreshPromise;
|
|
110
|
+
private codeExchangePromise;
|
|
111
|
+
private pendingRefresh;
|
|
112
|
+
private isOnline;
|
|
113
|
+
private channel;
|
|
114
|
+
private nextUserRecoveryAt;
|
|
115
|
+
private sessionCheckPromise;
|
|
116
|
+
private lastSessionCheckAt;
|
|
117
|
+
constructor(config: AuthManagerConfig, storage: BasicStorage, notify: () => void);
|
|
118
|
+
private get instanceKey();
|
|
119
|
+
private initCrossTabSync;
|
|
120
|
+
private broadcastTokenRefresh;
|
|
121
|
+
private broadcastSignIn;
|
|
122
|
+
private broadcastSignOut;
|
|
123
|
+
private broadcastSessionInvalidated;
|
|
124
|
+
/** Release resources (cross-tab channel). Used when switching users. */
|
|
125
|
+
destroy(): void;
|
|
18
126
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @returns The created object with its generated id
|
|
127
|
+
* Bootstrap auth: handle OAuth callback (?code=), restore session
|
|
128
|
+
* from refresh token, or load cached user for offline mode.
|
|
22
129
|
*/
|
|
23
|
-
|
|
130
|
+
initialize(): Promise<void>;
|
|
24
131
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @returns The upserted object
|
|
132
|
+
* Get a valid access token string. Refreshes proactively (5s buffer)
|
|
133
|
+
* or on demand (forceRefresh). Mutex prevents concurrent refreshes.
|
|
28
134
|
*/
|
|
29
|
-
|
|
135
|
+
getToken(options?: GetTokenOptions$1): Promise<string>;
|
|
136
|
+
getSignInUrl(redirectUri?: string, endpoints?: PdsEndpoints): Promise<string>;
|
|
137
|
+
signIn(redirectUri?: string): Promise<void>;
|
|
138
|
+
signInWithHandle(handle: string): Promise<void>;
|
|
139
|
+
signInWithCode(code: string, state?: string): Promise<AuthResult>;
|
|
30
140
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* @returns The updated object, or null if not found
|
|
141
|
+
* Sign out: revoke the session server-side (`POST /auth/logout`, best
|
|
142
|
+
* effort), then clear auth state and storage. Does NOT handle sync/DB
|
|
143
|
+
* cleanup — the client layer wraps this to add sync teardown.
|
|
35
144
|
*/
|
|
36
|
-
|
|
145
|
+
signOut(): Promise<void>;
|
|
37
146
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
147
|
+
* Best-effort server-side revocation of the current device/session and
|
|
148
|
+
* its refresh chain (Step 2 auth: logout is finally server-side).
|
|
149
|
+
* Never blocks or fails the local sign-out.
|
|
41
150
|
*/
|
|
42
|
-
|
|
151
|
+
private revokeSessionOnServer;
|
|
152
|
+
reconcileSession(reason?: string, options?: {
|
|
153
|
+
forceRefresh?: boolean;
|
|
154
|
+
throttleMs?: number;
|
|
155
|
+
}): Promise<void>;
|
|
156
|
+
hasScope(scope: string): boolean;
|
|
43
157
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @returns The object or null if not found
|
|
158
|
+
* Returns scopes that were requested but not granted in the current token.
|
|
159
|
+
* Useful after login or when a 403 is returned.
|
|
47
160
|
*/
|
|
48
|
-
|
|
161
|
+
missingScopes(): string[];
|
|
49
162
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
163
|
+
* Register online/offline and visibility handlers that retry pending
|
|
164
|
+
* refreshes and proactively refresh tokens when the app resumes from
|
|
165
|
+
* background (critical for PWAs and mobile browsers where timers are
|
|
166
|
+
* frozen while backgrounded).
|
|
167
|
+
* Returns a cleanup function for useEffect teardown.
|
|
52
168
|
*/
|
|
53
|
-
|
|
169
|
+
setupNetworkListeners(): () => void;
|
|
170
|
+
private get adminHostname();
|
|
171
|
+
private defaultPdsEndpoints;
|
|
172
|
+
private getActivePdsEndpoints;
|
|
173
|
+
private reportConnection;
|
|
54
174
|
/**
|
|
55
|
-
*
|
|
56
|
-
* @param fn - Filter function that returns true for matches
|
|
57
|
-
* @returns Array of matching objects
|
|
175
|
+
* After a new token is stored, decode JWT claims and fetch user info.
|
|
58
176
|
*/
|
|
59
|
-
|
|
177
|
+
private processNewToken;
|
|
178
|
+
private restoreCachedUser;
|
|
179
|
+
private fetchUser;
|
|
60
180
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
181
|
+
* Exchange an auth code or refresh token for an access token.
|
|
182
|
+
* Handles mutex (one in-flight refresh), token validation, and
|
|
183
|
+
* triggers processNewToken on success.
|
|
64
184
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
185
|
+
private exchangeToken;
|
|
186
|
+
private resetAuthState;
|
|
187
|
+
private clearStoredAuth;
|
|
188
|
+
private isNetworkError;
|
|
189
|
+
private getRefreshToken;
|
|
190
|
+
private syncRefreshTokenFromStorage;
|
|
191
|
+
private applyTokenClaims;
|
|
192
|
+
private broadcastSessionUpdate;
|
|
193
|
+
private handleUserFetchFailure;
|
|
194
|
+
private isCompatibleUser;
|
|
195
|
+
private recoverMissingUserProfile;
|
|
196
|
+
private isDefinitiveTokenErrorCode;
|
|
197
|
+
private isDefinitiveAuthFailure;
|
|
71
198
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
199
|
+
* Centralised auth status setter. Derives `isSignedIn` and `isAuthReady`
|
|
200
|
+
* from the status so they stay consistent.
|
|
201
|
+
*
|
|
202
|
+
* `isSignedIn` is intentionally `true` during `reauth_required` so the
|
|
203
|
+
* UI layer can still display user info while prompting re-authentication.
|
|
204
|
+
* Consumers should check `authStatus` (or a future convenience getter)
|
|
205
|
+
* when they need to distinguish "healthy session" from "needs re-auth".
|
|
75
206
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
207
|
+
private updateAuthStatus;
|
|
208
|
+
private clearStoredSessionTokens;
|
|
209
|
+
private restoreStoredSession;
|
|
210
|
+
private handleExternalTokenRefresh;
|
|
211
|
+
private fetchCurrentSession;
|
|
212
|
+
private markReauthRequired;
|
|
81
213
|
}
|
|
214
|
+
|
|
82
215
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
216
|
+
* Sync/2 wire protocol types — the client side of basic-server's
|
|
217
|
+
* `docs/SYNC_V2.md` contract (protocol version 1).
|
|
218
|
+
*
|
|
219
|
+
* WebSocket endpoint: `wss://<pds>/sync/`
|
|
220
|
+
* HTTP bootstrap: `GET /account/:project_id/db/snapshot`
|
|
221
|
+
* HTTP pull: `GET /account/:project_id/db/changes`
|
|
86
222
|
*/
|
|
87
|
-
|
|
223
|
+
declare const PROTOCOL_VERSION = 1;
|
|
224
|
+
type OpType = 'put' | 'patch' | 'delete';
|
|
225
|
+
/** An op as pushed by the client. `op_id` is the client-minted idempotency key. */
|
|
226
|
+
interface OpEnvelope {
|
|
227
|
+
op_id: string;
|
|
228
|
+
type: OpType;
|
|
229
|
+
table: string;
|
|
230
|
+
record_id: string;
|
|
231
|
+
data?: Record<string, unknown> | null;
|
|
232
|
+
/** What the writer had seen when creating the op. Recorded, never enforced (v1). */
|
|
233
|
+
base_seq?: number | null;
|
|
234
|
+
}
|
|
235
|
+
/** An op as delivered by the server (`ops` message / `changes` endpoint). */
|
|
236
|
+
interface LoggedOp extends OpEnvelope {
|
|
237
|
+
seq: number;
|
|
238
|
+
/** Server-stamped writer identity — never sent by the client. */
|
|
239
|
+
actor: string;
|
|
240
|
+
server_ts: string | number;
|
|
241
|
+
}
|
|
242
|
+
type SyncErrorCode = 'SCHEMA_VALIDATION_FAILED' | 'UNKNOWN_TABLE' | 'RECORD_NOT_FOUND' | 'PERMISSION_DENIED' | 'PAYLOAD_TOO_LARGE' | 'CHANNEL_FULL' | 'TOO_MANY_OPS' | 'RATE_LIMITED' | 'TRY_AGAIN' | 'SNAPSHOT_REQUIRED' | 'RESET_REQUIRED' | 'UNAUTHORIZED' | 'TOKEN_EXPIRED' | 'SHARE_REVOKED' | 'CONNECTION_REVOKED' | 'UNSUPPORTED_VERSION' | 'BAD_MESSAGE';
|
|
243
|
+
/** Terminal = poison-op rule applies: park in rejected store, never re-push. */
|
|
244
|
+
declare function isTerminalOpError(code: string | undefined, terminalFlag?: boolean): boolean;
|
|
245
|
+
/** Errors that mean "discard channel state, re-bootstrap from snapshot, resubscribe". */
|
|
246
|
+
declare function isRebootstrapError(code: string): boolean;
|
|
247
|
+
/** Errors that mean the subscription is gone and must not be retried. */
|
|
248
|
+
declare function isRevocationError(code: string): boolean;
|
|
249
|
+
/** Errors that mean the token was rejected; reauth then reconnect. */
|
|
250
|
+
declare function isAuthError(code: string): boolean;
|
|
251
|
+
interface SyncLimits {
|
|
252
|
+
max_ops_per_push: number;
|
|
253
|
+
max_op_bytes: number;
|
|
254
|
+
replay_limit: number;
|
|
255
|
+
}
|
|
256
|
+
declare const DEFAULT_LIMITS: SyncLimits;
|
|
88
257
|
/**
|
|
89
|
-
*
|
|
258
|
+
* First message on the socket. Sending only `{version, token}` opens a
|
|
259
|
+
* multiplexed session (auth-only welcome); streams are then bound with
|
|
260
|
+
* explicit `subscribe` messages. (The legacy single-subscription handshake
|
|
261
|
+
* with `cursor`/`share` on the hello is not used by this client.)
|
|
90
262
|
*/
|
|
91
|
-
interface
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
263
|
+
interface HelloMsg {
|
|
264
|
+
type: 'hello';
|
|
265
|
+
version: number;
|
|
266
|
+
token: string;
|
|
267
|
+
}
|
|
268
|
+
interface SubscribeFilter {
|
|
269
|
+
table: string;
|
|
270
|
+
record_ids?: string[];
|
|
99
271
|
}
|
|
272
|
+
interface SubscribeMsg {
|
|
273
|
+
type: 'subscribe';
|
|
274
|
+
sub: string;
|
|
275
|
+
/** Last seq seen. Required — a null/absent cursor gets SNAPSHOT_REQUIRED. */
|
|
276
|
+
cursor: number;
|
|
277
|
+
/** Mount a share instead of the token's own channel. */
|
|
278
|
+
share?: string;
|
|
279
|
+
/** Narrow an own-channel subscription (partial sync). */
|
|
280
|
+
filter?: SubscribeFilter;
|
|
281
|
+
}
|
|
282
|
+
interface UnsubscribeMsg {
|
|
283
|
+
type: 'unsubscribe';
|
|
284
|
+
sub: string;
|
|
285
|
+
}
|
|
286
|
+
interface PushMsg {
|
|
287
|
+
type: 'push';
|
|
288
|
+
sub: string;
|
|
289
|
+
ops: OpEnvelope[];
|
|
290
|
+
}
|
|
291
|
+
interface PingMsg {
|
|
292
|
+
type: 'ping';
|
|
293
|
+
cursor?: number;
|
|
294
|
+
}
|
|
295
|
+
/** Refresh auth on a live socket without reconnecting. */
|
|
296
|
+
interface TokenMsg {
|
|
297
|
+
type: 'token';
|
|
298
|
+
token: string;
|
|
299
|
+
}
|
|
300
|
+
type ClientMsg = HelloMsg | SubscribeMsg | UnsubscribeMsg | PushMsg | PingMsg | TokenMsg;
|
|
301
|
+
/** Multiplexed handshake: auth-only welcome (`channel`/`cursor` absent). */
|
|
302
|
+
interface WelcomeMsg {
|
|
303
|
+
type: 'welcome';
|
|
304
|
+
actor: string;
|
|
305
|
+
limits: SyncLimits;
|
|
306
|
+
channel?: string;
|
|
307
|
+
cursor?: number;
|
|
308
|
+
head?: number;
|
|
309
|
+
schema_version?: string | number | null;
|
|
310
|
+
}
|
|
311
|
+
interface SubscribedMsg {
|
|
312
|
+
type: 'subscribed';
|
|
313
|
+
sub: string;
|
|
314
|
+
channel: string;
|
|
315
|
+
cursor: number;
|
|
316
|
+
head: number;
|
|
317
|
+
schema_version: string | number | null;
|
|
318
|
+
}
|
|
319
|
+
interface UnsubscribedMsg {
|
|
320
|
+
type: 'unsubscribed';
|
|
321
|
+
sub: string;
|
|
322
|
+
}
|
|
323
|
+
interface OpsMsg {
|
|
324
|
+
type: 'ops';
|
|
325
|
+
sub: string;
|
|
326
|
+
/** May be empty — a filtered tail still advances `cursor`; always adopt it. */
|
|
327
|
+
ops: LoggedOp[];
|
|
328
|
+
cursor: number;
|
|
329
|
+
}
|
|
330
|
+
type PushResult = {
|
|
331
|
+
op_id: string;
|
|
332
|
+
seq: number;
|
|
333
|
+
} | {
|
|
334
|
+
op_id: string;
|
|
335
|
+
error: SyncErrorCode;
|
|
336
|
+
terminal?: boolean;
|
|
337
|
+
message?: string;
|
|
338
|
+
};
|
|
339
|
+
interface PushedMsg {
|
|
340
|
+
type: 'pushed';
|
|
341
|
+
sub: string;
|
|
342
|
+
results: PushResult[];
|
|
343
|
+
/** Channel head after commit. Absent when nothing was committed. */
|
|
344
|
+
cursor?: number;
|
|
345
|
+
}
|
|
346
|
+
interface PongMsg {
|
|
347
|
+
type: 'pong';
|
|
348
|
+
head: number;
|
|
349
|
+
heads?: Record<string, number>;
|
|
350
|
+
}
|
|
351
|
+
interface TokenOkMsg {
|
|
352
|
+
type: 'token_ok';
|
|
353
|
+
}
|
|
354
|
+
interface ErrorMsg {
|
|
355
|
+
type: 'error';
|
|
356
|
+
code: SyncErrorCode;
|
|
357
|
+
message?: string;
|
|
358
|
+
terminal?: boolean;
|
|
359
|
+
/** Present when the error concerns one subscription on a multiplexed socket. */
|
|
360
|
+
sub?: string;
|
|
361
|
+
}
|
|
362
|
+
type ServerMsg = WelcomeMsg | SubscribedMsg | UnsubscribedMsg | OpsMsg | PushedMsg | PongMsg | TokenOkMsg | ErrorMsg;
|
|
363
|
+
/** `GET /account/:project_id/db/snapshot` → `{ data: Snapshot }` */
|
|
364
|
+
interface Snapshot {
|
|
365
|
+
channel: string;
|
|
366
|
+
/** table → record_id → field values (tombstones excluded, no `id` field). */
|
|
367
|
+
records: Record<string, Record<string, Record<string, unknown>>>;
|
|
368
|
+
/** Head seq the snapshot is consistent with. */
|
|
369
|
+
cursor: number;
|
|
370
|
+
}
|
|
371
|
+
/** `GET /account/:project_id/db/changes` → `{ data: ChangesPage }` */
|
|
372
|
+
interface ChangesPage {
|
|
373
|
+
ops: LoggedOp[];
|
|
374
|
+
cursor: number;
|
|
375
|
+
more: boolean;
|
|
376
|
+
}
|
|
377
|
+
type SharePermission = 'read' | 'write';
|
|
378
|
+
interface ShareSelector {
|
|
379
|
+
table: string;
|
|
380
|
+
record_ids?: string[] | null;
|
|
381
|
+
}
|
|
382
|
+
/** A share row as returned by `GET /account/shares`. */
|
|
383
|
+
interface Share {
|
|
384
|
+
id: string;
|
|
385
|
+
account_id: string;
|
|
386
|
+
channel_id: string;
|
|
387
|
+
grantee: string;
|
|
388
|
+
selector: ShareSelector;
|
|
389
|
+
permission: SharePermission;
|
|
390
|
+
created_at: string;
|
|
391
|
+
expires_at: string | null;
|
|
392
|
+
revoked_at: string | null;
|
|
393
|
+
}
|
|
394
|
+
/** Client-minted op id (idempotency key). */
|
|
395
|
+
declare function mintOpId(): string;
|
|
100
396
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
397
|
+
* Apply one op to a record's field data. Returns the new data, or `undefined`
|
|
398
|
+
* when the record does not exist (deleted / never created).
|
|
399
|
+
*
|
|
400
|
+
* - `put`: create or replace the whole record
|
|
401
|
+
* - `patch`: shallow-merge top-level fields; no effect on a missing record
|
|
402
|
+
* - `delete`: tombstone (idempotent)
|
|
103
403
|
*/
|
|
104
|
-
declare
|
|
105
|
-
|
|
106
|
-
response?: any;
|
|
107
|
-
constructor(message: string, status: number, response?: any);
|
|
108
|
-
}
|
|
404
|
+
declare function applyOpToData(existing: Record<string, unknown> | undefined, op: OpEnvelope): Record<string, unknown> | undefined;
|
|
405
|
+
|
|
109
406
|
/**
|
|
110
|
-
*
|
|
407
|
+
* RestClient — the REST v2 surface under `/account/:project_id/db/...`.
|
|
408
|
+
*
|
|
409
|
+
* Used by the sync engine for bootstrap (`snapshot`) and pull sync
|
|
410
|
+
* (`changes`), by the shares API, and by the REST-mode table API (CRUD on
|
|
411
|
+
* materialized state). All writes go through the same server-side sync
|
|
412
|
+
* engine as WebSocket pushes — one write path.
|
|
111
413
|
*/
|
|
112
|
-
|
|
113
|
-
|
|
414
|
+
|
|
415
|
+
interface GetTokenOptions {
|
|
114
416
|
forceRefresh?: boolean;
|
|
115
417
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
418
|
+
declare class RestError extends Error {
|
|
419
|
+
readonly status: number;
|
|
420
|
+
readonly code?: string;
|
|
421
|
+
readonly response?: unknown;
|
|
422
|
+
constructor(message: string, status: number, code?: string, response?: unknown);
|
|
423
|
+
}
|
|
424
|
+
declare class NotAuthenticatedError extends Error {
|
|
425
|
+
constructor(message?: string);
|
|
426
|
+
}
|
|
427
|
+
interface RestClientOptions {
|
|
428
|
+
/** PDS base URL, e.g. `https://pds.basic.id` */
|
|
429
|
+
baseUrl: string;
|
|
430
|
+
/** Project id (UUID or DID) — used in `/account/:project_id/...` paths. */
|
|
121
431
|
projectId: string;
|
|
122
|
-
getToken: (options?: GetTokenOptions
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
432
|
+
getToken: (options?: GetTokenOptions) => Promise<string>;
|
|
433
|
+
log?: (...args: unknown[]) => void;
|
|
434
|
+
}
|
|
435
|
+
/** A record as returned by REST reads: flattened `{ id, ...fields }`. */
|
|
436
|
+
type RestRecord = {
|
|
437
|
+
id: string;
|
|
438
|
+
} & Record<string, unknown>;
|
|
439
|
+
declare class RestClient {
|
|
440
|
+
private readonly opts;
|
|
441
|
+
constructor(opts: RestClientOptions);
|
|
442
|
+
get projectId(): string;
|
|
443
|
+
/** `GET /account/:project_id/db` — tables, enforced schema version, channel head. */
|
|
444
|
+
getDbInfo(): Promise<{
|
|
445
|
+
tables: string[];
|
|
446
|
+
schema_version: string | number | null;
|
|
447
|
+
channel: {
|
|
448
|
+
id: string;
|
|
449
|
+
head_seq: number;
|
|
450
|
+
compacted_to_seq: number;
|
|
451
|
+
};
|
|
452
|
+
}>;
|
|
453
|
+
/** Bootstrap snapshot (SPEC §5). `share` bootstraps a mount; `table` filters. */
|
|
454
|
+
getSnapshot(options?: {
|
|
455
|
+
share?: string;
|
|
456
|
+
table?: string;
|
|
457
|
+
}): Promise<Snapshot>;
|
|
458
|
+
/** Pull ordered ops after a cursor — the non-WebSocket sync path. */
|
|
459
|
+
getChanges(options: {
|
|
460
|
+
cursor: number;
|
|
461
|
+
limit?: number;
|
|
462
|
+
share?: string;
|
|
463
|
+
table?: string;
|
|
464
|
+
}): Promise<ChangesPage>;
|
|
126
465
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
466
|
+
* Shares granted by and received by the caller. App tokens see only
|
|
467
|
+
* shares involving their own app (the ones they can mount).
|
|
129
468
|
*/
|
|
130
|
-
|
|
469
|
+
listShares(): Promise<{
|
|
470
|
+
granted: Share[];
|
|
471
|
+
received: Share[];
|
|
472
|
+
}>;
|
|
473
|
+
list(table: string, query?: Record<string, string>): Promise<RestRecord[]>;
|
|
474
|
+
getRecord(table: string, id: string): Promise<RestRecord | null>;
|
|
475
|
+
/** `POST` — server mints the record id. */
|
|
476
|
+
createRecord(table: string, value: Record<string, unknown>): Promise<RestRecord>;
|
|
477
|
+
/** `PUT` — full replace. REST semantics: 404 for missing records. */
|
|
478
|
+
putRecord(table: string, id: string, value: Record<string, unknown>): Promise<RestRecord | null>;
|
|
479
|
+
/** `PATCH` — partial merge. 404 → null. */
|
|
480
|
+
patchRecord(table: string, id: string, value: Record<string, unknown>): Promise<RestRecord | null>;
|
|
481
|
+
/** `DELETE`. Returns false when the record did not exist. */
|
|
482
|
+
deleteRecord(table: string, id: string): Promise<boolean>;
|
|
483
|
+
private get dbPath();
|
|
484
|
+
/** Authenticated request; retries once with a force-refreshed token on 401. */
|
|
485
|
+
private request;
|
|
131
486
|
}
|
|
132
487
|
|
|
133
488
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
489
|
+
* SyncStore — persistent local state for one sync keyspace (an own channel or
|
|
490
|
+
* a mounted share), backed by plain Dexie/IndexedDB.
|
|
491
|
+
*
|
|
492
|
+
* Layout (one Dexie database per keyspace):
|
|
493
|
+
* - one **view** store per app table: `{ id, ...fields }` — what the app
|
|
494
|
+
* reads (server state + pending ops rebased). `useQuery`/liveQuery reads
|
|
495
|
+
* these stores directly.
|
|
496
|
+
* - `_server`: confirmed records only, keyed `[table+record_id]`.
|
|
497
|
+
* - `_pending`: queued local ops (survive restarts), insertion-ordered.
|
|
498
|
+
* - `_rejected`: terminally rejected ops (poison-op rule — never re-pushed).
|
|
499
|
+
* - `_meta`: cursor, channel id, limits, schema version.
|
|
500
|
+
*
|
|
501
|
+
* The app-visible view is always `serverState + pending applied in order`
|
|
502
|
+
* (client responsibility #6). Rebasing is done per affected record inside
|
|
503
|
+
* the same transaction as the change that triggered it.
|
|
136
504
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
505
|
+
|
|
506
|
+
interface PendingRow {
|
|
507
|
+
/** Auto-increment — preserves creation order across restarts. */
|
|
508
|
+
idx?: number;
|
|
509
|
+
op_id: string;
|
|
510
|
+
op: OpEnvelope;
|
|
511
|
+
/** Set when the server acked the op (`pushed` seq) but the echo hasn't arrived yet. */
|
|
512
|
+
acked_seq?: number;
|
|
513
|
+
}
|
|
514
|
+
interface RejectedRow {
|
|
515
|
+
idx?: number;
|
|
516
|
+
op_id: string;
|
|
517
|
+
op: OpEnvelope;
|
|
518
|
+
error: SyncErrorCode | string;
|
|
519
|
+
message?: string;
|
|
520
|
+
rejected_at: number;
|
|
521
|
+
}
|
|
522
|
+
interface SyncStoreSchema {
|
|
523
|
+
version?: number;
|
|
524
|
+
tables: Record<string, {
|
|
525
|
+
fields: Record<string, {
|
|
526
|
+
indexed?: boolean;
|
|
527
|
+
}>;
|
|
528
|
+
}>;
|
|
529
|
+
}
|
|
530
|
+
declare class SyncStore {
|
|
531
|
+
readonly db: Dexie;
|
|
532
|
+
readonly name: string;
|
|
533
|
+
private readonly tableNames;
|
|
534
|
+
constructor(name: string, schema: SyncStoreSchema);
|
|
535
|
+
/** The Dexie view table for an app table (what liveQuery reads). */
|
|
536
|
+
view(table: string): Table<Record<string, unknown>, string>;
|
|
537
|
+
hasTable(table: string): boolean;
|
|
538
|
+
get tables(): string[];
|
|
539
|
+
private get server();
|
|
540
|
+
private get pending();
|
|
541
|
+
private get rejected();
|
|
542
|
+
private get meta();
|
|
543
|
+
private get allStores();
|
|
544
|
+
getCursor(): Promise<number | null>;
|
|
545
|
+
getChannel(): Promise<string | null>;
|
|
546
|
+
/**
|
|
547
|
+
* The account DID this keyspace's confirmed data belongs to. Absent for
|
|
548
|
+
* anonymous-era data (which may be merged into whichever account signs in).
|
|
549
|
+
*/
|
|
550
|
+
getOwner(): Promise<string | null>;
|
|
551
|
+
setOwner(did: string): Promise<void>;
|
|
552
|
+
/**
|
|
553
|
+
* Clear everything (views, server state, pending, rejected, meta) without
|
|
554
|
+
* deleting the database — used when the keyspace changes owners.
|
|
555
|
+
*/
|
|
556
|
+
wipeAll(): Promise<void>;
|
|
557
|
+
/** All pending ops in creation order (used to warm the in-memory queue). */
|
|
558
|
+
loadPending(): Promise<PendingRow[]>;
|
|
559
|
+
listRejected(): Promise<RejectedRow[]>;
|
|
560
|
+
clearRejected(): Promise<void>;
|
|
561
|
+
/** Record a server ack for a pending op (echo not yet seen). */
|
|
562
|
+
markAcked(opId: string, seq: number): Promise<void>;
|
|
141
563
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
564
|
+
* Enqueue a local op and apply it optimistically to the view.
|
|
565
|
+
* Returns the resulting view record (null when the op deletes it).
|
|
144
566
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
567
|
+
addPending(op: OpEnvelope): Promise<Record<string, unknown> | null>;
|
|
568
|
+
/**
|
|
569
|
+
* Commit a batch of incoming server ops (already filtered/deduped by the
|
|
570
|
+
* engine): update `_server`, drop confirmed pending ops, advance the
|
|
571
|
+
* cursor, and rebase every affected view record — in one transaction.
|
|
572
|
+
*/
|
|
573
|
+
commitIncoming(params: {
|
|
574
|
+
applyOps: LoggedOp[];
|
|
575
|
+
confirmedOpIds: string[];
|
|
576
|
+
cursor: number;
|
|
577
|
+
}): Promise<void>;
|
|
578
|
+
/** Persist a cursor advance with no ops (empty `ops` message / pushed cursor). */
|
|
579
|
+
setCursor(cursor: number): Promise<void>;
|
|
580
|
+
/**
|
|
581
|
+
* Terminal rejection: remove from pending, park in the rejected store,
|
|
582
|
+
* roll the view record back to server state + remaining pending ops.
|
|
583
|
+
*/
|
|
584
|
+
rejectPending(opId: string, error: SyncErrorCode | string, message?: string): Promise<RejectedRow | null>;
|
|
585
|
+
/**
|
|
586
|
+
* Replace all server state from a snapshot (cold start or
|
|
587
|
+
* SNAPSHOT_REQUIRED/RESET_REQUIRED recovery). Pending ops survive and are
|
|
588
|
+
* re-applied on top of the fresh state.
|
|
589
|
+
*/
|
|
590
|
+
replaceFromSnapshot(params: {
|
|
591
|
+
channel: string;
|
|
592
|
+
records: Record<string, Record<string, Record<string, unknown>>>;
|
|
593
|
+
cursor: number;
|
|
594
|
+
}): Promise<void>;
|
|
595
|
+
getViewRecord(table: string, id: string): Promise<Record<string, unknown> | null>;
|
|
596
|
+
getViewRecords(table: string): Promise<Record<string, unknown>[]>;
|
|
597
|
+
close(): void;
|
|
598
|
+
/** Delete the underlying IndexedDB database (sign-out / revoked mount). */
|
|
599
|
+
destroy(): Promise<void>;
|
|
600
|
+
private applyToServer;
|
|
601
|
+
/**
|
|
602
|
+
* Rebase one record: view = server data + pending ops for that record in
|
|
603
|
+
* creation order. Must run inside a transaction covering all stores.
|
|
604
|
+
*/
|
|
605
|
+
private recomputeViewRecord;
|
|
150
606
|
}
|
|
151
607
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
608
|
+
type SyncStatus = 'idle' | 'local' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped';
|
|
609
|
+
declare const OWN_SUB = "own";
|
|
610
|
+
declare function shareSubKey(shareId: string): string;
|
|
611
|
+
interface PendingEntry {
|
|
612
|
+
op: OpEnvelope;
|
|
613
|
+
sent: boolean;
|
|
614
|
+
ackedSeq?: number;
|
|
157
615
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
616
|
+
type SubStatus = 'initializing' | 'live' | 'revoked' | 'error';
|
|
617
|
+
declare class BoundedSet {
|
|
618
|
+
private readonly cap;
|
|
619
|
+
private set;
|
|
620
|
+
private order;
|
|
621
|
+
constructor(cap?: number);
|
|
622
|
+
has(value: string): boolean;
|
|
623
|
+
add(value: string): void;
|
|
624
|
+
clear(): void;
|
|
625
|
+
}
|
|
626
|
+
interface SubscriptionState {
|
|
627
|
+
key: string;
|
|
628
|
+
shareId: string | null;
|
|
629
|
+
store: SyncStore;
|
|
630
|
+
cursor: number;
|
|
631
|
+
pending: PendingEntry[];
|
|
632
|
+
/** Bound on the current socket (subscribed ack received). */
|
|
633
|
+
active: boolean;
|
|
634
|
+
bootstrapped: boolean;
|
|
635
|
+
status: SubStatus;
|
|
636
|
+
revokedCode?: string;
|
|
637
|
+
appliedOpIds: BoundedSet;
|
|
638
|
+
/** Serializes ops/pushed/bootstrap handling per subscription. */
|
|
639
|
+
chain: Promise<void>;
|
|
640
|
+
schemaVersion: string | number | null;
|
|
641
|
+
}
|
|
642
|
+
interface SyncEngineEvents {
|
|
643
|
+
status: SyncStatus;
|
|
644
|
+
/** Records changed in a subscription's view (local write or server ops). */
|
|
645
|
+
change: {
|
|
646
|
+
sub: string;
|
|
647
|
+
tables: string[];
|
|
648
|
+
};
|
|
649
|
+
rejected: {
|
|
650
|
+
sub: string;
|
|
651
|
+
rejection: RejectedRow;
|
|
652
|
+
};
|
|
653
|
+
/** Sub-level protocol errors (share revoked, re-bootstrapping, ...). */
|
|
654
|
+
suberror: {
|
|
655
|
+
sub: string;
|
|
656
|
+
code: string;
|
|
657
|
+
message?: string;
|
|
658
|
+
};
|
|
659
|
+
/** Connection-level revocation — the app connection is gone. */
|
|
660
|
+
revoked: {
|
|
661
|
+
code: string;
|
|
662
|
+
message?: string;
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
interface EngineSchema {
|
|
666
|
+
project_id?: string;
|
|
667
|
+
version?: number;
|
|
668
|
+
tables: Record<string, {
|
|
669
|
+
fields: Record<string, {
|
|
670
|
+
type: string;
|
|
671
|
+
required?: boolean;
|
|
672
|
+
indexed?: boolean;
|
|
673
|
+
}>;
|
|
674
|
+
}>;
|
|
675
|
+
}
|
|
676
|
+
interface SyncEngineOptions {
|
|
677
|
+
projectId: string;
|
|
678
|
+
schema: EngineSchema;
|
|
679
|
+
/** e.g. `wss://pds.basic.id/sync/` */
|
|
680
|
+
wsUrl: string;
|
|
681
|
+
getToken: (options?: {
|
|
682
|
+
forceRefresh?: boolean;
|
|
683
|
+
}) => Promise<string>;
|
|
684
|
+
/** Bootstrap fetch (REST snapshot). Injected so the engine stays transport-thin. */
|
|
685
|
+
fetchSnapshot: (options?: {
|
|
686
|
+
share?: string;
|
|
687
|
+
}) => Promise<Snapshot>;
|
|
688
|
+
/** Prefix for IndexedDB database names. Default `basic-sync`. */
|
|
689
|
+
dbNamePrefix?: string;
|
|
171
690
|
/**
|
|
172
|
-
*
|
|
691
|
+
* Keyspace segment appended to database names (multi-user: the local user
|
|
692
|
+
* id). `''`/unset = the legacy single-user keyspace.
|
|
173
693
|
*/
|
|
174
|
-
|
|
694
|
+
keyspaceId?: string;
|
|
175
695
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
696
|
+
* The account DID the current session belongs to, consulted at bootstrap
|
|
697
|
+
* for the owner guard: a keyspace stamped with a different DID is wiped
|
|
698
|
+
* before bootstrapping (never merge one account's local data into
|
|
699
|
+
* another); an unstamped keyspace (anonymous-era data) keeps its pending
|
|
700
|
+
* ops — that is the anonymous → signed-in migration path.
|
|
178
701
|
*/
|
|
179
|
-
|
|
702
|
+
getOwnerDid?: () => string | null | Promise<string | null>;
|
|
703
|
+
/**
|
|
704
|
+
* App name included in `subscribe` messages. Production derives the channel
|
|
705
|
+
* from the token and ignores this; the sync-playground conformance server
|
|
706
|
+
* requires it. Leave unset against production.
|
|
707
|
+
*/
|
|
708
|
+
appName?: string;
|
|
709
|
+
WebSocketImpl?: typeof WebSocket;
|
|
710
|
+
heartbeatMs?: number;
|
|
711
|
+
/** Validate writes locally before queueing (mirrors server). Default true. */
|
|
712
|
+
validateWrites?: boolean;
|
|
713
|
+
log?: (...args: unknown[]) => void;
|
|
714
|
+
}
|
|
715
|
+
declare class SyncEngine {
|
|
716
|
+
readonly projectId: string;
|
|
717
|
+
readonly schema: EngineSchema;
|
|
718
|
+
private readonly opts;
|
|
719
|
+
private readonly connection;
|
|
720
|
+
private readonly subs;
|
|
721
|
+
private limits;
|
|
722
|
+
private actor;
|
|
723
|
+
/** Own-sub store is open (local reads/writes work). */
|
|
724
|
+
private storesOpen;
|
|
725
|
+
/** A live connection is wanted (vs. local-only / paused). */
|
|
726
|
+
private connectIntended;
|
|
727
|
+
private openingLocal;
|
|
728
|
+
private revokedInfo;
|
|
729
|
+
private connectionStatus;
|
|
730
|
+
private _status;
|
|
731
|
+
private listeners;
|
|
732
|
+
private timers;
|
|
733
|
+
constructor(opts: SyncEngineOptions);
|
|
734
|
+
on<E extends keyof SyncEngineEvents>(event: E, fn: (data: SyncEngineEvents[E]) => void): () => void;
|
|
735
|
+
private emit;
|
|
736
|
+
get status(): SyncStatus;
|
|
737
|
+
get syncLimits(): SyncLimits;
|
|
738
|
+
get serverActor(): string | null;
|
|
739
|
+
getSubscription(key: string): SubscriptionState | undefined;
|
|
740
|
+
get own(): SubscriptionState | undefined;
|
|
741
|
+
get pendingCount(): number;
|
|
742
|
+
listRejected(subKey?: string): Promise<RejectedRow[]>;
|
|
743
|
+
clearRejected(subKey?: string): Promise<void>;
|
|
180
744
|
/**
|
|
181
|
-
*
|
|
745
|
+
* Open the own-channel keyspace for local reads/writes — no connection,
|
|
746
|
+
* no token needed. This is the anonymous / offline-cold-start entry point.
|
|
747
|
+
* Idempotent.
|
|
182
748
|
*/
|
|
183
|
-
|
|
749
|
+
openLocal(): Promise<void>;
|
|
184
750
|
/**
|
|
185
|
-
*
|
|
751
|
+
* Open the keyspace (if needed) and start syncing. Idempotent.
|
|
752
|
+
* Note: a `CONNECTION_REVOKED` latch is NOT cleared here — reconnecting a
|
|
753
|
+
* revoked app connection requires a fresh consent flow. Call
|
|
754
|
+
* {@link clearRevoked} (or rebind the engine) after re-authorization.
|
|
186
755
|
*/
|
|
187
|
-
|
|
756
|
+
connect(): Promise<void>;
|
|
757
|
+
/** Clear the revocation latch (after the user re-authorized the app). */
|
|
758
|
+
clearRevoked(): void;
|
|
759
|
+
/** @deprecated alias of {@link connect} */
|
|
760
|
+
start(): Promise<void>;
|
|
188
761
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
762
|
+
* Disconnect but keep stores open: local reads/writes keep working and
|
|
763
|
+
* ops queue for the next connect. Used on reauth_required.
|
|
192
764
|
*/
|
|
193
|
-
|
|
765
|
+
pause(): void;
|
|
766
|
+
/** Close the socket and stores; local data is kept. */
|
|
767
|
+
stop(): void;
|
|
194
768
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
769
|
+
* Stop and delete every local database for this project (sign-out).
|
|
770
|
+
* Best-effort discovery of mount keyspaces from previous sessions.
|
|
197
771
|
*/
|
|
198
|
-
|
|
772
|
+
destroyLocal(): Promise<void>;
|
|
199
773
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
774
|
+
* Mount a share: separate keyspace `(project, share)` with its own cursor
|
|
775
|
+
* and pending queue. Bootstraps + subscribes when the socket is online.
|
|
202
776
|
*/
|
|
203
|
-
|
|
777
|
+
mountShare(shareId: string): Promise<SubscriptionState>;
|
|
778
|
+
/** Unsubscribe a mount. Local cache is kept unless `purge` is set. */
|
|
779
|
+
unmountShare(shareId: string, options?: {
|
|
780
|
+
purge?: boolean;
|
|
781
|
+
}): Promise<void>;
|
|
204
782
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
783
|
+
* Queue a local op, apply it optimistically, and push when online.
|
|
784
|
+
* Returns the resulting view record (null when deleted).
|
|
785
|
+
* Throws on local validation failure (fail fast — the server would
|
|
786
|
+
* terminally reject it anyway).
|
|
207
787
|
*/
|
|
208
|
-
|
|
788
|
+
apply(subKey: string, partial: {
|
|
789
|
+
type: OpType;
|
|
790
|
+
table: string;
|
|
791
|
+
record_id: string;
|
|
792
|
+
data?: Record<string, unknown>;
|
|
793
|
+
}): Promise<Record<string, unknown> | null>;
|
|
794
|
+
private handleConnectionStatus;
|
|
795
|
+
private handleWelcome;
|
|
796
|
+
private handleMessage;
|
|
797
|
+
private handleSubscribed;
|
|
798
|
+
private handleError;
|
|
799
|
+
private openSub;
|
|
800
|
+
/** Bootstrap if needed, then bind the stream on the current socket. */
|
|
801
|
+
private activateSub;
|
|
802
|
+
/** Cold start = snapshot + tail; never log replay (§5). Pending survives. */
|
|
803
|
+
private bootstrapSub;
|
|
209
804
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
805
|
+
* Responsibilities 3+4+5: ordered apply, cursor advance, dedupe/confirm.
|
|
806
|
+
* Runs inside the sub's serial chain.
|
|
212
807
|
*/
|
|
213
|
-
|
|
808
|
+
private processOps;
|
|
214
809
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
810
|
+
* Push verdicts (§6.5, §8, §9). Success acks are recorded but the op stays
|
|
811
|
+
* pending until its echo arrives in seq order — this preserves strict
|
|
812
|
+
* ordered apply even when `pushed` races ahead of intermediate remote ops.
|
|
813
|
+
* Terminal errors apply the poison-op rule; retryables back off.
|
|
217
814
|
*/
|
|
815
|
+
private processPushed;
|
|
816
|
+
/** Push unsent pending ops, chunked to `limits.max_ops_per_push`. */
|
|
817
|
+
private flush;
|
|
818
|
+
private get dbPrefix();
|
|
819
|
+
/** Base database name for this keyspace (multi-user: includes the user id). */
|
|
820
|
+
private get baseDbName();
|
|
821
|
+
private enqueue;
|
|
822
|
+
private timer;
|
|
823
|
+
private recomputeStatus;
|
|
824
|
+
private log;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* The table API — the app-facing database surface, shaped around Sync/2 ops.
|
|
829
|
+
*
|
|
830
|
+
* Two implementations of the same interface:
|
|
831
|
+
* - `SyncDb` — offline-first over a SyncEngine subscription (own channel or
|
|
832
|
+
* a mounted share). Writes queue ops and apply optimistically; reads hit
|
|
833
|
+
* the local Dexie view stores (which `useQuery`/liveQuery observe).
|
|
834
|
+
* - `RestDb` — direct REST calls, no local persistence (for server-ish
|
|
835
|
+
* contexts or apps that don't want a local replica).
|
|
836
|
+
*/
|
|
837
|
+
|
|
838
|
+
type BasicRecord = {
|
|
839
|
+
id: string;
|
|
840
|
+
} & Record<string, unknown>;
|
|
841
|
+
interface BasicTable<T extends BasicRecord = BasicRecord> {
|
|
842
|
+
/** Create a record. Sync mode mints the id locally; REST mode server-side. */
|
|
843
|
+
create(data: Omit<T, 'id'>): Promise<T>;
|
|
844
|
+
/** Create or replace the whole record (Sync/2 `put` op). */
|
|
845
|
+
put(id: string, data: Omit<T, 'id'>): Promise<T>;
|
|
846
|
+
/** Shallow-merge top-level fields (`patch` op). Returns null when missing. */
|
|
847
|
+
patch(id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null>;
|
|
848
|
+
/** Delete (tombstone). Idempotent. */
|
|
849
|
+
delete(id: string): Promise<void>;
|
|
850
|
+
get(id: string): Promise<T | null>;
|
|
218
851
|
getAll(): Promise<T[]>;
|
|
852
|
+
find(predicate: (record: T) => boolean): Promise<T[]>;
|
|
853
|
+
/** Sync mode: the Dexie view table (for liveQuery / advanced queries). */
|
|
854
|
+
ref?: Table<T, string>;
|
|
855
|
+
}
|
|
856
|
+
interface BasicDb {
|
|
857
|
+
readonly kind: 'sync' | 'rest';
|
|
858
|
+
table<T extends BasicRecord = BasicRecord>(name: string): BasicTable<T>;
|
|
859
|
+
}
|
|
860
|
+
declare class SyncDb implements BasicDb {
|
|
861
|
+
private readonly engine;
|
|
862
|
+
private readonly subKey;
|
|
863
|
+
readonly kind: "sync";
|
|
864
|
+
private readonly tables;
|
|
865
|
+
constructor(engine: SyncEngine, subKey?: string);
|
|
866
|
+
table<T extends BasicRecord = BasicRecord>(name: string): BasicTable<T>;
|
|
867
|
+
}
|
|
868
|
+
declare class RestDb implements BasicDb {
|
|
869
|
+
private readonly rest;
|
|
870
|
+
private readonly schema?;
|
|
871
|
+
readonly kind: "rest";
|
|
872
|
+
private readonly tables;
|
|
873
|
+
constructor(rest: RestClient, schema?: {
|
|
874
|
+
tables?: Record<string, unknown>;
|
|
875
|
+
} | undefined);
|
|
876
|
+
table<T extends BasicRecord = BasicRecord>(name: string): BasicTable<T>;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* User registry — multiple local users per project (anonymous or signed-in),
|
|
881
|
+
* one active at a time, switchable.
|
|
882
|
+
*
|
|
883
|
+
* - Profiles live in localStorage under `basic_users:{projectId}` and are
|
|
884
|
+
* shared across tabs.
|
|
885
|
+
* - The *active* user id is per-tab (sessionStorage) so different tabs can be
|
|
886
|
+
* on different users; it survives the OAuth redirect, so a sign-in resumes
|
|
887
|
+
* on the profile that initiated it. Falls back to the most recently active
|
|
888
|
+
* profile.
|
|
889
|
+
* - Each profile owns an isolated sync keyspace and a namespaced slice of
|
|
890
|
+
* auth storage (refresh token, cached userinfo, PKCE state, ...) via
|
|
891
|
+
* `PrefixedStorage`.
|
|
892
|
+
* - A pre-multi-user session (0.9.0-beta.0: bare `basic_refresh_token` +
|
|
893
|
+
* `basic-sync:{projectId}` keyspace) is adopted as the first profile with
|
|
894
|
+
* empty prefix/keyspace — no key or IndexedDB renames.
|
|
895
|
+
*/
|
|
896
|
+
|
|
897
|
+
type BasicUserKind = 'anon' | 'account';
|
|
898
|
+
interface BasicUserProfile {
|
|
899
|
+
/** Local profile id (uuid). Stable across sign-in upgrades. */
|
|
900
|
+
id: string;
|
|
901
|
+
kind: BasicUserKind;
|
|
902
|
+
/** Account identity, set once signed in. */
|
|
903
|
+
did?: string | null;
|
|
904
|
+
handle?: string | null;
|
|
905
|
+
email?: string | null;
|
|
906
|
+
name?: string | null;
|
|
907
|
+
picture?: string | null;
|
|
219
908
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* Returns empty array if not authenticated (graceful degradation for read operations)
|
|
909
|
+
* Keyspace segment for sync data. `''` = the legacy pre-multi-user
|
|
910
|
+
* keyspace (`basic-sync:{projectId}`); otherwise db names append it.
|
|
223
911
|
*/
|
|
224
|
-
|
|
912
|
+
keyspace: string;
|
|
225
913
|
/**
|
|
226
|
-
*
|
|
914
|
+
* Prefix for this profile's auth storage keys. `''` = legacy unprefixed
|
|
915
|
+
* keys; otherwise `u:{id}:`.
|
|
227
916
|
*/
|
|
228
|
-
|
|
917
|
+
storagePrefix: string;
|
|
918
|
+
createdAt: number;
|
|
919
|
+
lastActiveAt: number;
|
|
229
920
|
}
|
|
230
|
-
|
|
231
|
-
|
|
921
|
+
/** Namespaces every key of an underlying BasicStorage adapter. */
|
|
922
|
+
declare class PrefixedStorage implements BasicStorage {
|
|
923
|
+
private readonly inner;
|
|
924
|
+
readonly prefix: string;
|
|
925
|
+
constructor(inner: BasicStorage, prefix: string);
|
|
232
926
|
get(key: string): Promise<string | null>;
|
|
233
927
|
set(key: string, value: string): Promise<void>;
|
|
234
928
|
remove(key: string): Promise<void>;
|
|
235
929
|
}
|
|
236
|
-
declare class
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
930
|
+
declare class UserRegistry {
|
|
931
|
+
private readonly storage;
|
|
932
|
+
private readonly projectId;
|
|
933
|
+
constructor(storage: BasicStorage, projectId: string);
|
|
934
|
+
list(): Promise<BasicUserProfile[]>;
|
|
935
|
+
private save;
|
|
936
|
+
get(id: string): Promise<BasicUserProfile | null>;
|
|
937
|
+
createAnon(): Promise<BasicUserProfile>;
|
|
938
|
+
update(id: string, patch: Partial<Omit<BasicUserProfile, 'id' | 'keyspace' | 'storagePrefix'>>): Promise<BasicUserProfile | null>;
|
|
939
|
+
remove(id: string): Promise<void>;
|
|
940
|
+
/** The profile (if any) already bound to an account DID. */
|
|
941
|
+
findByDid(did: string): Promise<BasicUserProfile | null>;
|
|
942
|
+
private getActiveIdRaw;
|
|
943
|
+
setActiveId(id: string): void;
|
|
944
|
+
private clearActiveId;
|
|
945
|
+
/**
|
|
946
|
+
* Resolve the active profile for this tab: sessionStorage choice if it
|
|
947
|
+
* still exists, else the most recently active profile, else null.
|
|
948
|
+
*/
|
|
949
|
+
resolveActive(): Promise<BasicUserProfile | null>;
|
|
950
|
+
touch(id: string): Promise<void>;
|
|
951
|
+
/**
|
|
952
|
+
* Adopt a pre-multi-user session as the first profile. Idempotent: runs
|
|
953
|
+
* only when the registry is empty and a bare refresh token exists. The
|
|
954
|
+
* adopted profile keeps the unprefixed storage keys and the legacy
|
|
955
|
+
* keyspace name, so nothing needs to move.
|
|
956
|
+
*/
|
|
957
|
+
adoptLegacySession(): Promise<BasicUserProfile | null>;
|
|
240
958
|
}
|
|
241
|
-
declare const STORAGE_KEYS: {
|
|
242
|
-
readonly REFRESH_TOKEN: "basic_refresh_token";
|
|
243
|
-
readonly USER_INFO: "basic_user_info";
|
|
244
|
-
readonly AUTH_STATE: "basic_auth_state";
|
|
245
|
-
readonly REDIRECT_URI: "basic_redirect_uri";
|
|
246
|
-
readonly SERVER_URL: "basic_server_url";
|
|
247
|
-
readonly PDS_ENDPOINTS: "basic_pds_endpoints";
|
|
248
|
-
readonly LAST_CONNECT_REPORT: "basic_last_connect_report";
|
|
249
|
-
readonly DEBUG: "basic_debug";
|
|
250
|
-
readonly CODE_VERIFIER: "basic_code_verifier";
|
|
251
|
-
};
|
|
252
959
|
|
|
253
|
-
type User = {
|
|
254
|
-
sub?: string;
|
|
255
|
-
name?: string;
|
|
256
|
-
email?: string;
|
|
257
|
-
picture?: string;
|
|
258
|
-
};
|
|
259
960
|
/**
|
|
260
|
-
*
|
|
961
|
+
* BasicClient — the framework-agnostic SDK core. Owns, per active user:
|
|
962
|
+
* - AuthManager (OAuth/PKCE, tokens, cross-tab session; storage namespaced
|
|
963
|
+
* per local user profile)
|
|
964
|
+
* - SyncEngine (Sync/2 client on the profile's own keyspace)
|
|
965
|
+
* - RestClient (REST v2: snapshot/changes bootstrap, shares, CRUD)
|
|
261
966
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* - `recovering` — A session likely exists (refresh token / cached user) but
|
|
265
|
-
* the SDK hasn't confirmed it yet (e.g. offline, mid-refresh).
|
|
266
|
-
* - `reauth_required` — The session is definitively invalid (revoked, expired
|
|
267
|
-
* refresh token, etc.). The user must sign in again.
|
|
268
|
-
* NOTE: `isSignedIn` remains `true` in this state so the UI
|
|
269
|
-
* can display user info while prompting re-authentication.
|
|
270
|
-
* Use `authStatus === 'reauth_required'` to distinguish
|
|
271
|
-
* this from a healthy signed-in state.
|
|
272
|
-
* - `signed_out` — No session. User is not authenticated.
|
|
967
|
+
* plus the multi-user registry: several local users (anonymous or signed-in)
|
|
968
|
+
* exist side by side, one active at a time per tab, switchable.
|
|
273
969
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
970
|
+
* Local-first rules:
|
|
971
|
+
* - The local keyspace opens with no token and no network (anonymous mode is
|
|
972
|
+
* on by default; offline cold start works).
|
|
973
|
+
* - Anonymous writes are pending ops; sign-in upgrades the profile in place
|
|
974
|
+
* and the normal bootstrap+flush merges them into the account.
|
|
975
|
+
* - `reauth_required` pauses the connection but keeps local reads/writes.
|
|
976
|
+
* - Sign-out wipes the profile's local data and falls through to the next
|
|
977
|
+
* (or a fresh anonymous) user. No page reloads.
|
|
277
978
|
*/
|
|
278
|
-
type AuthStatus = 'bootstrapping' | 'authenticated' | 'recovering' | 'reauth_required' | 'signed_out';
|
|
279
|
-
type AuthResult = {
|
|
280
|
-
success: boolean;
|
|
281
|
-
error?: string;
|
|
282
|
-
code?: string;
|
|
283
|
-
};
|
|
284
|
-
type GetTokenOptions = {
|
|
285
|
-
forceRefresh?: boolean;
|
|
286
|
-
};
|
|
287
979
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
/** Sync
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
980
|
+
type BasicMode = 'sync' | 'rest';
|
|
981
|
+
interface BasicAuthConfig {
|
|
982
|
+
scopes?: string | string[];
|
|
983
|
+
/** PDS URL for auth, data, and sync (default https://pds.basic.id) */
|
|
984
|
+
pds_url?: string;
|
|
985
|
+
/** Admin server URL for connect reporting + schema status (default https://api.basic.tech) */
|
|
986
|
+
admin_url?: string;
|
|
987
|
+
/** Sync WebSocket URL. Default: pds_url with ws(s) scheme + `/sync/`. */
|
|
988
|
+
sync_url?: string;
|
|
989
|
+
}
|
|
990
|
+
interface BasicClientConfig {
|
|
991
|
+
/** The Basic schema document (`{ project_id, version, tables }`). */
|
|
992
|
+
schema?: Record<string, unknown> & {
|
|
993
|
+
project_id?: string;
|
|
994
|
+
version?: number;
|
|
995
|
+
tables?: Record<string, unknown>;
|
|
996
|
+
};
|
|
997
|
+
/** Project id override; normally taken from `schema.project_id`. */
|
|
998
|
+
project_id?: string;
|
|
999
|
+
auth?: BasicAuthConfig;
|
|
1000
|
+
storage?: BasicStorage;
|
|
1001
|
+
debug?: boolean;
|
|
1002
|
+
/** 'sync' (offline-first local replica, default) or 'rest' (direct API). */
|
|
1003
|
+
mode?: BasicMode;
|
|
1004
|
+
/**
|
|
1005
|
+
* Anonymous/local-first usage (sync mode only, default true): the local db
|
|
1006
|
+
* works without a session; data merges into the account on sign-in.
|
|
1007
|
+
*/
|
|
1008
|
+
anonymous?: boolean;
|
|
1009
|
+
/** Node/testing: pass the `ws` constructor. */
|
|
1010
|
+
WebSocketImpl?: typeof WebSocket;
|
|
1011
|
+
}
|
|
1012
|
+
/** Local schema vs server status (dev toolbar and debugging). */
|
|
1013
|
+
interface BasicSchemaDevInfo {
|
|
309
1014
|
projectId: string | null;
|
|
310
1015
|
localVersion: number | undefined;
|
|
311
1016
|
status: string;
|
|
312
1017
|
valid: boolean;
|
|
313
1018
|
lastCheckedAt: number;
|
|
314
1019
|
error?: string;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
*/
|
|
319
|
-
type BasicContextType = {
|
|
1020
|
+
}
|
|
1021
|
+
interface BasicClientSnapshot {
|
|
1022
|
+
/** Auth bootstrap finished and (in sync mode) the local db is decided. */
|
|
320
1023
|
isReady: boolean;
|
|
321
1024
|
isSignedIn: boolean;
|
|
322
1025
|
authStatus: AuthStatus;
|
|
323
1026
|
authErrorCode: string | null;
|
|
324
1027
|
user: User | null;
|
|
325
1028
|
did: string | null;
|
|
1029
|
+
/** Space-separated scopes granted in the current access token. */
|
|
326
1030
|
scope: string | null;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
signOut: () => Promise<void>;
|
|
332
|
-
signInWithCode: (code: string, state?: string) => Promise<AuthResult>;
|
|
333
|
-
getToken: (options?: GetTokenOptions) => Promise<string>;
|
|
334
|
-
getSignInUrl: (redirectUri?: string) => Promise<string>;
|
|
335
|
-
db: BasicDB;
|
|
336
|
-
dbStatus: DBStatus;
|
|
337
|
-
dbMode: DBMode;
|
|
338
|
-
/** Local schema vs server status; null if no schema on the provider. */
|
|
1031
|
+
syncStatus: SyncStatus;
|
|
1032
|
+
pendingCount: number;
|
|
1033
|
+
/** True when the schema allows sync (valid + published + matching server). */
|
|
1034
|
+
syncEnabled: boolean;
|
|
339
1035
|
devInfo: BasicSchemaDevInfo | null;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1036
|
+
mode: BasicMode;
|
|
1037
|
+
/** All local user profiles (anonymous and signed-in). */
|
|
1038
|
+
users: BasicUserProfile[];
|
|
1039
|
+
/** The profile active in this tab. */
|
|
1040
|
+
activeUser: BasicUserProfile | null;
|
|
1041
|
+
/** Active profile has no account yet (local-only workspace). */
|
|
1042
|
+
isAnonymous: boolean;
|
|
1043
|
+
}
|
|
1044
|
+
interface ShareMountHandle {
|
|
1045
|
+
shareId: string;
|
|
1046
|
+
db: BasicDb;
|
|
1047
|
+
}
|
|
1048
|
+
declare class BasicClient {
|
|
1049
|
+
readonly rest: RestClient;
|
|
1050
|
+
readonly mode: BasicMode;
|
|
1051
|
+
readonly config: BasicClientConfig;
|
|
1052
|
+
readonly projectId: string | undefined;
|
|
1053
|
+
readonly users: UserRegistry | null;
|
|
1054
|
+
private readonly rawStorage;
|
|
1055
|
+
private readonly restDb;
|
|
1056
|
+
private readonly debug;
|
|
1057
|
+
private readonly anonymousEnabled;
|
|
1058
|
+
private readonly authConfig;
|
|
1059
|
+
private readonly syncUrl;
|
|
1060
|
+
private binding;
|
|
1061
|
+
private usersCache;
|
|
1062
|
+
private devInfo;
|
|
1063
|
+
private syncEnabled;
|
|
1064
|
+
private schemaChecked;
|
|
1065
|
+
private started;
|
|
1066
|
+
private signOutInProgress;
|
|
1067
|
+
/** Serializes profile transitions (switch, dispose, sign-out fallthrough). */
|
|
1068
|
+
private profileOps;
|
|
1069
|
+
private mounts;
|
|
1070
|
+
private listeners;
|
|
1071
|
+
private snapshot;
|
|
1072
|
+
constructor(config: BasicClientConfig);
|
|
1073
|
+
get auth(): AuthManager;
|
|
1074
|
+
get engine(): SyncEngine | null;
|
|
1075
|
+
/** The database handle for the active user. Identity changes on switch. */
|
|
1076
|
+
get db(): BasicDb;
|
|
1077
|
+
get activeUser(): BasicUserProfile | null;
|
|
1078
|
+
/** Bootstrap: version migrations, profile resolution, schema check, auth init. */
|
|
1079
|
+
start(): Promise<void>;
|
|
1080
|
+
/**
|
|
1081
|
+
* Sign out the active user: server-side revoke, wipe the profile's local
|
|
1082
|
+
* data, drop the profile, and fall through to the next (or a fresh
|
|
1083
|
+
* anonymous) user.
|
|
1084
|
+
*/
|
|
1085
|
+
signOut(): Promise<void>;
|
|
1086
|
+
/** Switch this tab to another local user. */
|
|
1087
|
+
switchUser(id: string): Promise<void>;
|
|
1088
|
+
/** Create a fresh anonymous user and switch to it. */
|
|
1089
|
+
addUser(): Promise<BasicUserProfile>;
|
|
1090
|
+
/**
|
|
1091
|
+
* Remove a local user: best-effort server-side revoke, wipe its keyspace
|
|
1092
|
+
* and auth storage, drop the profile. Removing the active user signs out.
|
|
1093
|
+
*/
|
|
1094
|
+
removeUser(id: string): Promise<void>;
|
|
1095
|
+
/** Stop connections and listeners; local data is kept. */
|
|
1096
|
+
stop(): void;
|
|
1097
|
+
/** Re-run the remote schema status check (dev toolbar). */
|
|
1098
|
+
refreshSchemaStatus(): Promise<void>;
|
|
1099
|
+
listRejected(): Promise<RejectedRow[]>;
|
|
1100
|
+
clearRejected(): Promise<void>;
|
|
1101
|
+
/** Shares granted by / received by this user for this app. */
|
|
1102
|
+
listShares(): Promise<{
|
|
1103
|
+
granted: Share[];
|
|
1104
|
+
received: Share[];
|
|
1105
|
+
}>;
|
|
1106
|
+
/** Mount a share: separate local keyspace + subscription. */
|
|
1107
|
+
mountShare(shareId: string): Promise<ShareMountHandle>;
|
|
1108
|
+
unmountShare(shareId: string, options?: {
|
|
1109
|
+
purge?: boolean;
|
|
1110
|
+
}): Promise<void>;
|
|
1111
|
+
getMountedShare(shareId: string): ShareMountHandle | undefined;
|
|
1112
|
+
subscribe: (listener: () => void) => (() => void);
|
|
1113
|
+
getSnapshot: () => BasicClientSnapshot;
|
|
1114
|
+
private createBinding;
|
|
362
1115
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
1116
|
+
* Bind and boot a profile. Publishes the new binding first so React
|
|
1117
|
+
* subscriptions re-attach to the new db, then tears the old binding down
|
|
1118
|
+
* on the next tick (avoids in-flight live queries hitting a closed store).
|
|
365
1119
|
*/
|
|
1120
|
+
private activateProfile;
|
|
1121
|
+
private bindingMatches;
|
|
1122
|
+
/** After sign-out/disposal: resume on the next profile or a fresh anon one. */
|
|
1123
|
+
private activateNextProfileLocked;
|
|
1124
|
+
/** Wipe a (non-active) profile's local footprint: keyspace dbs + auth keys. */
|
|
1125
|
+
private disposeProfileData;
|
|
1126
|
+
private deleteKeyspaceDatabases;
|
|
1127
|
+
/** Previous auth status, for transition detection (revoked-latch clearing). */
|
|
1128
|
+
private lastAuthStatus;
|
|
1129
|
+
private handleAuthChange;
|
|
1130
|
+
/**
|
|
1131
|
+
* Drive the engine from auth + schema state:
|
|
1132
|
+
* - local keyspace opens with no token (anonymous mode / offline cold start)
|
|
1133
|
+
* - connect when a session exists (recovering counts — the connection
|
|
1134
|
+
* retries token acquisition itself)
|
|
1135
|
+
* - reauth_required pauses the connection, keeps local data usable
|
|
1136
|
+
*/
|
|
1137
|
+
private syncLifecycle;
|
|
1138
|
+
/**
|
|
1139
|
+
* After sign-in: bind the account identity to the active profile
|
|
1140
|
+
* (anonymous → account upgrade) and dedupe against an existing profile
|
|
1141
|
+
* for the same DID.
|
|
1142
|
+
*/
|
|
1143
|
+
private maybeUpgradeProfile;
|
|
1144
|
+
private refreshUsers;
|
|
1145
|
+
private queueProfileOp;
|
|
1146
|
+
private checkSchema;
|
|
1147
|
+
private buildSnapshot;
|
|
1148
|
+
private publish;
|
|
1149
|
+
}
|
|
1150
|
+
declare function createBasicClient(config: BasicClientConfig): BasicClient;
|
|
1151
|
+
|
|
1152
|
+
interface BasicProviderProps {
|
|
1153
|
+
children: React.ReactNode;
|
|
1154
|
+
/** The Basic schema object containing project_id and table definitions. */
|
|
1155
|
+
schema?: Record<string, unknown>;
|
|
1156
|
+
/** Project id override; normally taken from `schema.project_id`. */
|
|
366
1157
|
project_id?: string;
|
|
367
|
-
|
|
368
|
-
schema?: any;
|
|
369
|
-
debug?: boolean;
|
|
1158
|
+
auth?: BasicAuthConfig;
|
|
370
1159
|
storage?: BasicStorage;
|
|
371
|
-
|
|
1160
|
+
debug?: boolean;
|
|
1161
|
+
/**
|
|
1162
|
+
* - 'sync' (default): offline-first local replica synced over Sync/2
|
|
1163
|
+
* - 'rest': direct REST calls, no local persistence
|
|
1164
|
+
*/
|
|
1165
|
+
mode?: BasicMode;
|
|
372
1166
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
1167
|
+
* Anonymous/local-first usage (sync mode only, default true): the db works
|
|
1168
|
+
* without a session and local data merges into the account on sign-in.
|
|
1169
|
+
* Set false to require sign-in before any local data exists.
|
|
376
1170
|
*/
|
|
377
|
-
|
|
378
|
-
/** Show floating dev toolbar (localhost
|
|
1171
|
+
anonymous?: boolean;
|
|
1172
|
+
/** Show the floating dev toolbar (localhost / NODE_ENV=development / debug). */
|
|
379
1173
|
devToolbar?: boolean;
|
|
380
|
-
|
|
381
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* Render children before auth/db are ready (default false: children render
|
|
1176
|
+
* once the client finished bootstrapping, like previous versions).
|
|
1177
|
+
*/
|
|
1178
|
+
renderWhileLoading?: boolean;
|
|
1179
|
+
}
|
|
1180
|
+
declare function BasicProvider({ children, schema, project_id, auth, storage, debug, mode, anonymous, devToolbar, renderWhileLoading, }: BasicProviderProps): React.JSX.Element;
|
|
382
1181
|
|
|
383
|
-
type BasicDevToolbarProps = {
|
|
384
|
-
/** When false, toolbar does not render. Defaults to true when used standalone. */
|
|
385
|
-
enabled?: boolean;
|
|
386
|
-
/** Same as BasicProvider `debug` — when true, toolbar shows even off localhost. */
|
|
387
|
-
debug?: boolean;
|
|
388
|
-
};
|
|
389
1182
|
/**
|
|
390
|
-
*
|
|
1183
|
+
* Reactive live queries against the local database (sync mode), built on
|
|
1184
|
+
* dexie-react-hooks' `useLiveQuery`. Any read through `db.table(...)` (or
|
|
1185
|
+
* its `ref` Dexie table) is observable.
|
|
1186
|
+
*
|
|
1187
|
+
* ```tsx
|
|
1188
|
+
* const todos = useQuery(() => db.table('todos').getAll())
|
|
1189
|
+
* ```
|
|
1190
|
+
*
|
|
1191
|
+
* User-aware: the active user's id is appended to `deps` automatically, so
|
|
1192
|
+
* queries re-subscribe against the new user's database on `switchUser` /
|
|
1193
|
+
* sign-in / sign-out — provided the query reads `db` from the current render
|
|
1194
|
+
* (e.g. from `useBasic()`/`useDb()`). For closures created outside render,
|
|
1195
|
+
* pass `[db]` in deps explicitly.
|
|
391
1196
|
*/
|
|
392
|
-
declare function
|
|
1197
|
+
declare function useQuery<T>(querier: () => T | Promise<T | undefined> | undefined, deps?: unknown[]): T | undefined;
|
|
1198
|
+
/** The BasicClient instance from the nearest provider. */
|
|
1199
|
+
declare function useBasicClient(): BasicClient;
|
|
1200
|
+
interface UseAuthResult {
|
|
1201
|
+
/** Auth bootstrap finished (a session may or may not exist). */
|
|
1202
|
+
isReady: boolean;
|
|
1203
|
+
isSignedIn: boolean;
|
|
1204
|
+
/** Active user is a local-only (anonymous) workspace with no account. */
|
|
1205
|
+
isAnonymous: boolean;
|
|
1206
|
+
status: AuthStatus;
|
|
1207
|
+
errorCode: string | null;
|
|
1208
|
+
user: User | null;
|
|
1209
|
+
did: string | null;
|
|
1210
|
+
/** Space-separated scopes granted in the current access token. */
|
|
1211
|
+
scope: string | null;
|
|
1212
|
+
hasScope: (scope: string) => boolean;
|
|
1213
|
+
missingScopes: () => string[];
|
|
1214
|
+
signIn: (redirectUri?: string) => Promise<void>;
|
|
1215
|
+
signInWithHandle: (handle: string) => Promise<void>;
|
|
1216
|
+
signInWithCode: (code: string, state?: string) => Promise<AuthResult>;
|
|
1217
|
+
signOut: () => Promise<void>;
|
|
1218
|
+
getToken: (options?: GetTokenOptions$1) => Promise<string>;
|
|
1219
|
+
getSignInUrl: (redirectUri?: string) => Promise<string>;
|
|
1220
|
+
}
|
|
1221
|
+
/** Auth state + actions. */
|
|
1222
|
+
declare function useAuth(): UseAuthResult;
|
|
1223
|
+
/** The database handle (own channel). Offline-first in sync mode. */
|
|
1224
|
+
declare function useDb(): BasicDb;
|
|
1225
|
+
interface UseSyncStatusResult {
|
|
1226
|
+
/** Engine status: idle | connecting | online | offline | auth_required | revoked | stopped */
|
|
1227
|
+
status: SyncStatus;
|
|
1228
|
+
/** True when the schema is valid + published and sync can run. */
|
|
1229
|
+
enabled: boolean;
|
|
1230
|
+
/** Locally queued ops not yet confirmed by the server. */
|
|
1231
|
+
pendingCount: number;
|
|
1232
|
+
/** Terminally rejected ops (poison-op rule — never re-pushed). */
|
|
1233
|
+
listRejected: () => Promise<RejectedRow[]>;
|
|
1234
|
+
clearRejected: () => Promise<void>;
|
|
1235
|
+
}
|
|
1236
|
+
/** Sync engine state: connection status, pending queue depth, rejected ops. */
|
|
1237
|
+
declare function useSyncStatus(): UseSyncStatusResult;
|
|
1238
|
+
interface UseSharesResult {
|
|
1239
|
+
/** Shares this user granted to others (for this app). */
|
|
1240
|
+
granted: Share[];
|
|
1241
|
+
/** Shares others granted to this user (mountable). */
|
|
1242
|
+
received: Share[];
|
|
1243
|
+
isLoading: boolean;
|
|
1244
|
+
error: Error | null;
|
|
1245
|
+
refresh: () => Promise<void>;
|
|
1246
|
+
}
|
|
1247
|
+
/** List shares granted by / received by the signed-in user for this app. */
|
|
1248
|
+
declare function useShares(): UseSharesResult;
|
|
1249
|
+
interface UseShareResult {
|
|
1250
|
+
/** Table API over the mounted share's keyspace; null until mounted. */
|
|
1251
|
+
db: BasicDb | null;
|
|
1252
|
+
status: 'mounting' | 'mounted' | 'error' | 'revoked';
|
|
1253
|
+
error: Error | null;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Mount a share (data another user granted to you) and get a db handle for
|
|
1257
|
+
* it. The mount lives in its own local keyspace with its own cursor and
|
|
1258
|
+
* pending queue — never merged with your own data.
|
|
1259
|
+
*/
|
|
1260
|
+
declare function useShare(shareId: string | null | undefined): UseShareResult;
|
|
1261
|
+
interface UseUsersResult {
|
|
1262
|
+
/** All local user profiles (anonymous and signed-in), shared across tabs. */
|
|
1263
|
+
users: BasicUserProfile[];
|
|
1264
|
+
/** The profile active in this tab. */
|
|
1265
|
+
activeUser: BasicUserProfile | null;
|
|
1266
|
+
isAnonymous: boolean;
|
|
1267
|
+
/** Switch this tab to another local user. */
|
|
1268
|
+
switchUser: (id: string) => Promise<void>;
|
|
1269
|
+
/** Create a fresh anonymous user and switch to it. */
|
|
1270
|
+
addUser: () => Promise<BasicUserProfile>;
|
|
1271
|
+
/** Remove a local user (wipes its local data; active user = sign out). */
|
|
1272
|
+
removeUser: (id: string) => Promise<void>;
|
|
1273
|
+
}
|
|
1274
|
+
/** Multiple local users (anonymous or signed-in) with per-tab switching. */
|
|
1275
|
+
declare function useUsers(): UseUsersResult;
|
|
1276
|
+
interface UseBasicResult extends UseAuthResult {
|
|
1277
|
+
db: BasicDb;
|
|
1278
|
+
sync: UseSyncStatusResult;
|
|
1279
|
+
/** All local user profiles. */
|
|
1280
|
+
users: BasicUserProfile[];
|
|
1281
|
+
/** The profile active in this tab. */
|
|
1282
|
+
activeUser: BasicUserProfile | null;
|
|
1283
|
+
/** Local schema vs server status; null if no schema on the provider. */
|
|
1284
|
+
devInfo: BasicClientSnapshot['devInfo'];
|
|
1285
|
+
refreshSchemaStatus: () => Promise<void>;
|
|
1286
|
+
client: BasicClient;
|
|
1287
|
+
}
|
|
1288
|
+
/** Umbrella hook: auth + db + sync status + users. */
|
|
1289
|
+
declare function useBasic(): UseBasicResult;
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* SyncConnection — one multiplexed WebSocket to `wss://<pds>/sync/`
|
|
1293
|
+
* (SYNC_V2.md §6.9: one socket, many streams).
|
|
1294
|
+
*
|
|
1295
|
+
* Owns the transport only: hello handshake, heartbeat ping, exponential
|
|
1296
|
+
* backoff reconnect, and token refresh on auth rejection. Everything above
|
|
1297
|
+
* the socket (subscriptions, cursors, pending queues) belongs to SyncEngine,
|
|
1298
|
+
* which receives every parsed server message via `onMessage`.
|
|
1299
|
+
*/
|
|
1300
|
+
|
|
1301
|
+
type ConnectionStatus = 'idle' | 'connecting' | 'online' | 'offline' | 'auth_failed' | 'stopped';
|
|
1302
|
+
interface SyncConnectionOptions {
|
|
1303
|
+
/** e.g. `wss://pds.basic.id/sync/` */
|
|
1304
|
+
wsUrl: string;
|
|
1305
|
+
getToken: (options?: {
|
|
1306
|
+
forceRefresh?: boolean;
|
|
1307
|
+
}) => Promise<string>;
|
|
1308
|
+
/** Pass the `ws` constructor in Node; defaults to global WebSocket. */
|
|
1309
|
+
WebSocketImpl?: typeof WebSocket;
|
|
1310
|
+
/** Heartbeat interval (default 30s per spec). */
|
|
1311
|
+
heartbeatMs?: number;
|
|
1312
|
+
onWelcome: (msg: WelcomeMsg) => void;
|
|
1313
|
+
onMessage: (msg: ServerMsg) => void;
|
|
1314
|
+
onStatus: (status: ConnectionStatus) => void;
|
|
1315
|
+
log?: (...args: unknown[]) => void;
|
|
1316
|
+
}
|
|
1317
|
+
declare class SyncConnection {
|
|
1318
|
+
private readonly opts;
|
|
1319
|
+
private readonly WS;
|
|
1320
|
+
private readonly heartbeatMs;
|
|
1321
|
+
private ws;
|
|
1322
|
+
private _status;
|
|
1323
|
+
private stopped;
|
|
1324
|
+
private reconnectDelay;
|
|
1325
|
+
private timers;
|
|
1326
|
+
private heartbeatTimer;
|
|
1327
|
+
/** One forced-refresh reconnect attempt per auth rejection. */
|
|
1328
|
+
private authRetryUsed;
|
|
1329
|
+
private removeOnlineListener;
|
|
1330
|
+
constructor(opts: SyncConnectionOptions);
|
|
1331
|
+
get status(): ConnectionStatus;
|
|
1332
|
+
get isOnline(): boolean;
|
|
1333
|
+
start(): void;
|
|
1334
|
+
stop(): void;
|
|
1335
|
+
/** Send a message; returns false when the socket is not open. */
|
|
1336
|
+
send(msg: ClientMsg): boolean;
|
|
1337
|
+
/** Refresh auth on the live socket (no reconnect). */
|
|
1338
|
+
sendToken(): Promise<void>;
|
|
1339
|
+
/**
|
|
1340
|
+
* The server rejected our token (`UNAUTHORIZED` / `TOKEN_EXPIRED` + close).
|
|
1341
|
+
* Retry once with a force-refreshed token; give up (status `auth_failed`)
|
|
1342
|
+
* when the refresh itself fails or a fresh token is rejected again.
|
|
1343
|
+
*/
|
|
1344
|
+
handleAuthRejection(): void;
|
|
1345
|
+
private open;
|
|
1346
|
+
private handleMessage;
|
|
1347
|
+
private startHeartbeat;
|
|
1348
|
+
private stopHeartbeat;
|
|
1349
|
+
private scheduleReconnect;
|
|
1350
|
+
private listenForNetwork;
|
|
1351
|
+
private clearTimers;
|
|
1352
|
+
private setStatus;
|
|
1353
|
+
private log;
|
|
1354
|
+
}
|
|
393
1355
|
|
|
394
1356
|
type ResolvedDid = {
|
|
395
1357
|
did: string;
|
|
@@ -418,4 +1380,15 @@ declare function resolveDid(did: string): Promise<ResolvedDid>;
|
|
|
418
1380
|
*/
|
|
419
1381
|
declare function resolveHandle(handle: string): Promise<ResolvedDid>;
|
|
420
1382
|
|
|
421
|
-
|
|
1383
|
+
type BasicDevToolbarProps = {
|
|
1384
|
+
/** When false, toolbar does not render. Defaults to true when used standalone. */
|
|
1385
|
+
enabled?: boolean;
|
|
1386
|
+
/** Same as BasicProvider `debug` — when true, toolbar shows even off localhost. */
|
|
1387
|
+
debug?: boolean;
|
|
1388
|
+
};
|
|
1389
|
+
/**
|
|
1390
|
+
* Floating dev-only toolbar: auth, DB/sync, and schema status. Requires `BasicProvider` with `debug` or localhost / NODE_ENV=development for visibility unless `enabled` is forced.
|
|
1391
|
+
*/
|
|
1392
|
+
declare function BasicDevToolbar({ enabled, debug }: BasicDevToolbarProps): React.JSX.Element | null;
|
|
1393
|
+
|
|
1394
|
+
export { AuthManager, type AuthResult, type AuthStatus, type BasicAuthConfig, BasicClient, type BasicClientConfig, type BasicClientSnapshot, type BasicDb, BasicDevToolbar, type BasicDevToolbarProps, type BasicMode, BasicProvider, type BasicProviderProps, type BasicRecord, type BasicSchemaDevInfo, type BasicStorage, type BasicTable, type BasicUserKind, type BasicUserProfile, type ChangesPage, type ConnectionStatus, DEFAULT_LIMITS, type GetTokenOptions$1 as GetTokenOptions, LocalStorageAdapter, type LoggedOp, NotAuthenticatedError, OWN_SUB, type OpEnvelope, type OpType, PROTOCOL_VERSION, type PdsEndpoints, type PendingRow, PrefixedStorage, type PushResult, type RejectedRow, type ResolvedDid, RestClient, type RestClientOptions, RestDb, RestError, type RestRecord, STORAGE_KEYS, type Share, type ShareMountHandle, type SharePermission, type ShareSelector, type Snapshot, type SubscriptionState, SyncConnection, SyncDb, SyncEngine, type SyncEngineEvents, type SyncEngineOptions, type SyncErrorCode, type SyncLimits, type SyncStatus, SyncStore, type SyncStoreSchema, type Token, type UseAuthResult, type UseBasicResult, type UseShareResult, type UseSharesResult, type UseSyncStatusResult, type UseUsersResult, type User, UserRegistry, applyOpToData, createBasicClient, isAuthError, isRebootstrapError, isRevocationError, isTerminalOpError, mintOpId, resolveDid, resolveDidWebUrl, resolveHandle, shareSubKey, useAuth, useBasic, useBasicClient, useDb, useQuery, useShare, useShares, useSyncStatus, useUsers };
|