@estra/client 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Realm 8
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,54 @@
1
- # Temporary Holding Version
1
+ # @estra/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The client for [Estra](https://estra.cloud): documents and queries, live listeners, auth and callable
4
+ functions. It runs in browsers, in Node 20+ and on edge runtimes, and has no dependencies.
5
+
6
+ ```sh
7
+ npm install @estra/client
8
+ ```
9
+
10
+ ## Firebase-compatible API
11
+
12
+ If your app uses the Firebase web SDK, switch the imports and point the app at your Estra URL:
13
+
14
+ ```ts
15
+ import { initializeApp } from "@estra/client/compat/app";
16
+ import { getAuth, signInWithEmailAndPassword } from "@estra/client/compat/auth";
17
+ import { collection, getFirestore, onSnapshot, query, where } from "@estra/client/compat/firestore";
18
+
19
+ const app = initializeApp({ estraUrl: "https://my-app.estra.cloud" });
20
+ const auth = getAuth(app);
21
+ const db = getFirestore(app);
22
+
23
+ await signInWithEmailAndPassword(auth, "ann@example.com", "password");
24
+ onSnapshot(query(collection(db, "posts"), where("authorId", "==", auth.currentUser!.uid)), (snap) => {
25
+ for (const d of snap.docs) console.log(d.id, d.data());
26
+ });
27
+ ```
28
+
29
+ `@estra/client/compat/storage` covers file storage the same way.
30
+
31
+ ## Callable functions
32
+
33
+ ```ts
34
+ import { getFunctions, httpsCallable } from "@estra/client/functions";
35
+
36
+ const functions = getFunctions({ url: "https://my-app.estra.cloud", getToken: () => auth.currentUser?.getIdToken() ?? null });
37
+ const { data } = await httpsCallable(functions, "addPost")({ title: "Hello" });
38
+ ```
39
+
40
+ ## Servers and scripts
41
+
42
+ `httpTransport` reads and writes as whoever its token belongs to. With your project's service key it
43
+ acts as an admin and skips security rules, so keep that key on servers only.
44
+
45
+ ```ts
46
+ import { httpTransport } from "@estra/client";
47
+
48
+ const db = httpTransport({ url: "https://my-app.estra.cloud", getToken: () => process.env.ESTRA_SERVICE_KEY!, WebSocket: false });
49
+ const [post] = await db.getAll(["posts/p1"]);
50
+ await db.commit({ writes: [{ op: "update", path: "posts/p1", data: { score: { $op: "inc", by: 1 } } }] });
51
+ const { docs } = await db.query({ from: "posts", where: { field: "score", op: ">", value: 10 }, limit: 20 });
52
+ ```
53
+
54
+ MIT licensed.
@@ -0,0 +1,2 @@
1
+ export * from "./query.js";
2
+ export * from "./local.js";
@@ -0,0 +1,2 @@
1
+ export * from "./query.js";
2
+ export * from "./local.js";
@@ -0,0 +1,14 @@
1
+ import { type DocOut, type Filter, type Query, type Write } from "./query.js";
2
+ /** The value at a dotted path ("stats.plays"), or undefined. */
3
+ export declare function getPath(o: Record<string, unknown>, path: string): unknown;
4
+ /** A doc's data after a write, or null after a delete. `now` stands in for serverTimestamp. Keys may be dotted. */
5
+ export declare function applyWrite(before: Record<string, unknown> | null, w: Write, now: number): Record<string, unknown> | null;
6
+ export declare function matchesFilter(d: DocOut, f: Filter | undefined): boolean;
7
+ /** Is this document in the query's collection (path or group)? */
8
+ export declare function inCollection(d: DocOut, q: Query): boolean;
9
+ /** Compares two docs in the query's result order (orderBy, then id). */
10
+ export declare function orderComparator(query: Query): (a: DocOut, b: DocOut) => number;
11
+ /** Could this doc be in the query's results, ignoring limit/limitToLast/offset? */
12
+ export declare function matchesQuery(d: DocOut, q: Query): boolean;
13
+ /** Runs a query over documents in memory, with Firestore/Estra semantics. */
14
+ export declare function runLocal(docs: Iterable<DocOut>, query: Query): DocOut[];
@@ -0,0 +1,186 @@
1
+ // In-memory evaluation over wire-format documents: apply a write (sentinels included) and run a
2
+ // query (filters, order, cursors, limit, near). Clients use it for the offline cache and latency
3
+ // compensation; the server's realtime sessions use it to skip re-reads a change can't affect.
4
+ // Mirrors the SQL engine's semantics (packages/server/src/compile.ts): keep them in step.
5
+ import { withImplicitOrder } from "./query.js";
6
+ const isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
7
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
8
+ const isSentinel = (v) => isObj(v) && typeof v.$op === "string";
9
+ /** The value at a dotted path ("stats.plays"), or undefined. */
10
+ export function getPath(o, path) {
11
+ let cur = o;
12
+ for (const k of path.split("."))
13
+ cur = isObj(cur) ? cur[k] : undefined;
14
+ return cur;
15
+ }
16
+ function setPath(o, path, v) {
17
+ const segs = path.split(".");
18
+ let cur = o;
19
+ for (const k of segs.slice(0, -1)) {
20
+ if (!isObj(cur[k]))
21
+ cur[k] = {};
22
+ cur = cur[k];
23
+ }
24
+ const last = segs[segs.length - 1];
25
+ if (v === undefined || v === null)
26
+ delete cur[last];
27
+ else
28
+ cur[last] = v;
29
+ }
30
+ /** A doc's data after a write, or null after a delete. `now` stands in for serverTimestamp. Keys may be dotted. */
31
+ export function applyWrite(before, w, now) {
32
+ if (w.op === "delete")
33
+ return null;
34
+ if (w.op === "check")
35
+ return before;
36
+ const out = w.op === "set" && !w.merge ? {} : structuredClone(before ?? {});
37
+ for (const [k, v] of Object.entries(w.data)) {
38
+ if (!isSentinel(v)) {
39
+ setPath(out, k, v);
40
+ continue;
41
+ }
42
+ const cur = getPath(out, k);
43
+ if (v.$op === "delete")
44
+ setPath(out, k, undefined);
45
+ else if (v.$op === "now")
46
+ setPath(out, k, { $t: now });
47
+ else if (v.$op === "inc")
48
+ setPath(out, k, (typeof cur === "number" ? cur : 0) + (v.by ?? 0));
49
+ else if (v.$op === "union") {
50
+ const arr = Array.isArray(cur) ? [...cur] : [];
51
+ for (const e of v.values ?? [])
52
+ if (!arr.some((x) => same(x, e)))
53
+ arr.push(e);
54
+ setPath(out, k, arr);
55
+ }
56
+ else if (v.$op === "remove")
57
+ setPath(out, k, (Array.isArray(cur) ? cur : []).filter((x) => !(v.values ?? []).some((r) => same(x, r))));
58
+ }
59
+ return out;
60
+ }
61
+ /** Comparable form of a wire value (timestamps → ms, refs → path). */
62
+ const key = (v) => (isObj(v) && typeof v.$t === "number" ? v.$t : isObj(v) && typeof v.$ref === "string" ? v.$ref : typeof v === "boolean" ? (v ? 1 : 0) : v);
63
+ function cmp(a, b) {
64
+ const x = key(a), y = key(b);
65
+ if (typeof x === "number" && typeof y === "number")
66
+ return x - y;
67
+ if (typeof x === "string" && typeof y === "string")
68
+ return x < y ? -1 : x > y ? 1 : 0;
69
+ return null;
70
+ }
71
+ const fieldOf = (d, f) => (f === "id" || f === "__name__" ? d.id : getPath(d.data, f));
72
+ export function matchesFilter(d, f) {
73
+ if (!f)
74
+ return true;
75
+ if ("and" in f)
76
+ return f.and.every((x) => matchesFilter(d, x));
77
+ if ("or" in f)
78
+ return f.or.some((x) => matchesFilter(d, x));
79
+ const v = fieldOf(d, f.field);
80
+ const missing = v === undefined || v === null;
81
+ const eq = (a, b) => same(key(a), key(b));
82
+ switch (f.op) {
83
+ case "==":
84
+ return f.value === null ? missing : !missing && eq(v, f.value);
85
+ case "!=":
86
+ return f.value === null ? !missing : !missing && !eq(v, f.value);
87
+ case "<":
88
+ case "<=":
89
+ case ">":
90
+ case ">=": {
91
+ if (missing)
92
+ return false;
93
+ const c = cmp(v, f.value);
94
+ if (c === null)
95
+ return false;
96
+ return f.op === "<" ? c < 0 : f.op === "<=" ? c <= 0 : f.op === ">" ? c > 0 : c >= 0;
97
+ }
98
+ case "in":
99
+ return !missing && Array.isArray(f.value) && f.value.some((x) => eq(v, x));
100
+ case "not-in":
101
+ return !missing && Array.isArray(f.value) && !f.value.some((x) => eq(v, x));
102
+ case "array-contains":
103
+ return Array.isArray(v) && v.some((x) => eq(x, f.value));
104
+ case "array-contains-any":
105
+ return Array.isArray(v) && Array.isArray(f.value) && v.some((x) => f.value.some((y) => eq(x, y)));
106
+ }
107
+ return false;
108
+ }
109
+ const haversineMi = (a, b, c, d) => {
110
+ const r = (x) => (x * Math.PI) / 180;
111
+ const h = Math.sin(r(c - a) / 2) ** 2 + Math.cos(r(a)) * Math.cos(r(c)) * Math.sin(r(d - b) / 2) ** 2;
112
+ return 3958.8 * 2 * Math.asin(Math.min(1, Math.sqrt(h)));
113
+ };
114
+ /** Is this document in the query's collection (path or group)? */
115
+ export function inCollection(d, q) {
116
+ const segs = d.path.split("/");
117
+ if (q.group !== undefined)
118
+ return segs[segs.length - 2] === q.group;
119
+ return segs.slice(0, -1).join("/") === q.from;
120
+ }
121
+ const orderKeys = (q) => {
122
+ const orders = q.orderBy ?? [];
123
+ return orders.some((o) => o.field === "id" || o.field === "__name__") ? orders : [...orders, { field: "id", dir: orders.at(-1)?.dir ?? "asc" }];
124
+ };
125
+ /** Compares two docs in the query's result order (orderBy, then id). */
126
+ export function orderComparator(query) {
127
+ const keys = orderKeys(withImplicitOrder(query));
128
+ return (a, b) => {
129
+ for (const k of keys) {
130
+ const c = cmp(fieldOf(a, k.field), fieldOf(b, k.field)) ?? 0;
131
+ if (c)
132
+ return k.dir === "desc" ? -c : c;
133
+ }
134
+ return 0;
135
+ };
136
+ }
137
+ /** Could this doc be in the query's results, ignoring limit/limitToLast/offset? */
138
+ export function matchesQuery(d, q) {
139
+ return runLocal([d], { ...q, limit: undefined, limitToLast: undefined, offset: undefined }).length === 1;
140
+ }
141
+ /** Runs a query over documents in memory, with Firestore/Estra semantics. */
142
+ export function runLocal(docs, query) {
143
+ const q = withImplicitOrder(query);
144
+ let out = [...docs].filter((d) => inCollection(d, q) && matchesFilter(d, q.where));
145
+ const orders = q.orderBy ?? [];
146
+ for (const o of orders)
147
+ if (o.field !== "id" && o.field !== "__name__")
148
+ out = out.filter((d) => getPath(d.data, o.field) !== undefined && getPath(d.data, o.field) !== null);
149
+ if (q.near) {
150
+ const n = q.near;
151
+ out = out.flatMap((d) => {
152
+ const g = getPath(d.data, n.field);
153
+ if (!g?.$geo)
154
+ return [];
155
+ const dist = haversineMi(n.lat, n.lng, g.$geo[0], g.$geo[1]);
156
+ return dist <= n.radiusMi ? [{ ...d, distanceMi: Math.round(dist * 100) / 100 }] : [];
157
+ });
158
+ }
159
+ const keys = orderKeys(q);
160
+ const sortKey = orderComparator(q);
161
+ if (q.near && !orders.length)
162
+ out.sort((a, b) => (a.distanceMi ?? 0) - (b.distanceMi ?? 0));
163
+ else
164
+ out.sort(sortKey);
165
+ // Where a doc sits against a cursor in result order: <0 before it, 0 on it, >0 past it.
166
+ const against = (d, cursor) => {
167
+ for (let i = 0; i < cursor.length; i++) {
168
+ const k = keys[i];
169
+ const c = cmp(fieldOf(d, k.field), cursor[i]) ?? 0;
170
+ if (c)
171
+ return k.dir === "desc" ? -c : c;
172
+ }
173
+ return 0;
174
+ };
175
+ const start = q.startAfter ?? q.startAt;
176
+ if (start)
177
+ out = out.filter((d) => (q.startAt ? against(d, start) >= 0 : against(d, start) > 0));
178
+ const end = q.endBefore ?? q.endAt;
179
+ if (end)
180
+ out = out.filter((d) => (q.endAt ? against(d, end) <= 0 : against(d, end) < 0));
181
+ if (q.offset)
182
+ out = out.slice(q.offset);
183
+ if (q.limitToLast !== undefined)
184
+ return out.slice(Math.max(0, out.length - q.limitToLast));
185
+ return q.limit !== undefined ? out.slice(0, q.limit) : out;
186
+ }
@@ -0,0 +1,132 @@
1
+ export type Op = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "array-contains" | "array-contains-any";
2
+ export interface FieldFilter {
3
+ field: string;
4
+ op: Op;
5
+ value: unknown;
6
+ }
7
+ export type Filter = FieldFilter | {
8
+ and: Filter[];
9
+ } | {
10
+ or: Filter[];
11
+ };
12
+ export interface Order {
13
+ field: string;
14
+ dir?: "asc" | "desc";
15
+ }
16
+ export interface Near {
17
+ field: string;
18
+ lat: number;
19
+ lng: number;
20
+ radiusMi: number;
21
+ }
22
+ export interface Query {
23
+ /** Collection path ("posts", "posts/abc/comments"), or `group` for every parent at once. */
24
+ from?: string;
25
+ /** Collection id for a collection-group query ("comments"). */
26
+ group?: string;
27
+ where?: Filter;
28
+ orderBy?: Order[];
29
+ limit?: number;
30
+ /** The last N results in orderBy order (needs an orderBy; not with limit). */
31
+ limitToLast?: number;
32
+ /** Skip this many results first (with limit, not limitToLast). Service key only (clients page with cursors); at most 10,000. */
33
+ offset?: number;
34
+ /** Cursor values in orderBy order, optionally followed by the document id. */
35
+ startAfter?: unknown[];
36
+ startAt?: unknown[];
37
+ /** Upper cursors, same shape as the start ones. */
38
+ endAt?: unknown[];
39
+ endBefore?: unknown[];
40
+ /** Points within radiusMi of (lat, lng). Results carry `distanceMi`. */
41
+ near?: Near;
42
+ aggregate?: {
43
+ count?: boolean;
44
+ sum?: string[];
45
+ avg?: string[];
46
+ };
47
+ }
48
+ /** Field value sentinels (the Firestore FieldValue equivalents). */
49
+ export type Sentinel = {
50
+ $op: "inc";
51
+ by: number;
52
+ } | {
53
+ $op: "union";
54
+ values: unknown[];
55
+ } | {
56
+ $op: "remove";
57
+ values: unknown[];
58
+ } | {
59
+ $op: "now";
60
+ } | {
61
+ $op: "delete";
62
+ };
63
+ export interface Precondition {
64
+ exists?: boolean;
65
+ /** Row version (`v` of a read); 0 = must not exist. */
66
+ version?: number;
67
+ }
68
+ export type Write = {
69
+ op: "set";
70
+ path: string;
71
+ data: Record<string, unknown>;
72
+ merge?: boolean;
73
+ pre?: Precondition;
74
+ } | {
75
+ op: "update";
76
+ path: string;
77
+ data: Record<string, unknown>;
78
+ pre?: Precondition;
79
+ } | {
80
+ op: "delete";
81
+ path: string;
82
+ pre?: Precondition;
83
+ }
84
+ /** Writes nothing: only asserts `pre` (transactions guard the documents they read but don't write). */
85
+ | {
86
+ op: "check";
87
+ path: string;
88
+ pre: Precondition;
89
+ };
90
+ export interface CommitRequest {
91
+ writes: Write[];
92
+ /** Idempotency key: a commit replayed with the same key is applied once. */
93
+ opKey?: string;
94
+ }
95
+ export interface DocOut {
96
+ path: string;
97
+ id: string;
98
+ v: number;
99
+ createTime: number;
100
+ updateTime: number;
101
+ data: Record<string, unknown>;
102
+ distanceMi?: number;
103
+ /** Set by the client's offline cache: where this copy came from. */
104
+ meta?: {
105
+ fromCache: boolean;
106
+ hasPendingWrites: boolean;
107
+ };
108
+ }
109
+ export declare const inc: (by?: number) => Sentinel;
110
+ export declare const arrayUnion: (...values: unknown[]) => Sentinel;
111
+ export declare const arrayRemove: (...values: unknown[]) => Sentinel;
112
+ export declare const serverTimestamp: () => Sentinel;
113
+ export declare const deleteField: () => Sentinel;
114
+ /** Tagged wire values: timestamps, refs and points. */
115
+ export declare const ts: (ms: number) => {
116
+ $t: number;
117
+ };
118
+ export declare const ref: (path: string) => {
119
+ $ref: string;
120
+ };
121
+ export declare const geo: (lat: number, lng: number) => {
122
+ $geo: [number, number];
123
+ };
124
+ /**
125
+ * Firestore's implicit order: a query with inequality filters and no orderBy is ordered by the
126
+ * inequality fields (ascending, by field name), then by id. Everything that orders results (SQL,
127
+ * the gateway's paging, the in-memory engine) goes through this, so a `limit` keeps the same rows.
128
+ * https://firebase.google.com/docs/firestore/query-data/order-limit-data#limitations
129
+ */
130
+ export declare function withImplicitOrder<Q extends Query>(q: Q): Q & {
131
+ orderBy?: Order[];
132
+ };
@@ -0,0 +1,34 @@
1
+ // The query and write AST: the JSON every client sends (POST /v1/query, /v1/commit).
2
+ export const inc = (by = 1) => ({ $op: "inc", by });
3
+ export const arrayUnion = (...values) => ({ $op: "union", values });
4
+ export const arrayRemove = (...values) => ({ $op: "remove", values });
5
+ export const serverTimestamp = () => ({ $op: "now" });
6
+ export const deleteField = () => ({ $op: "delete" });
7
+ /** Tagged wire values: timestamps, refs and points. */
8
+ export const ts = (ms) => ({ $t: ms });
9
+ export const ref = (path) => ({ $ref: path });
10
+ export const geo = (lat, lng) => ({ $geo: [lat, lng] });
11
+ const INEQUALITY = new Set(["<", "<=", ">", ">=", "!=", "not-in"]);
12
+ /**
13
+ * Firestore's implicit order: a query with inequality filters and no orderBy is ordered by the
14
+ * inequality fields (ascending, by field name), then by id. Everything that orders results (SQL,
15
+ * the gateway's paging, the in-memory engine) goes through this, so a `limit` keeps the same rows.
16
+ * https://firebase.google.com/docs/firestore/query-data/order-limit-data#limitations
17
+ */
18
+ export function withImplicitOrder(q) {
19
+ if (q.orderBy?.length || q.near)
20
+ return q;
21
+ const fields = new Set();
22
+ const walk = (f) => {
23
+ if (!f)
24
+ return;
25
+ if ("and" in f)
26
+ f.and.forEach(walk);
27
+ else if ("or" in f)
28
+ f.or.forEach(walk);
29
+ else if (INEQUALITY.has(f.op) && f.field !== "id" && f.field !== "__name__")
30
+ fields.add(f.field);
31
+ };
32
+ walk(q.where);
33
+ return fields.size ? { ...q, orderBy: [...fields].sort().map((field) => ({ field, dir: "asc" })) } : q;
34
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ export interface AuthUser {
2
+ uid: string;
3
+ email: string | null;
4
+ emailVerified: boolean;
5
+ displayName: string | null;
6
+ photoUrl: string | null;
7
+ anonymous: boolean;
8
+ disabled: boolean;
9
+ providers: string[];
10
+ createdAt: number;
11
+ lastSignInAt: number | null;
12
+ /** Sessions from before this time (ms) were revoked; null when never revoked. */
13
+ tokensValidAfter?: number | null;
14
+ claims: Record<string, unknown>;
15
+ }
16
+ /** Where the session lives: localStorage on the web, Keychain/Keystore on mobile, memory in tests. */
17
+ export interface SessionStorage {
18
+ get(key: string): string | null | Promise<string | null>;
19
+ set(key: string, value: string): void | Promise<void>;
20
+ remove(key: string): void | Promise<void>;
21
+ }
22
+ export declare const memoryStorage: () => SessionStorage;
23
+ export declare const browserStorage: () => SessionStorage;
24
+ /** The slice of `window` the popup/redirect flows use (injectable for tests). */
25
+ export interface WindowLike {
26
+ location: {
27
+ origin: string;
28
+ href: string;
29
+ hash: string;
30
+ assign(url: string): void;
31
+ };
32
+ open(url: string, target?: string, features?: string): {
33
+ closed: boolean;
34
+ location: {
35
+ href: string;
36
+ };
37
+ close(): void;
38
+ } | null;
39
+ addEventListener(type: "message", fn: (e: {
40
+ origin: string;
41
+ data: unknown;
42
+ source?: unknown;
43
+ }) => void): void;
44
+ removeEventListener(type: "message", fn: (e: {
45
+ origin: string;
46
+ data: unknown;
47
+ source?: unknown;
48
+ }) => void): void;
49
+ history?: {
50
+ replaceState(data: unknown, unused: string, url: string): void;
51
+ };
52
+ }
53
+ export type OAuthProvider = "google.com" | "apple.com" | "github.com";
54
+ export interface AuthOptions {
55
+ url: string;
56
+ /** Browser window for popup/redirect sign-in (defaults to globalThis.window). */
57
+ window?: WindowLike;
58
+ storage?: SessionStorage;
59
+ fetch?: typeof fetch;
60
+ /** Refresh this long before expiry (default 5 min). */
61
+ refreshMarginMs?: number;
62
+ }
63
+ export type Unsubscribe = () => void;
64
+ export declare function createAuth(o: AuthOptions): {
65
+ readonly currentUser: AuthUser | null;
66
+ /** Resolves once the stored session (if any) is loaded. */
67
+ ready: () => Promise<void>;
68
+ onAuthStateChanged(cb: (u: AuthUser | null) => void): Unsubscribe;
69
+ /** A valid access token (refreshed when near expiry), or null when signed out. */
70
+ getIdToken(forceRefresh?: boolean): Promise<string | null>;
71
+ signUp: (email: string, password: string, displayName?: string) => Promise<AuthUser>;
72
+ signIn: (email: string, password: string) => Promise<AuthUser>;
73
+ /** Google/Apple: pass the ID token from Google Identity Services or Sign in with Apple. */
74
+ signInWithIdToken: (provider: "google.com" | "apple.com", idToken: string, o?: {
75
+ nonce?: string;
76
+ displayName?: string;
77
+ }) => Promise<AuthUser>;
78
+ signInAnonymously: () => Promise<AuthUser>;
79
+ /** Google / Apple / GitHub in a popup (call from a click handler). */
80
+ signInWithPopup: (provider: OAuthProvider) => Promise<AuthUser>;
81
+ /** Link a provider to the signed-in account (an anonymous account becomes a full one, same uid). */
82
+ linkWithPopup: (provider: OAuthProvider) => Promise<AuthUser>;
83
+ /** Sign in again with the same account (before sensitive changes). Fails if a different account answers. */
84
+ reauthenticateWithPopup: (provider: OAuthProvider) => Promise<AuthUser>;
85
+ /** Full-page redirect sign-in; finish with getRedirectResult() on the page you return to. */
86
+ signInWithRedirect: (provider: OAuthProvider, returnTo?: string) => void;
87
+ linkWithRedirect: (provider: OAuthProvider, returnTo?: string) => Promise<void>;
88
+ /** After a redirect: the signed-in user, null when this page load isn't a sign-in return. */
89
+ getRedirectResult: () => Promise<AuthUser | null>;
90
+ /** Native apps: link a Google/Apple ID token to the signed-in account. */
91
+ linkWithIdToken: (provider: "google.com" | "apple.com", idToken: string, nonce?: string) => Promise<AuthUser>;
92
+ sendSignInLink: (email: string) => Promise<unknown>;
93
+ /** Finish a link from the email: mode + oobCode come from its query string. */
94
+ signInWithLink: (oobCode: string) => Promise<AuthUser>;
95
+ sendPasswordReset: (email: string) => Promise<unknown>;
96
+ confirmPasswordReset: (oobCode: string, newPassword: string) => Promise<unknown>;
97
+ sendEmailVerification: () => Promise<unknown>;
98
+ applyEmailVerification: (oobCode: string) => Promise<void>;
99
+ updateProfile: (p: {
100
+ displayName?: string | null;
101
+ photoUrl?: string | null;
102
+ password?: string;
103
+ currentPassword?: string;
104
+ }) => Promise<AuthUser>;
105
+ deleteUser: () => Promise<void>;
106
+ signOut: () => Promise<void>;
107
+ };
108
+ export type EstraAuth = ReturnType<typeof createAuth>;