@zuzjs/flare 0.2.1 → 0.2.4

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/dist/index.d.ts CHANGED
@@ -1,80 +1,251 @@
1
- export { AuthGuard, AuthToken, Dropbox, Google, NormalizedProfile, OAuthProvider, ProviderId } from '@zuzjs/auth';
1
+ import { AuthToken, AuthGuard, ProviderId } from '@zuzjs/auth';
2
+ export { Anonymous, Apple, AuthGuard, AuthToken, CreateUserWithEmailAndPasswordInput, Credentials, Dropbox, Facebook, GitHub, Google, NormalizedProfile, OAuthProvider, ProviderId, Providers, SignInAnonymouslyInput, SignInWithEmailAndPasswordInput, Twitter, setupProvider } from '@zuzjs/auth';
2
3
 
3
4
  /**
4
5
  * Client Configuration
5
6
  */
6
7
  interface FlareConfig {
7
- /** WebSocket endpoint URL (e.g., 'http://localhost:5050' or 'wss://api.example.com') */
8
8
  endpoint: string;
9
- /** Application/Project ID */
10
- appId: string;
11
- /** API Key for authentication (optional) */
12
- apiKey?: string;
13
9
  /**
14
- * RSA public key (PEM) returned by `flare app create`.
15
- * When provided, all outgoing messages are wrapped in an RSA-OAEP encrypted
16
- * envelope: `{ enc: "rsa", data: "<base64>" }` before being sent over the wire.
10
+ * Optional HTTP base URL for auth API calls.
11
+ * When set, all auth HTTP calls go through this base instead of calling
12
+ * Flare directly. Use this to route calls through a Next.js proxy so CSRF
13
+ * is handled entirely server-side.
14
+ * Example: '/api/flare' (relative, browser resolves against current origin)
17
15
  */
16
+ httpBase?: string;
17
+ appId: string;
18
+ apiKey?: string;
18
19
  publicKey?: string;
19
- /** Enable automatic reconnection (default: true) */
20
20
  autoReconnect?: boolean;
21
- /** Initial reconnect delay in seconds (default: 2) */
22
21
  reconnectDelay?: number;
23
- /** Maximum reconnect delay in seconds (default: 60) */
24
22
  maxReconnectDelay?: number;
25
- /** Enable debug logging (default: false) */
26
23
  debug?: boolean;
27
- /** Connection timeout in milliseconds (default: 10000) */
24
+ requestTiming?: boolean;
28
25
  connectionTimeout?: number;
29
26
  }
30
- /**
31
- * Query configuration (NEW v0.2: Object-based query syntax)
32
- */
27
+ type FlareAuthProviderId = "credentials" | "anonymous" | "google" | "facebook" | "github" | "dropbox" | "apple" | "twitter";
28
+ interface FlareAuthProviderPublicConfig {
29
+ enabled: boolean;
30
+ clientId?: string;
31
+ scopes?: string[];
32
+ }
33
+ interface FlareAuthConfig {
34
+ appId: string;
35
+ enabled: boolean;
36
+ needsEmailVerification?: boolean;
37
+ autoSendVerificationEmail?: boolean;
38
+ redirectUri?: string;
39
+ csrfToken?: string;
40
+ cookie?: {
41
+ accessTokenName?: string;
42
+ refreshTokenName?: string;
43
+ csrfTokenName?: string;
44
+ domain?: string;
45
+ path?: string;
46
+ secure?: boolean;
47
+ sameSite?: "Lax" | "Strict" | "None";
48
+ accessTokenMaxAge?: number;
49
+ refreshTokenMaxAge?: number;
50
+ csrfTokenMaxAge?: number;
51
+ };
52
+ providers: Record<FlareAuthProviderId, FlareAuthProviderPublicConfig>;
53
+ }
54
+ interface FlareAuthSession {
55
+ uid: string;
56
+ accessToken: string;
57
+ refreshToken: string | null;
58
+ provider?: string;
59
+ email?: string | null;
60
+ emailVerified?: boolean;
61
+ }
62
+ interface FlareAuthUser {
63
+ uid: string;
64
+ email: string;
65
+ email_verified: string;
66
+ [x: string]: any;
67
+ }
68
+ type AuthStateListener = (session: FlareAuthSession & FlareAuthUser | null) => void;
69
+ type AuthConfigListener = (conf: FlareAuthConfig) => void;
70
+ interface SubscribeOptions {
71
+ skipSnapshot?: boolean;
72
+ }
73
+ type QueryOperator = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "array-contains" | "array-contains-any" | "like" | "not-like" | "contains" | "exists" | "not-exists";
33
74
  interface QueryConfig {
34
75
  field: string;
35
76
  op: QueryOperator;
36
77
  value: unknown;
37
78
  }
38
- /**
39
- * Where condition (NEW v0.2: Object-based syntax)
40
- * Example: { age: ">= 25", role: "admin" }
41
- */
79
+ /** OR group */
80
+ interface OrFilter {
81
+ or: QueryConfig[];
82
+ }
83
+ type AnyFilter = QueryConfig | OrFilter;
42
84
  type WhereCondition = Record<string, string | number | boolean | any[]>;
43
- type QueryOperator = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "array-contains";
85
+ interface OrderByClause {
86
+ field: string;
87
+ dir?: "asc" | "desc";
88
+ }
89
+ interface GroupByClause {
90
+ fields: string[];
91
+ }
92
+ interface HavingClause {
93
+ field: string;
94
+ op: "==" | "!=" | "<" | "<=" | ">" | ">=";
95
+ value: number;
96
+ }
97
+ interface CursorValue {
98
+ values: unknown[];
99
+ }
100
+ type AggregateFunction = "count" | "sum" | "avg" | "min" | "max" | "distinct";
101
+ interface AggregateSpec {
102
+ fn: AggregateFunction;
103
+ field?: string;
104
+ alias?: string;
105
+ }
44
106
  /**
45
- * Subscription callback
107
+ * Join definition used by CollectionReference.Join().
108
+ *
109
+ * Example:
110
+ * Join("tasks", { source: "id", target: "boardId", as: "tasks" })
46
111
  */
47
- type SubscriptionCallback<T = any> = (data: SubscriptionData<T>) => void;
112
+ interface JoinQueryPattern {
113
+ where?: AnyFilter[];
114
+ orderBy?: OrderByClause[];
115
+ limit?: number;
116
+ offset?: number;
117
+ startAt?: CursorValue;
118
+ startAfter?: CursorValue;
119
+ endAt?: CursorValue;
120
+ endBefore?: CursorValue;
121
+ aggregate?: AggregateSpec[];
122
+ groupBy?: GroupByClause;
123
+ having?: HavingClause[];
124
+ vectorSearch?: VectorSearchClause;
125
+ select?: string[];
126
+ distinctField?: string;
127
+ }
128
+ interface NestedJoinClause extends JoinQueryPattern {
129
+ /** Joined collection name for this nested join. */
130
+ collection: string;
131
+ /** Field from the parent join result. */
132
+ source: string;
133
+ /** Field from this nested collection to match source. */
134
+ target: string;
135
+ /** Alias where nested rows are attached in each parent join row. */
136
+ as: string;
137
+ /** If true, expect at most one joined row. */
138
+ single?: boolean;
139
+ /** Recursive nested joins. */
140
+ joins?: NestedJoinClause[];
141
+ }
142
+ interface JoinClause extends JoinQueryPattern {
143
+ /** Field from the base collection (the collection you started the query on). */
144
+ source: string;
145
+ /** Field from the joined collection that should match source. */
146
+ target: string;
147
+ /** Alias where joined rows will be attached in each result object. */
148
+ as: string;
149
+ /** If true, expect at most one joined row (object instead of array on server side). */
150
+ single?: boolean;
151
+ /** Optional nested joins under this join. */
152
+ joins?: NestedJoinClause[];
153
+ }
154
+ /** Internal wire-ready join shape sent to server query engine. */
155
+ interface StructuredJoinClause extends JoinQueryPattern {
156
+ from: string;
157
+ localField: string;
158
+ foreignField: string;
159
+ as: string;
160
+ single?: boolean;
161
+ joins?: StructuredJoinClause[];
162
+ }
163
+ interface VectorSearchClause {
164
+ field: string;
165
+ vector: number[];
166
+ k: number;
167
+ metric?: "cosine" | "euclidean" | "dotProduct";
168
+ minScore?: number;
169
+ }
170
+ /** Full structured query (Firestore + SQL feature set) */
171
+ interface StructuredQuery {
172
+ where?: AnyFilter[];
173
+ orderBy?: OrderByClause[];
174
+ limit?: number;
175
+ offset?: number;
176
+ startAt?: CursorValue;
177
+ startAfter?: CursorValue;
178
+ endAt?: CursorValue;
179
+ endBefore?: CursorValue;
180
+ aggregate?: AggregateSpec[];
181
+ groupBy?: GroupByClause;
182
+ having?: HavingClause[];
183
+ joins?: StructuredJoinClause[];
184
+ vectorSearch?: VectorSearchClause;
185
+ select?: string[];
186
+ distinctField?: string;
187
+ }
188
+ type QueryPresetSpec<Params extends Record<string, unknown> = Record<string, unknown>, Row = any> = {
189
+ params: Params;
190
+ row: Row;
191
+ };
192
+ type QueryPresetMap = Record<string, QueryPresetSpec<any, any>>;
193
+ type QueryPresetParams<TSpec> = TSpec extends QueryPresetSpec<infer Params, any> ? Params : Record<string, unknown>;
194
+ type QueryPresetRow<TSpec> = TSpec extends QueryPresetSpec<any, infer Row> ? Row : any;
195
+ type ChangeOperation = 'insert' | 'update' | 'replace' | 'delete';
48
196
  /**
49
- * Subscription data received from server
197
+ * Fired once when the subscription is first established.
198
+ * `data` is always an array — the full matching collection snapshot.
50
199
  */
51
- interface SubscriptionData<T = any> {
200
+ interface SnapshotEvent<T = any> {
201
+ type: 'snapshot';
52
202
  subscriptionId: string;
53
203
  collection: string;
54
- docId?: string;
55
- data: T | T[];
56
- type: 'snapshot' | 'change';
57
- operation?: 'insert' | 'update' | 'delete' | 'replace';
204
+ data: T[];
58
205
  }
59
206
  /**
60
- * Document reference
207
+ * Fired on every subsequent document mutation that matches the subscription query.
208
+ * `data` is the single affected document (null on delete).
61
209
  */
210
+ interface ChangeEvent<T = any> {
211
+ type: 'change';
212
+ subscriptionId: string;
213
+ collection: string;
214
+ docId: string;
215
+ operation: ChangeOperation;
216
+ data: T | null;
217
+ }
218
+ /** Discriminated union — narrow on `event.type` to get the right shape. */
219
+ type SubscriptionData<T = any> = SnapshotEvent<T> | ChangeEvent<T>;
220
+ type SubscriptionCallback<T = any> = (data: SubscriptionData<T>) => void;
221
+ interface SubscriptionError {
222
+ code?: string;
223
+ message: string;
224
+ permissionDenied: boolean;
225
+ raw?: unknown;
226
+ }
227
+ type SubscriptionErrorCallback = (error: SubscriptionError) => void;
228
+ interface SubscriptionHandle {
229
+ (): void;
230
+ unsubscribe: () => void;
231
+ onError: (callback: SubscriptionErrorCallback) => SubscriptionHandle;
232
+ onPermissionDenied: (callback: SubscriptionErrorCallback) => SubscriptionHandle;
233
+ catch: (callback: SubscriptionErrorCallback) => SubscriptionHandle;
234
+ }
235
+ type DocAddedCallback<T = any> = (data: T, docId: string) => void;
236
+ type DocUpdatedCallback<T = any> = (data: T, docId: string) => void;
237
+ type DocDeletedCallback<T = any> = (docId: string) => void;
238
+ type DocChangedCallback<T = any> = (data: T | null, docId: string, operation: ChangeOperation) => void;
62
239
  interface DocumentSnapshot<T = any> {
63
240
  id: string;
64
241
  data: T | null;
65
242
  exists: boolean;
66
243
  }
67
- /**
68
- * Collection query result
69
- */
70
244
  interface QuerySnapshot<T = any> {
71
245
  docs: DocumentSnapshot<T>[];
72
246
  size: number;
73
247
  empty: boolean;
74
248
  }
75
- /**
76
- * Offline operation
77
- */
78
249
  interface OfflineOperation {
79
250
  id: string;
80
251
  type: 'write' | 'delete';
@@ -84,18 +255,74 @@ interface OfflineOperation {
84
255
  merge?: boolean;
85
256
  clientTs: number;
86
257
  }
87
- /**
88
- * Authentication result
89
- */
90
258
  interface AuthResult {
91
259
  uid: string;
92
260
  token?: string;
93
261
  }
94
- /**
95
- * Connection state
96
- */
97
262
  type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'error';
263
+ interface AuthWithPendingVerificationResult {
264
+ verificationRequired: true;
265
+ created: true;
266
+ emailSent: boolean;
267
+ preview?: {
268
+ code: string;
269
+ link: string;
270
+ };
271
+ }
272
+ interface AuthWithTokenResult extends AuthResult {
273
+ accessToken: string;
274
+ refreshToken: string | null;
275
+ authToken: AuthToken;
276
+ created: boolean;
277
+ }
278
+ interface PresenceMember {
279
+ uid: string;
280
+ socketId: string;
281
+ room: string;
282
+ meta?: Record<string, unknown>;
283
+ joinedAt: number;
284
+ lastSeen: number;
285
+ }
286
+ type PresenceCallback = (members: PresenceMember[]) => void;
287
+ type PresenceJoinCallback = (member: PresenceMember) => void;
288
+ type PresenceLeaveCallback = (uid: string) => void;
289
+ /** Fields marked as vector will be auto-embedded before write */
290
+ type VectorFieldConfig = {
291
+ /** Dimensions of the vector (e.g. 1536 for OpenAI ada-002) */
292
+ dimensions: number;
293
+ /** Optional custom embedding function; defaults to client-configured embedder */
294
+ embed?: (text: string) => Promise<number[]>;
295
+ };
296
+ type RulePermission = "create" | "read" | "update" | "delete";
297
+ interface FlareRule {
298
+ id: string;
299
+ name: string;
300
+ auth: "any" | "guest" | "auth";
301
+ collection: string;
302
+ document?: string;
303
+ condition?: string;
304
+ permissions: RulePermission[];
305
+ }
306
+ interface SecurityRuleEntry {
307
+ ".read"?: string;
308
+ ".write"?: string;
309
+ ".create"?: string;
310
+ ".update"?: string;
311
+ ".delete"?: string;
312
+ }
313
+ type SecurityRulesMap = Record<string, SecurityRuleEntry>;
314
+ declare const flareRulesToSecurityMap: (rules: FlareRule[]) => SecurityRulesMap;
315
+ declare const securityMapToFlareRules: (rules: SecurityRulesMap) => FlareRule[];
98
316
 
317
+ /**
318
+ * Parse ORM-style where condition: { age: ">= 25", role: "admin" }
319
+ * Returns array of QueryConfig objects
320
+ */
321
+ declare function parseWhereCondition(condition: WhereCondition): QueryConfig[];
322
+ /**
323
+ * Parse string value to appropriate type
324
+ */
325
+ declare function parseValue(val: string): any;
99
326
  /**
100
327
  * Query builder for document operations (ORM-style)
101
328
  * Supports: doc('users').update({...}).where({ id: 'alice' })
@@ -114,7 +341,7 @@ declare class DocumentQueryBuilder<T = any> implements PromiseLike<T | null | vo
114
341
  private setData?;
115
342
  private deleteOp;
116
343
  private promise?;
117
- constructor(client: FlareClient, collection: string, legacyId?: string | undefined);
344
+ constructor(client: FlareClient<any>, collection: string, legacyId?: string | undefined);
118
345
  /**
119
346
  * Set where condition
120
347
  */
@@ -157,6 +384,7 @@ declare class DocumentQueryBuilder<T = any> implements PromiseLike<T | null | vo
157
384
  */
158
385
  onSnapshot(callback: SubscriptionCallback<T>): () => void;
159
386
  }
387
+
160
388
  /**
161
389
  * Legacy document reference (for backward compatibility)
162
390
  */
@@ -164,63 +392,124 @@ declare class DocumentReference<T = any> {
164
392
  private client;
165
393
  readonly collection: string;
166
394
  readonly id: string;
167
- constructor(client: FlareClient, collection: string, id: string);
395
+ constructor(client: FlareClient<any>, collection: string, id: string);
168
396
  get(): Promise<T | null>;
169
397
  set(data: Partial<T>): Promise<void>;
170
398
  update(data: Partial<T>): Promise<void>;
171
399
  delete(): Promise<void>;
172
400
  onSnapshot(callback: SubscriptionCallback<T>): () => void;
401
+ /**
402
+ * Fires when this document is updated / replaced.
403
+ * Aliases: onDocModified, onDocChange
404
+ */
405
+ onDocUpdated(callback: DocUpdatedCallback<T>): () => void;
406
+ /** Alias for onDocUpdated */
407
+ onDocModified(callback: DocUpdatedCallback<T>): () => void;
408
+ /** Alias for onDocUpdated */
409
+ onDocChange(callback: DocUpdatedCallback<T>): () => void;
410
+ /**
411
+ * Fires when this document is deleted.
412
+ */
413
+ onDocDeleted(callback: DocDeletedCallback<T>): () => void;
414
+ /**
415
+ * Fires on any change to this document (update / delete).
416
+ * `data` is null on deletes.
417
+ */
418
+ onDocChanged(callback: DocChangedCallback<T>): () => void;
173
419
  }
174
- /**
175
- * Collection reference with ORM-style query builder
176
- *
177
- * This class is thenable - you can await it directly without .get()
178
- * @example
179
- * await collection('users').where({ age: '>= 25' })
180
- */
181
- declare class CollectionReference<T = any> implements PromiseLike<T[]> {
420
+
421
+ type CollectionPresetMethods<TPresetMap extends QueryPresetMap> = {
422
+ [K in keyof TPresetMap & string]: (params: QueryPresetParams<TPresetMap[K]>) => CollectionQuery<QueryPresetRow<TPresetMap[K]>, TPresetMap>;
423
+ };
424
+ type CollectionQuery<T = any, TPresetMap extends QueryPresetMap = {}> = CollectionReference<T, TPresetMap> & CollectionPresetMethods<TPresetMap>;
425
+ declare class CollectionReference<T = any, TPresetMap extends QueryPresetMap = {}> implements PromiseLike<T[]> {
182
426
  private client;
183
427
  readonly collection: string;
184
- private queries;
428
+ private sq;
185
429
  private promise?;
186
- constructor(client: FlareClient, collection: string);
187
- /**
188
- * Get a document reference (legacy API)
189
- */
430
+ constructor(client: FlareClient<TPresetMap>, collection: string);
190
431
  doc(id: string): DocumentReference<T>;
432
+ private clone;
433
+ with<Name extends keyof TPresetMap & string>(name: Name, params: QueryPresetParams<TPresetMap[Name]>): CollectionQuery<QueryPresetRow<TPresetMap[Name]>, TPresetMap>;
434
+ with(name: string, params?: Record<string, unknown>): CollectionQuery<T, TPresetMap>;
435
+ /** ORM shorthand: .where({ age: ">= 25", role: "admin" }) */
436
+ where(condition: WhereCondition): CollectionQuery<T, TPresetMap>;
437
+ /** Explicit field/op/value */
438
+ where(field: string, op: QueryConfig['op'], value: unknown): CollectionQuery<T, TPresetMap>;
439
+ /** OR group: .orWhere([{ field:"status", op:"==", value:"active" }, ...]) */
440
+ orWhere(filters: QueryConfig[]): CollectionQuery<T, TPresetMap>;
441
+ /** Get items starting from the most recently created (descending sequence) */
442
+ latest(): CollectionQuery<T, TPresetMap>;
443
+ /** Get items starting from the first ever created (ascending sequence) */
444
+ oldest(): CollectionQuery<T, TPresetMap>;
445
+ orderBy(field: string, dir?: "asc" | "desc"): CollectionQuery<T, TPresetMap>;
446
+ limit(n: number): CollectionQuery<T, TPresetMap>;
447
+ offset(n: number): CollectionQuery<T, TPresetMap>;
448
+ startAt(...values: unknown[]): CollectionQuery<T, TPresetMap>;
449
+ startAfter(...values: unknown[]): CollectionQuery<T, TPresetMap>;
450
+ endAt(...values: unknown[]): CollectionQuery<T, TPresetMap>;
451
+ endBefore(...values: unknown[]): CollectionQuery<T, TPresetMap>;
452
+ aggregate(...specs: AggregateSpec[]): CollectionQuery<T, TPresetMap>;
453
+ count(alias?: string): CollectionQuery<T, TPresetMap>;
454
+ sum(field: string, alias?: string): CollectionQuery<T, TPresetMap>;
455
+ avg(field: string, alias?: string): CollectionQuery<T, TPresetMap>;
456
+ min(field: string, alias?: string): CollectionQuery<T, TPresetMap>;
457
+ max(field: string, alias?: string): CollectionQuery<T, TPresetMap>;
458
+ distinct(field: string, alias?: string): CollectionQuery<T, TPresetMap>;
459
+ groupBy(...fields: string[]): CollectionQuery<T, TPresetMap>;
460
+ having(field: string, op: HavingClause['op'], value: number): CollectionQuery<T, TPresetMap>;
461
+ private buildStructuredJoin;
191
462
  /**
192
- * Add a where filter (NEW ORM-style API)
193
- * @example .where({ age: ">= 25", role: "admin" })
463
+ * Join another collection into this query.
464
+ *
465
+ * @param collectionName Joined collection name.
466
+ * @param j Join mapping clause.
467
+ * @example
468
+ * flare.collection("boards")
469
+ * .Join("tasks", { source: "id", target: "boardId", as: "tasks" })
470
+ * .get();
194
471
  */
195
- where(condition: WhereCondition): CollectionReference<T>;
472
+ Join(collectionName: string, j: JoinClause): CollectionQuery<T, TPresetMap>;
196
473
  /**
197
- * Get all documents once
198
- * @deprecated Use await directly instead of .get()
474
+ * Legacy join signature.
475
+ * Prefer Join(collectionName, clause) for better readability.
199
476
  */
200
- get(): Promise<T[]>;
477
+ join(collectionName: string, j: JoinClause): CollectionQuery<T, TPresetMap>;
478
+ join(j: JoinClause & {
479
+ from?: string;
480
+ collection?: string;
481
+ }): CollectionQuery<T, TPresetMap>;
482
+ select(...fields: string[]): CollectionQuery<T, TPresetMap>;
483
+ /** Returns unique values for a single field */
484
+ distinctField(field: string): CollectionQuery<T, TPresetMap>;
201
485
  /**
202
- * Internal execute method
486
+ * KNN nearest-neighbour search (requires Atlas vector index).
487
+ * @example
488
+ * col.vectorSearch({ field: "embedding", vector: [...1536 numbers...], k: 10 })
203
489
  */
490
+ vectorSearch(opts: VectorSearchClause): CollectionQuery<T, TPresetMap>;
491
+ get(): Promise<T[]>;
492
+ private _isStructured;
204
493
  private _execute;
205
- /**
206
- * Make this class thenable so it can be awaited directly
207
- */
494
+ private _executeQuery;
495
+ private _executeSubscribe;
208
496
  then<TResult1 = T[], TResult2 = never>(onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): PromiseLike<TResult1 | TResult2>;
209
497
  /**
210
- * Subscribe to real-time updates
211
- */
212
- onSnapshot(callback: SubscriptionCallback<T[]>): () => void;
213
- /**
214
- * Add a new document with auto-generated ID
498
+ * Subscribe to real-time updates.
499
+ * The full StructuredQuery (including orderBy, where, limit, offset) is sent
500
+ * to the server so the initial snapshot respects all constraints.
501
+ * Individual change events are then sorted / filtered client-side to keep
502
+ * the live result consistent.
215
503
  */
504
+ onSnapshot(callback: SubscriptionCallback<T[]>): SubscriptionHandle;
505
+ onDocAdded(callback: DocAddedCallback<T>): () => void;
506
+ onDocUpdated(callback: DocUpdatedCallback<T>): () => void;
507
+ onDocModified(callback: DocUpdatedCallback<T>): () => void;
508
+ onDocChange(callback: DocUpdatedCallback<T>): () => void;
509
+ onDocDeleted(callback: DocDeletedCallback<T>): () => void;
510
+ onDocChanged(callback: DocChangedCallback<T>): () => void;
216
511
  add(data: Partial<T>): Promise<DocumentReference<T>>;
217
- /**
218
- * Update documents matching the where condition
219
- */
220
512
  update(data: Partial<T>): DocumentQueryBuilder<T>;
221
- /**
222
- * Delete documents matching the where condition
223
- */
224
513
  delete(): DocumentQueryBuilder<T>;
225
514
  }
226
515
 
@@ -233,8 +522,13 @@ declare enum FlareAction {
233
522
  AUTH = "auth",
234
523
  PING = "ping",
235
524
  OFFLINE_SYNC = "offline_sync",
236
- /** General-purpose RPC / topic call */
237
- CALL = "call"
525
+ CALL = "call",
526
+ /** One-shot rich query (no real-time subscription) */
527
+ QUERY = "query",
528
+ /** Presence */
529
+ PRESENCE_JOIN = "presence_join",
530
+ PRESENCE_LEAVE = "presence_leave",
531
+ PRESENCE_HEARTBEAT = "presence_heartbeat"
238
532
  }
239
533
  /** Server Response */
240
534
  declare enum FlareEvent {
@@ -245,116 +539,640 @@ declare enum FlareEvent {
245
539
  PONG = "pong",
246
540
  AUTH_OK = "auth_ok",
247
541
  OFFLINE_ACK = "offline_ack",
248
- /** Response to a CALL */
249
- CALL_RESPONSE = "call_response"
542
+ CALL_RESPONSE = "call_response",
543
+ QUERY_RESULT = "query_result",
544
+ PRESENCE_STATE = "presence_state",
545
+ PRESENCE_JOIN = "presence_join",
546
+ PRESENCE_LEAVE = "presence_leave"
250
547
  }
251
548
  interface BaseMessage {
252
549
  id: string;
253
550
  type: FlareAction | FlareEvent;
254
551
  ts: number;
255
552
  }
553
+ interface SubscribeMessage extends BaseMessage {
554
+ type: FlareAction.SUBSCRIBE;
555
+ collection: string;
556
+ docId?: string;
557
+ query?: Record<string, unknown>;
558
+ skipSnapshot?: boolean;
559
+ resumeToken?: string;
560
+ }
561
+
562
+ type TransportOptions = {
563
+ url: string;
564
+ onMessage: (data: any) => void;
565
+ onOpen?: () => void;
566
+ onClose?: () => void;
567
+ onError?: (error: Error) => void;
568
+ autoReconnect?: boolean;
569
+ reconnectDelay?: number;
570
+ maxReconnectDelay?: number;
571
+ debug?: boolean;
572
+ /** RSA public key (PEM). When set, all outgoing messages are RSA-OAEP encrypted. */
573
+ publicKey?: string;
574
+ };
575
+
576
+ declare class FlareTransport {
577
+ private socket;
578
+ private reconnectInterval;
579
+ private maxReconnectDelay;
580
+ private isConnected;
581
+ private shouldReconnect;
582
+ private options;
583
+ private messageQueue;
584
+ private heartbeatInterval;
585
+ private connectionTimeout;
586
+ constructor(options: TransportOptions);
587
+ connect(): void;
588
+ private handleReconnect;
589
+ private startHeartbeat;
590
+ private stopHeartbeat;
591
+ private flushQueue;
592
+ send(message: object): void;
593
+ disconnect(): void;
594
+ get connected(): boolean;
595
+ private log;
596
+ }
256
597
 
257
598
  type ConnectionListener = (state: ConnectionState) => void;
258
599
  type ErrorListener = (error: Error) => void;
259
- declare class FlareClient {
260
- private transport;
261
- private readonly config;
262
- private readonly pendingAcks;
263
- private readonly subscriptions;
264
- private readonly offlineQueue;
265
- private currentState;
266
- private connectionListeners;
267
- private errorListeners;
268
- private authToken?;
269
- private userId?;
270
- private isDebug;
600
+ type HttpResponseSnapshot = {
601
+ status: number;
602
+ headers: Record<string, string>;
603
+ data: any;
604
+ };
605
+ type ActiveSubscription = {
606
+ baseId: string;
607
+ liveId: string;
608
+ collection: string;
609
+ docId?: string;
610
+ query?: QueryConfig | StructuredQuery;
611
+ callback: SubscriptionCallback;
612
+ options: SubscribeOptions;
613
+ };
614
+ type QueryPresetHandler<Params extends Record<string, unknown> = Record<string, unknown>, Row = any> = (ref: CollectionQuery<any, any>, params: Params) => CollectionQuery<Row, any>;
615
+ /** Embedder function registered by the user */
616
+ type EmbedFn = (text: string) => Promise<number[]>;
617
+ declare class FlareBase<TPresetMap extends QueryPresetMap = {}> {
618
+ protected transport: FlareTransport;
619
+ protected readonly config: FlareConfig;
620
+ protected readonly pendingAcks: Map<string, (value: any) => void>;
621
+ protected readonly subscriptions: Map<string, SubscriptionCallback>;
622
+ protected readonly activeSubscriptions: Map<string, ActiveSubscription>;
623
+ protected readonly queryPresets: Map<string, QueryPresetHandler<any, any>>;
624
+ protected readonly subscriptionErrorHandlers: Map<string, Set<SubscriptionErrorCallback>>;
625
+ protected readonly subscriptionPermissionHandlers: Map<string, Set<SubscriptionErrorCallback>>;
626
+ protected readonly subscriptionLastErrors: Map<string, SubscriptionError>;
627
+ protected readonly offlineQueue: any[];
628
+ protected currentState: ConnectionState;
629
+ protected connectionListeners: ConnectionListener[];
630
+ protected errorListeners: ErrorListener[];
631
+ protected isDebug: boolean;
632
+ protected socketAuthUid: string;
633
+ protected pendingSubscriptionReplay: boolean;
634
+ protected subscriptionReplayPromise: Promise<void>;
635
+ protected requestTraceSeq: number;
636
+ protected requestTimingEnabled: boolean;
637
+ protected httpInFlight: Map<string, Promise<HttpResponseSnapshot>>;
638
+ protected httpResponseCache: Map<string, HttpResponseSnapshot>;
639
+ protected readonly maxHttpCacheEntries = 200;
640
+ protected presenceCallbacks: Map<string, PresenceCallback[]>;
641
+ protected presenceJoinCbs: Map<string, PresenceJoinCallback[]>;
642
+ protected presenceLeaveCbs: Map<string, PresenceLeaveCallback[]>;
643
+ protected presenceHeartbeatTimer?: ReturnType<typeof setInterval>;
644
+ protected embedder?: EmbedFn;
645
+ protected vectorSchema: Map<string, Map<string, VectorFieldConfig>>;
646
+ protected throwFetchFlareError(payload: unknown, fallbackMessage: string, fallbackCode: string): never;
647
+ protected nowMs(): number;
648
+ protected normalizeHeaders(headers?: HeadersInit): Record<string, string>;
649
+ protected redactHeaders(headers: Record<string, string>): Record<string, string>;
650
+ protected stableStringify(value: unknown): string;
651
+ protected buildHttpCacheKey(method: string, url: string, headers: Record<string, string>, body: unknown, credentials?: RequestCredentials): string;
652
+ protected shouldCacheResponse(method: string, url: string): boolean;
653
+ protected rememberHttpResponse(key: string, value: HttpResponseSnapshot): void;
654
+ protected createTimedFetchTrace(snapshot: HttpResponseSnapshot, requestId: number, startedAtMs: number, method: string, url: string, networkMs: number): {
655
+ response: {
656
+ status: number;
657
+ ok: boolean;
658
+ headers: {
659
+ get: (name: string) => string | null;
660
+ };
661
+ json: () => Promise<any>;
662
+ };
663
+ requestId: number;
664
+ startedAtMs: number;
665
+ networkMs: number;
666
+ method: string;
667
+ url: string;
668
+ };
669
+ protected logHttpTiming(...args: any[]): void;
670
+ protected mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit;
671
+ protected toWireField(field: string): string;
672
+ protected fromWireField(field: string): string;
673
+ protected normalizeOutboundData(value: unknown): unknown;
674
+ protected normalizeInboundData(value: unknown): unknown;
675
+ protected normalizeOutboundAnyFilter(filter: Record<string, unknown>): Record<string, unknown>;
676
+ protected normalizeOutboundQuery(query: unknown): unknown;
677
+ protected timedFetch(label: string, input: string, init?: RequestInit): Promise<{
678
+ response: {
679
+ status: number;
680
+ ok: boolean;
681
+ headers: {
682
+ get: (name: string) => string | null;
683
+ };
684
+ json: () => Promise<any>;
685
+ };
686
+ requestId: number;
687
+ startedAtMs: number;
688
+ networkMs: number;
689
+ method: string;
690
+ url: string;
691
+ }>;
692
+ protected parseJsonWithTiming(label: string, trace: {
693
+ requestId: number;
694
+ startedAtMs: number;
695
+ response: {
696
+ status: number;
697
+ ok: boolean;
698
+ headers: {
699
+ get: (name: string) => string | null;
700
+ };
701
+ json: () => Promise<any>;
702
+ };
703
+ networkMs: number;
704
+ method: string;
705
+ url: string;
706
+ }): Promise<any>;
707
+ protected getHttpBase(): string;
708
+ protected log(...args: any[]): void;
271
709
  constructor(config: FlareConfig);
272
- /**
273
- * Connect to the server
274
- */
275
710
  connect(): void;
276
- /**
277
- * Disconnect from the server
278
- */
279
711
  disconnect(): void;
280
- /**
281
- * Get a collection reference
282
- */
283
- collection<T = any>(name: string): CollectionReference<T>;
284
- /**
285
- * Get a document query builder (NEW ORM-style API)
286
- * @example flare.doc('users').update({...}).where({ id: 'alice' })
287
- */
288
- doc<T = any>(collection: string): DocumentQueryBuilder<T>;
289
- /**
290
- * Get a document reference (Legacy API - deprecated)
291
- * @deprecated Use doc(collection).where({ id: '...' }) instead
292
- */
293
- doc<T = any>(collection: string, id: string): DocumentReference<T>;
294
- /**
295
- * Authenticate with a token
296
- */
297
- auth(token: string): Promise<AuthResult>;
298
- /**
299
- * Sign out
300
- */
301
- signOut(): void;
302
- /**
303
- * Get current user ID
304
- */
305
- get currentUser(): string | undefined;
306
- /**
307
- * Get connection state
308
- */
309
712
  get connectionState(): ConnectionState;
310
- /**
311
- * Check if connected
312
- */
313
713
  get isConnected(): boolean;
314
- /**
315
- * Listen to connection state changes
316
- */
317
714
  onConnectionStateChange(listener: ConnectionListener): () => void;
715
+ onError(callback: ErrorListener): () => void;
716
+ collection<T = any>(name: string): CollectionQuery<T, TPresetMap>;
717
+ registerQueryPreset<Name extends string, Params extends Record<string, unknown>, Row = any>(name: Name, handler: QueryPresetHandler<Params, Row>): this & FlareBase<TPresetMap & Record<Name, QueryPresetSpec<Params, Row>>>;
718
+ registerQueryPresets<TRegistry extends Record<string, QueryPresetHandler<any, any>>>(presets: TRegistry): this & FlareBase<TPresetMap & {
719
+ [K in keyof TRegistry]: TRegistry[K] extends QueryPresetHandler<infer Params, infer Row> ? QueryPresetSpec<Params, Row> : QueryPresetSpec<Record<string, unknown>, any>;
720
+ }>;
721
+ hasQueryPreset(name: string): boolean;
722
+ applyQueryPreset<Name extends keyof TPresetMap & string>(ref: CollectionReference<any, TPresetMap>, name: Name, params: QueryPresetParams<TPresetMap[Name]>): CollectionQuery<QueryPresetRow<TPresetMap[Name]>, TPresetMap>;
723
+ applyQueryPreset<T = any>(ref: CollectionQuery<T, TPresetMap>, name: string, params?: Record<string, unknown>): CollectionQuery<T, TPresetMap>;
724
+ doc<T = any>(collection: string): DocumentQueryBuilder<T>;
725
+ doc<T = any>(collection: string, id: string): DocumentReference<T>;
726
+ ping(): Promise<number>;
727
+ call<T = Record<string, unknown>>(topic: string, payload?: Record<string, unknown>): Promise<T>;
728
+ query<T = Record<string, unknown>>(collection: string, q?: StructuredQuery): Promise<T[]>;
729
+ setEmbedder(fn: EmbedFn): void;
730
+ markVectorField(collection: string, field: string, config?: VectorFieldConfig): void;
731
+ embedVectorFields(collection: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
732
+ joinPresence(room: string, meta?: Record<string, unknown>): Promise<() => void>;
733
+ leavePresence(room: string): Promise<void>;
734
+ onPresenceState(room: string, cb: PresenceCallback): () => void;
735
+ onPresenceJoin(room: string, cb: PresenceJoinCallback): () => void;
736
+ onPresenceLeave(room: string, cb: PresenceLeaveCallback): () => void;
737
+ private _startPresenceHeartbeat;
738
+ private _stopPresenceHeartbeat;
739
+ syncOffline(): Promise<void>;
740
+ protected beforeActivateSubscription(_entry: ActiveSubscription): Promise<void>;
741
+ protected activateSubscription(entry: ActiveSubscription): Promise<void>;
742
+ protected toSubscriptionError(err: unknown): SubscriptionError;
743
+ protected emitSubscriptionError(baseId: string, error: SubscriptionError): void;
744
+ protected replayActiveSubscriptions(): Promise<void>;
745
+ subscribe(subId: string, collection: string, docId: string | undefined, query: QueryConfig | StructuredQuery | undefined, callback: SubscriptionCallback, options?: SubscribeOptions): SubscriptionHandle;
746
+ send(type: FlareAction, payload: any): Promise<any>;
747
+ private handleTransportError;
748
+ protected onConnected(): void;
749
+ protected onDisconnected(): void;
750
+ protected setState(state: ConnectionState): void;
751
+ protected handleIncoming(msg: any): void;
752
+ }
753
+
754
+ /**
755
+ * FlareAuth extends FlareBase with all authentication and CSRF logic.
756
+ *
757
+ * CSRF strategy
758
+ * ─────────────
759
+ * The server sets an HttpOnly cookie on /auth/config responses. The raw
760
+ * cookie value is NOT readable by JS (HttpOnly), but the server also echoes
761
+ * the token in the `x-flare-csrf` response header so the client can send
762
+ * it back as `x-flare-csrf` on every mutating request.
763
+ *
764
+ * Two environments are supported:
765
+ * 1. Browser – header is read from the /auth/config fetch; stored in
766
+ * `this.csrfToken` (in-memory only, never written to a
767
+ * readable cookie by the client).
768
+ * 2. Next.js SSR – use the standalone `createCsrfProxy()` handler
769
+ * (see Client/proxy.ts) which captures the Set-Cookie header
770
+ * from Flare server and sets it on the client domain too, so
771
+ * the browser owns two HttpOnly cookies: one from the Flare
772
+ * domain and one from the Next.js domain.
773
+ */
774
+ declare class FlareAuth<TPresetMap extends QueryPresetMap = {}> extends FlareBase<TPresetMap> {
775
+ protected authToken?: string;
776
+ protected userId?: string;
777
+ protected authGuard?: AuthGuard;
778
+ protected authConfig?: FlareAuthConfig;
779
+ /** In-memory CSRF token extracted from the `x-flare-csrf` response header */
780
+ protected csrfToken?: string;
781
+ protected csrfInitPromise?: Promise<void>;
782
+ protected csrfBootstrapAttempted: boolean;
783
+ protected socketAuthSyncPromise?: Promise<void>;
784
+ protected authSession: FlareAuthSession | null;
785
+ protected authStateListeners: AuthStateListener[];
786
+ protected authConfigListeners: AuthConfigListener[];
787
+ protected currentProfile: FlareAuthUser | undefined;
788
+ private getDefaultCsrfCookieName;
789
+ getCsrfCookieName(): string;
318
790
  /**
319
- * Listen to errors
791
+ * Read the CSRF token from a browser-readable cookie (set by the server as
792
+ * a non-HttpOnly fallback) or fall back to the in-memory value captured
793
+ * from the response header.
794
+ *
795
+ * Priority:
796
+ * 1. Non-HttpOnly cookie on the current domain (browser only)
797
+ * 2. In-memory value from `x-flare-csrf` response header
320
798
  */
321
- onError(callback: ErrorListener): () => void;
799
+ getCsrfToken(): string | null;
800
+ private getCookieValue;
322
801
  /**
323
- * Ping the server
802
+ * Extract CSRF token from a server response.
803
+ * The server now sends it ONLY as the `x-flare-csrf` response header
804
+ * (not in the JSON body). We still support the body field as a fallback
805
+ * for older server versions.
324
806
  */
325
- ping(): Promise<number>;
807
+ protected extractCsrfToken(json: unknown, response?: {
808
+ headers: {
809
+ get: (name: string) => string | null;
810
+ };
811
+ }): string | undefined;
812
+ getCsrfHeaders(): Record<string, string>;
326
813
  /**
327
- * Invoke a server-side CALL handler by topic and await its response.
814
+ * Inject CSRF token directly (used by SSR middleware).
815
+ * This allows the server to bootstrap CSRF once and inject it into the client,
816
+ * preventing redundant /auth/config calls.
328
817
  *
329
818
  * @example
330
- * const result = await flare.call("agent:backup", { name: "db-daily" });
331
- * console.log(result.jobId);
332
- *
333
- * @param topic The topic registered with `flare.registerCallHandler(topic, fn)` on the server.
334
- * @param payload Arbitrary JSON payload forwarded to the handler.
335
- * @returns The object returned by the server handler.
336
- * @throws If the server returns `success: false` or the request times out.
337
- */
338
- call<T = Record<string, unknown>>(topic: string, payload?: Record<string, unknown>): Promise<T>;
339
- /**
340
- * Sync offline operations
819
+ * // In Next.js middleware
820
+ * const csrf = extractCsrfFromRequest(request, appId);
821
+ * if (csrf) {
822
+ * flareClient.setCsrfToken(csrf);
823
+ * }
341
824
  */
342
- syncOffline(): Promise<void>;
343
- private handleTransportError;
344
- private onConnected;
345
- private onDisconnected;
346
- private setState;
347
- private handleIncoming;
825
+ setCsrfToken(token: string): void;
826
+ ensureCsrfProtection(): Promise<void>;
827
+ loadAuthConfig(): Promise<FlareAuthConfig>;
828
+ protected fetchAuthConfig(): Promise<FlareAuthConfig>;
829
+ onAuthConfigLoaded(listener: AuthConfigListener): () => void;
830
+ protected setProfile(profile: FlareAuthUser): void;
831
+ protected setAuthSession(session: FlareAuthSession | null): void;
832
+ onAuthStateChanged(listener: AuthStateListener): () => void;
833
+ onAuthStateChange(listener: AuthStateListener): () => void;
834
+ get currentUser(): FlareAuthUser | undefined;
835
+ getCurrentUser(): FlareAuthUser | undefined;
836
+ protected syncSocketAuth(accessToken?: string | null): Promise<void>;
837
+ protected updateSocketIdentity(uid?: string, forceReplay?: boolean): Promise<void>;
838
+ protected beforeActivateSubscription(_entry: any): Promise<void>;
839
+ protected onConnected(): void;
840
+ protected handleIncoming(msg: any): void;
841
+ auth(token: string): Promise<AuthResult>;
842
+ signInWithEmailAndPassword(email: string, password: string, options?: {
843
+ scope?: string[];
844
+ createIfMissing?: boolean;
845
+ }): Promise<AuthResult & {
846
+ kind?: string;
847
+ accessToken: string;
848
+ refreshToken: string | null;
849
+ authToken: AuthToken;
850
+ created?: boolean;
851
+ }>;
852
+ signInWithEmail(email: string, password: string, options?: {
853
+ scope?: string[];
854
+ createIfMissing?: boolean;
855
+ }): Promise<AuthResult & {
856
+ kind?: string;
857
+ accessToken: string;
858
+ refreshToken: string | null;
859
+ authToken: AuthToken;
860
+ created?: boolean;
861
+ }>;
862
+ createUserWithEmail(email: string, password: string, options?: {
863
+ scope?: string[];
864
+ additionalParams?: Record<string, string>;
865
+ signInIfAllowed?: boolean;
866
+ }): Promise<{
867
+ kind?: string;
868
+ verificationRequired: true;
869
+ emailSent: boolean;
870
+ preview?: {
871
+ code: string;
872
+ link: string;
873
+ };
874
+ } | (AuthResult & {
875
+ kind?: string;
876
+ accessToken: string;
877
+ refreshToken: string | null;
878
+ authToken: AuthToken;
879
+ verificationRequired?: false;
880
+ emailSent?: boolean;
881
+ preview?: {
882
+ code: string;
883
+ link: string;
884
+ };
885
+ })>;
886
+ createUserWithEmailAndPassword(email: string, password: string, options?: {
887
+ scope?: string[];
888
+ additionalParams?: Record<string, string>;
889
+ signInIfAllowed?: boolean;
890
+ }): Promise<{
891
+ kind?: string;
892
+ verificationRequired: true;
893
+ emailSent: boolean;
894
+ preview?: {
895
+ code: string;
896
+ link: string;
897
+ };
898
+ } | (AuthResult & {
899
+ kind?: string;
900
+ accessToken: string;
901
+ refreshToken: string | null;
902
+ authToken: AuthToken;
903
+ verificationRequired?: false;
904
+ emailSent?: boolean;
905
+ preview?: {
906
+ code: string;
907
+ link: string;
908
+ };
909
+ })>;
910
+ signInOrCreateWithEmail(email: string, password: string, options?: {
911
+ scope?: string[];
912
+ additionalParams?: Record<string, string>;
913
+ }): Promise<{
914
+ kind?: string;
915
+ verificationRequired: true;
916
+ created: true;
917
+ emailSent: boolean;
918
+ preview?: {
919
+ code: string;
920
+ link: string;
921
+ };
922
+ } | (AuthResult & {
923
+ accessToken: string;
924
+ refreshToken: string | null;
925
+ authToken: AuthToken;
926
+ created: boolean;
927
+ })>;
928
+ signInOrCreateWithEmailAndPassword(email: string, password: string, options?: {
929
+ scope?: string[];
930
+ additionalParams?: Record<string, string>;
931
+ }): Promise<{
932
+ kind?: string;
933
+ verificationRequired: true;
934
+ created: true;
935
+ emailSent: boolean;
936
+ preview?: {
937
+ code: string;
938
+ link: string;
939
+ };
940
+ } | (AuthResult & {
941
+ accessToken: string;
942
+ refreshToken: string | null;
943
+ authToken: AuthToken;
944
+ created: boolean;
945
+ })>;
946
+ sendEmailVerification(email: string): Promise<{
947
+ sent: boolean;
948
+ emailSent: boolean;
949
+ preview?: {
950
+ code: string;
951
+ link: string;
952
+ };
953
+ }>;
954
+ verifyEmailWithCode(email: string, code: string): Promise<{
955
+ verified: boolean;
956
+ email: string;
957
+ }>;
958
+ confirmEmailLink(token: string, email: string): Promise<{
959
+ verified: boolean;
960
+ email: string;
961
+ }>;
962
+ sendAccountRecovery(email: string): Promise<{
963
+ sent: boolean;
964
+ emailSent?: boolean;
965
+ preview?: {
966
+ code: string;
967
+ token: string;
968
+ };
969
+ }>;
970
+ recoverAccountWithCode(email: string, code: string, newPassword: string): Promise<{
971
+ recovered: boolean;
972
+ email: string;
973
+ sessionsRevoked?: number;
974
+ }>;
975
+ recoverAccountWithToken(token: string, newPassword: string): Promise<{
976
+ recovered: boolean;
977
+ email: string;
978
+ sessionsRevoked?: number;
979
+ }>;
980
+ signIn(providerId: ProviderId, options?: {
981
+ returnTo?: string;
982
+ metaTag?: string;
983
+ }): Promise<any>;
984
+ signIn(authGuard: Pick<AuthGuard, 'signIn'>, providerId: ProviderId, options?: {
985
+ returnTo?: string;
986
+ metaTag?: string;
987
+ }): Promise<any>;
988
+ signInWithGoogle(options?: {
989
+ returnTo?: string;
990
+ metaTag?: string;
991
+ }): Promise<any>;
992
+ signInWithGitHub(options?: {
993
+ returnTo?: string;
994
+ metaTag?: string;
995
+ }): Promise<any>;
996
+ signInWithFacebook(options?: {
997
+ returnTo?: string;
998
+ metaTag?: string;
999
+ }): Promise<any>;
1000
+ signInWithDropbox(options?: {
1001
+ returnTo?: string;
1002
+ metaTag?: string;
1003
+ }): Promise<any>;
1004
+ handleSignInRedirect(autoRedirect?: boolean): Promise<(AuthResult & {
1005
+ authToken: AuthToken;
1006
+ provider?: ProviderId;
1007
+ }) | null>;
1008
+ handleSignInRedirect(authGuard: Pick<AuthGuard, 'handleRedirect'>, autoRedirect?: boolean): Promise<(AuthResult & {
1009
+ authToken: AuthToken;
1010
+ provider?: ProviderId;
1011
+ }) | null>;
1012
+ private exchangeProviderToken;
1013
+ protected getAuthGuard(): Promise<AuthGuard>;
1014
+ refreshAuthSession(refresh_token?: string): Promise<FlareAuthSession | null>;
1015
+ issueSsrToken(ttlSeconds?: number): Promise<{
1016
+ token: string;
1017
+ token_type: string;
1018
+ expires_in: number;
1019
+ uid: string;
1020
+ role: string;
1021
+ email?: string;
1022
+ }>;
1023
+ signOut(): Promise<void>;
1024
+ protected registerWithEmail(email: string, password: string, options?: {
1025
+ scope?: string[];
1026
+ additionalParams?: Record<string, string>;
1027
+ signInIfAllowed?: boolean;
1028
+ }): Promise<Record<string, any>>;
1029
+ protected requestEmailPasswordToken(email: string, password: string, scope?: string[]): Promise<AuthToken & {
1030
+ kind: string;
1031
+ }>;
1032
+ protected fetchAuthMe(token: string): Promise<{
1033
+ id?: string;
1034
+ email?: string | null;
1035
+ email_verified?: boolean;
1036
+ }>;
1037
+ }
1038
+
1039
+ /**
1040
+ * Client/index.ts ─ entry point
1041
+ *
1042
+ * FlareClient is the public-facing class. All logic lives in:
1043
+ * - Client/base.ts → transport, subscriptions, presence, vector, offline
1044
+ * - Client/auth.ts → CSRF capture, all auth & session methods
1045
+ *
1046
+ * CSRF Protection (SSR-Only by Default)
1047
+ * ────────────────────────────────────
1048
+ * FlareClient does NOT automatically fetch CSRF on construction. Instead:
1049
+ *
1050
+ * 1. SSR (Next.js): Middleware fetches /auth/config once, sets CSRF as HttpOnly
1051
+ * cookie on response. Methods automatically use this cookie (no extra calls).
1052
+ *
1053
+ * 2. Browser-only (SPA): Explicitly call client.ensureCsrfProtection() before
1054
+ * mutations to fetch /auth/config and cache CSRF token in memory.
1055
+ *
1056
+ * Why? Eliminates redundant /auth/config calls in SSR, where every method used
1057
+ * to fetch it again internally. Now CSRF bootstrapping happens once (in middleware),
1058
+ * and auth methods just use getCsrfHeaders() which returns the cached token
1059
+ * (if available) or empty object (relying on HttpOnly cookie validation).
1060
+ *
1061
+ * This file wires FlareAuth together and leaves CSRF bootstrapping to the user.
1062
+ */
1063
+
1064
+ declare class FlareClient<TPresetMap extends QueryPresetMap = {}> extends FlareAuth<TPresetMap> {
1065
+ constructor(config: FlareConfig);
1066
+ }
1067
+
1068
+ /**
1069
+ * Client/proxy.ts ─ Next.js SSR CSRF proxy helper
1070
+ *
1071
+ * Why this exists
1072
+ * ───────────────
1073
+ * The Flare server now sets the CSRF token exclusively as an HttpOnly cookie
1074
+ * on its own domain (e.g. api.flare.example.com) and also echoes it in the
1075
+ * `x-flare-csrf` response header so the browser client can capture it in-
1076
+ * memory and send it back as a request header.
1077
+ *
1078
+ * When your Next.js app runs server-side rendering (SSR / Route Handlers /
1079
+ * Server Actions) it operates on a *different* domain (e.g. app.example.com).
1080
+ * The browser's HttpOnly Flare cookie is scoped to the Flare domain and is
1081
+ * therefore NOT automatically forwarded by the browser to your Next.js
1082
+ * server-side fetch calls.
1083
+ *
1084
+ * Solution — two-cookie strategy
1085
+ * ────────────────────────────────
1086
+ * 1. Browser → Flare domain : Flare sets `__flare_csrf_<appId>` as HttpOnly
1087
+ * on the Flare domain. Browser also reads the
1088
+ * token from the `x-flare-csrf` header and keeps
1089
+ * it in-memory for direct API calls.
1090
+ *
1091
+ * 2. Browser → Next.js domain : Mount `createCsrfProxy()` at a route such as
1092
+ * `/api/flare/csrf`. On first browser load call
1093
+ * this route; it hits Flare's /auth/config,
1094
+ * reads the `x-flare-csrf` header, and sets
1095
+ * it as an HttpOnly cookie on the *Next.js*
1096
+ * domain too (`__flare_csrf_<appId>`).
1097
+ * All subsequent SSR fetch calls from the
1098
+ * Next.js server can then read this cookie from
1099
+ * the incoming request and forward it as the
1100
+ * `x-flare-csrf` header to Flare.
1101
+ *
1102
+ * Usage (Next.js App Router)
1103
+ * ─────────────────────────────
1104
+ * // app/api/flare/csrf/route.ts
1105
+ * import { createCsrfProxy } from "@zuzjs/flare-client/proxy";
1106
+ * export const GET = createCsrfProxy({ endpoint: "https://api.flare.example.com", appId: "my-app" });
1107
+ *
1108
+ * // In any Server Component / Route Handler:
1109
+ * import { extractCsrfFromRequest, buildFlareHeaders } from "@zuzjs/flare-client/proxy";
1110
+ * const csrf = extractCsrfFromRequest(request, "my-app");
1111
+ * const res = await withGet(`${FLARE}/auth/whatever`, { headers: buildFlareHeaders(csrf), ignoreKind: true });
1112
+ *
1113
+ * Usage (Next.js Pages Router)
1114
+ * ──────────────────────────────
1115
+ * // pages/api/flare/csrf.ts
1116
+ * import { createCsrfProxyHandler } from "@zuzjs/flare-client/proxy";
1117
+ * export default createCsrfProxyHandler({ endpoint: "...", appId: "my-app" });
1118
+ */
1119
+ interface CsrfProxyConfig {
1120
+ /** Base URL of the Flare server, e.g. "https://api.flare.example.com" */
1121
+ endpoint: string;
1122
+ /** App ID passed to Flare's /auth/config */
1123
+ appId: string;
1124
+ /** Optional Flare API key */
1125
+ apiKey?: string;
348
1126
  /**
349
- * Internal: Send message to server
1127
+ * Name of the proxy cookie written on the Next.js domain.
1128
+ * Defaults to `__flare_csrf_<appId>`.
350
1129
  */
351
- send(type: FlareAction, payload: any): Promise<any>;
1130
+ proxyCookieName?: string;
352
1131
  /**
353
- * Internal: Create a subscription
1132
+ * Max-Age for the proxy cookie in seconds.
1133
+ * Defaults to 3600 (1 hour).
354
1134
  */
355
- subscribe(subId: string, collection: string, docId: string | undefined, query: QueryConfig | undefined, callback: SubscriptionCallback): () => void;
356
- private log;
1135
+ proxyCookieMaxAge?: number;
357
1136
  }
1137
+ /**
1138
+ * Creates a Next.js App Router `GET` handler that:
1139
+ * 1. Calls Flare's /auth/config and captures the `x-flare-csrf` header.
1140
+ * 2. Sets it as `__flare_csrf_<appId>` HttpOnly cookie on the current domain.
1141
+ * 3. Returns the token in JSON for the browser to store in-memory as well.
1142
+ *
1143
+ * Mount at: `app/api/flare/csrf/route.ts`
1144
+ */
1145
+ declare function createCsrfProxy(config: CsrfProxyConfig): (_request: Request) => Promise<Response>;
1146
+ /**
1147
+ * Creates a Next.js Pages Router API handler (`pages/api/flare/csrf.ts`).
1148
+ * Same behaviour as `createCsrfProxy()` but uses the Node.js req/res API.
1149
+ */
1150
+ declare function createCsrfProxyHandler(config: CsrfProxyConfig): (req: any, res: any) => Promise<void>;
1151
+ /**
1152
+ * Extract the proxied CSRF token from an incoming Next.js request's cookies.
1153
+ *
1154
+ * Call this inside Server Components, Route Handlers, or `getServerSideProps`
1155
+ * to retrieve the token that was set by `createCsrfProxy`.
1156
+ *
1157
+ * @example
1158
+ * // App Router Route Handler
1159
+ * const csrf = extractCsrfFromRequest(request, "my-app");
1160
+ */
1161
+ declare function extractCsrfFromRequest(request: Request | {
1162
+ cookies: Record<string, string> | {
1163
+ get(name: string): {
1164
+ value: string;
1165
+ } | undefined;
1166
+ };
1167
+ }, appId: string, proxyCookieName?: string): string | null;
1168
+ /**
1169
+ * Build the headers object to attach to a server-side fetch call to Flare,
1170
+ * forwarding the CSRF token and (optionally) the Authorization bearer token.
1171
+ */
1172
+ declare function buildFlareHeaders(csrfToken: string | null, options?: {
1173
+ accessToken?: string;
1174
+ apiKey?: string;
1175
+ }): Record<string, string>;
358
1176
 
359
1177
  declare class FlareError extends Error {
360
1178
  readonly code: string;
@@ -362,6 +1180,65 @@ declare class FlareError extends Error {
362
1180
  constructor(message: string, code: string, cause?: unknown | undefined);
363
1181
  }
364
1182
 
1183
+ declare enum FlareErrors {
1184
+ authEmailNotVerified = "auth/email-not-verified",
1185
+ authEmailAlreadyVerified = "auth/email-already-verified",
1186
+ authInvalidToken = "auth/invalid-token",
1187
+ authUserDisabled = "auth/user-disabled",
1188
+ authUserNotFound = "auth/user-not-found",
1189
+ authWrongPassword = "auth/wrong-password",
1190
+ authEmailAlreadyInUse = "auth/email-already-in-use",
1191
+ authInvalidEmail = "auth/invalid-email",
1192
+ authWeakPassword = "auth/weak-password",
1193
+ authTooManyRequests = "auth/too-many-requests",
1194
+ authInternalError = "auth/internal-error"
1195
+ }
1196
+
1197
+ declare enum FlareResponseCodes {
1198
+ health = "health",
1199
+ authConfig = "auth_config",
1200
+ authRegistration = "auth/registration",
1201
+ authRegistrationVerificationRequired = "auth/registration-verification-required",
1202
+ authSession = "auth/session",
1203
+ authExchange = "auth/exchange",
1204
+ authLogout = "auth/logout",
1205
+ authSsrBridge = "auth/ssr_bridge",
1206
+ authSsrVerify = "auth/ssr_verify",
1207
+ accountRecovery = "account/recovery",
1208
+ emailVerification = "email/verification",
1209
+ verificationDispatch = "verification/dispatch",
1210
+ authProfile = "auth/profile",
1211
+ adminToken = "admin/token",
1212
+ documentDelete = "document/delete",
1213
+ documentsDelete = "documents/delete",
1214
+ documents = "documents",
1215
+ document = "document",
1216
+ documentCreate = "document/create",
1217
+ documentUpdate = "document/update",
1218
+ oauthProviderResponse = "oauth_provider_response",
1219
+ success = "success",
1220
+ response = "response"
1221
+ }
1222
+ interface AuthConfigResponse {
1223
+ kind: string;
1224
+ appId: string;
1225
+ enabled: boolean;
1226
+ csrfToken?: string;
1227
+ cookie: {
1228
+ accessTokenName: string;
1229
+ refreshTokenName: string;
1230
+ csrfTokenName: string;
1231
+ path: string;
1232
+ secure: boolean;
1233
+ sameSite: 'Strict' | 'Lax' | 'None';
1234
+ accessTokenMaxAge: number;
1235
+ refreshTokenMaxAge: number;
1236
+ csrfTokenMaxAge: number;
1237
+ };
1238
+ providers: Record<string, any>;
1239
+ ssr: Record<string, any>;
1240
+ }
1241
+
365
1242
  /**
366
1243
  * Initialize and connect to FlareServer
367
1244
  * Returns a singleton instance
@@ -376,4 +1253,4 @@ declare const getFlare: () => FlareClient | null;
376
1253
  */
377
1254
  declare const disconnectFlare: () => void;
378
1255
 
379
- export { type AuthResult, type BaseMessage, CollectionReference, type ConnectionState, DocumentQueryBuilder, DocumentReference, type DocumentSnapshot, FlareAction, type FlareConfig, FlareError, FlareEvent, type OfflineOperation, type QueryConfig, type QueryOperator, type QuerySnapshot, type SubscriptionCallback, type SubscriptionData, type WhereCondition, connectApp, FlareClient as default, disconnectFlare, getFlare };
1256
+ export { type AggregateFunction, type AggregateSpec, type AnyFilter, type AuthConfigListener, type AuthConfigResponse, type AuthResult, type AuthStateListener, type AuthWithPendingVerificationResult, type AuthWithTokenResult, type BaseMessage, type ChangeEvent, type ChangeOperation, type CollectionPresetMethods, type CollectionQuery, CollectionReference, type ConnectionState, type CsrfProxyConfig, type CursorValue, type DocAddedCallback, type DocChangedCallback, type DocDeletedCallback, type DocUpdatedCallback, DocumentQueryBuilder, DocumentReference, type DocumentSnapshot, FlareAction, type FlareAuthConfig, type FlareAuthProviderId, type FlareAuthProviderPublicConfig, type FlareAuthSession, type FlareAuthUser, type FlareConfig, FlareError, FlareErrors, FlareEvent, FlareResponseCodes, type FlareRule, type GroupByClause, type HavingClause, type JoinClause, type JoinQueryPattern, type NestedJoinClause, type OfflineOperation, type OrFilter, type OrderByClause, type PresenceCallback, type PresenceJoinCallback, type PresenceLeaveCallback, type PresenceMember, type QueryConfig, type QueryOperator, type QueryPresetMap, type QueryPresetParams, type QueryPresetRow, type QueryPresetSpec, type QuerySnapshot, type RulePermission, type SecurityRuleEntry, type SecurityRulesMap, type SnapshotEvent, type StructuredJoinClause, type StructuredQuery, type SubscribeMessage, type SubscribeOptions, type SubscriptionCallback, type SubscriptionData, type SubscriptionError, type SubscriptionErrorCallback, type SubscriptionHandle, type VectorFieldConfig, type VectorSearchClause, type WhereCondition, buildFlareHeaders, connectApp, createCsrfProxy, createCsrfProxyHandler, FlareClient as default, disconnectFlare, extractCsrfFromRequest, flareRulesToSecurityMap, getFlare, parseValue, parseWhereCondition, securityMapToFlareRules };