clefbase 2.0.2 → 2.0.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.
@@ -1,163 +1,107 @@
1
1
  import { HttpClient } from "../http";
2
- import type { AuthUser, AuthResult } from "../types";
3
- /**
4
- * Options for `auth.signInWithGateway()`.
5
- */
2
+ import type { AuthUser, AuthResult, AuthOptions } from "../types";
6
3
  export interface GatewaySignInOptions {
7
4
  /**
8
- * The URL the gateway should redirect back to after authentication.
9
- * Defaults to `window.location.origin` (your app's root).
10
- * Must be registered as an allowed redirect for your project in the gateway.
5
+ * Where the gateway should redirect after sign-in.
6
+ * Defaults to `window.location.origin`.
7
+ * Must be registered as an allowed redirect for your project.
11
8
  */
12
9
  redirectUrl?: string;
13
10
  }
11
+ export interface OpenAuthUIOptions {
12
+ /**
13
+ * How to display the auth UI.
14
+ * - `"modal"` (default) — overlay on top of the current page
15
+ * - `"redirect"` — full-page redirect to auth.cleforyx.com
16
+ */
17
+ mode?: "modal" | "redirect";
18
+ /** Called when the user successfully signs in. */
19
+ onSuccess?: (result: AuthResult) => void;
20
+ /** Called when the modal is dismissed without signing in. */
21
+ onDismiss?: () => void;
22
+ /** Where to redirect after sign-in (redirect mode only). Defaults to current page. */
23
+ redirectUrl?: string;
24
+ }
14
25
  type AuthStateCallback = (user: AuthUser | null) => void;
15
- /**
16
- * Authentication service. Obtain via `getAuth(app)`.
17
- *
18
- * @example
19
- * const auth = getAuth(app);
20
- *
21
- * // Email / password
22
- * const { user } = await auth.signIn("alice@example.com", "pass");
23
- *
24
- * // Google — redirect to gateway, come back signed in
25
- * await auth.signInWithGateway("google");
26
- *
27
- * // On every page load, handle the gateway callback:
28
- * const result = await auth.handleGatewayCallback();
29
- * if (result) console.log("Signed in via gateway:", result.user.email);
30
- */
31
26
  export declare class Auth {
32
27
  private readonly http;
33
28
  private readonly store;
34
29
  private listeners;
35
- /** The base URL of the Cleforyx auth gateway (e.g. https://auth.cleforyx.com) */
36
30
  private readonly gatewayUrl;
37
- /** The project ID — used as the `?project=` param when redirecting to the gateway */
38
31
  private readonly projectId;
39
- constructor(http: HttpClient, gatewayUrl: string, projectId: string);
32
+ private _modalCleanup;
33
+ private readonly apiEndpoint;
34
+ private readonly dbId?;
35
+ constructor(http: HttpClient, gatewayUrl: string, projectId: string, options?: AuthOptions);
36
+ /**
37
+ * Get the appropriate endpoint base path for API calls.
38
+ * - "public": /auth/public (for client-side flows)
39
+ * - "external": /auth (for server-side flows with API key)
40
+ * @internal
41
+ */
42
+ private getEndpointPath;
40
43
  private notify;
41
44
  private handleResult;
42
45
  /** @internal — used by Storage to attach the bearer token to upload requests */
43
46
  getAuthHeaders(): Record<string, string>;
44
- /** The currently signed-in user, or null. */
45
47
  get currentUser(): AuthUser | null;
46
- /** The raw bearer token for the active session, or null. */
47
48
  get currentToken(): string | null;
48
- /**
49
- * Create a new account with email + password.
50
- *
51
- * @example
52
- * const { user } = await auth.signUp("alice@example.com", "pass123", {
53
- * displayName: "Alice",
54
- * metadata: { role: "admin" },
55
- * });
56
- */
57
49
  signUp(email: string, password: string, profile?: {
58
50
  displayName?: string;
59
51
  photoUrl?: string;
60
52
  metadata?: Record<string, unknown>;
61
53
  }): Promise<AuthResult>;
62
- /**
63
- * Sign in with email + password.
64
- *
65
- * @example
66
- * const { user, token } = await auth.signIn("alice@example.com", "pass");
67
- */
68
54
  signIn(email: string, password: string): Promise<AuthResult>;
69
55
  /**
70
- * Redirect the user to the Cleforyx auth gateway to sign in with an
71
- * OAuth provider (currently `"google"`).
72
- *
73
- * The gateway handles the OAuth flow and redirects back to your app with
74
- * `?cfx_token=<token>&cfx_project=<projectId>` in the URL.
75
- * Call `auth.handleGatewayCallback()` on page load to complete sign-in.
56
+ * Open the Cleforyx hosted auth UI as a modal overlay on top of your page.
57
+ * Handles sign-in via email/password, Google, and magic link seamlessly.
76
58
  *
77
- * **This method never returns** it calls `window.location.href`.
59
+ * The modal closes automatically on successful sign-in and fires `onSuccess`.
78
60
  *
79
61
  * @example
80
- * // On button click:
81
- * await auth.signInWithGateway("google");
82
- *
83
- * // With a custom return URL:
84
- * await auth.signInWithGateway("google", {
85
- * redirectUrl: "https://myapp.cleforyx.com/dashboard",
62
+ * auth.openAuthUI({
63
+ * onSuccess: (result) => {
64
+ * console.log("Signed in:", result.user.email);
65
+ * navigate("/dashboard");
66
+ * },
86
67
  * });
87
68
  */
88
- signInWithGateway(provider: "google", options?: GatewaySignInOptions): Promise<never>;
69
+ openAuthUI(options?: OpenAuthUIOptions): void;
70
+ /** Close the auth modal programmatically if open. */
71
+ closeAuthUI(): void;
89
72
  /**
90
- * Check if the current URL contains a gateway callback (`?cfx_token=`).
91
- * Useful for rendering a "Finishing sign-in…" loading state before calling
92
- * `handleGatewayCallback()`.
73
+ * Redirect to the Cleforyx auth gateway for OAuth sign-in.
74
+ * Use `openAuthUI()` for a modal experience instead.
93
75
  *
94
76
  * @example
95
- * if (auth.isGatewayCallbackPending()) {
96
- * showLoadingSpinner();
97
- * }
77
+ * await auth.signInWithGateway("google");
98
78
  */
79
+ signInWithGateway(provider: "google", options?: GatewaySignInOptions): Promise<never>;
99
80
  isGatewayCallbackPending(): boolean;
100
81
  /**
101
- * Call this on every page load (ideally before rendering your app).
102
- * If the URL contains `?cfx_token=`, the token is validated against
103
- * the server, the session is saved, and the signed-in `AuthResult` is
104
- * returned. The query params are cleaned from the URL automatically.
105
- *
106
- * Returns `null` if there is no pending callback.
82
+ * Call on every page load to handle the gateway redirect callback.
83
+ * Detects `?cfx_token=` in the URL, validates it, and returns the signed-in user.
107
84
  *
108
85
  * @example
109
- * // In your app's root component or entry point:
110
86
  * useEffect(() => {
111
87
  * auth.handleGatewayCallback().then(result => {
112
- * if (result) {
113
- * console.log("Signed in:", result.user.email);
114
- * navigate("/dashboard");
115
- * }
88
+ * if (result) navigate("/dashboard");
116
89
  * });
117
90
  * }, []);
118
- *
119
- * // Or outside React:
120
- * const result = await auth.handleGatewayCallback();
121
- * if (result) showDashboard(result.user);
122
91
  */
123
92
  handleGatewayCallback(): Promise<AuthResult | null>;
124
- /** Sign out and clear the local session. */
125
93
  signOut(): Promise<void>;
126
- /** Re-fetch the current user's profile from the server and update the cache. */
127
94
  refreshCurrentUser(): Promise<AuthUser | null>;
128
- /**
129
- * Update the signed-in user's profile.
130
- *
131
- * @example
132
- * await auth.updateProfile({ displayName: "Bob", metadata: { theme: "dark" } });
133
- */
134
95
  updateProfile(updates: {
135
96
  displayName?: string;
136
97
  photoUrl?: string;
137
98
  metadata?: Record<string, unknown>;
138
99
  }): Promise<AuthUser>;
139
- /** Change password. All other sessions are invalidated server-side. */
140
100
  changePassword(currentPassword: string, newPassword: string): Promise<void>;
141
- /** Request a password-reset email. Safe to call even for unregistered addresses. */
142
101
  sendPasswordResetEmail(email: string): Promise<void>;
143
- /** Complete a password reset with the token from the reset email. */
144
102
  confirmPasswordReset(resetToken: string, newPassword: string): Promise<void>;
145
- /** Send an email verification code to the currently signed-in user. */
146
103
  sendEmailVerification(): Promise<void>;
147
- /** Verify email with the code the user received. */
148
104
  verifyEmail(code: string): Promise<AuthUser>;
149
- /**
150
- * Subscribe to auth state changes.
151
- * Fires immediately with the current user, then on every sign-in or sign-out.
152
- * Returns an unsubscribe function.
153
- *
154
- * @example
155
- * const unsubscribe = auth.onAuthStateChanged(user => {
156
- * if (user) console.log("signed in:", user.email);
157
- * else console.log("signed out");
158
- * });
159
- * unsubscribe();
160
- */
161
105
  onAuthStateChanged(callback: AuthStateCallback): () => void;
162
106
  }
163
107
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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"}
@@ -47,31 +47,34 @@ class SessionStore {
47
47
  catch { /* ignore */ }
48
48
  }
49
49
  }
50
- /**
51
- * Authentication service. Obtain via `getAuth(app)`.
52
- *
53
- * @example
54
- * const auth = getAuth(app);
55
- *
56
- * // Email / password
57
- * const { user } = await auth.signIn("alice@example.com", "pass");
58
- *
59
- * // Google — redirect to gateway, come back signed in
60
- * await auth.signInWithGateway("google");
61
- *
62
- * // On every page load, handle the gateway callback:
63
- * const result = await auth.handleGatewayCallback();
64
- * if (result) console.log("Signed in via gateway:", result.user.email);
65
- */
66
50
  class Auth {
67
- constructor(http, gatewayUrl, projectId) {
51
+ constructor(http, gatewayUrl, projectId, options) {
68
52
  this.http = http;
69
53
  this.store = new SessionStore();
70
54
  this.listeners = [];
55
+ this._modalCleanup = null;
71
56
  this.gatewayUrl = gatewayUrl.replace(/\/+$/, "");
72
57
  this.projectId = projectId;
58
+ this.apiEndpoint = options?.apiEndpoint ?? "external";
59
+ this.dbId = options?.dbId;
73
60
  this.store.load();
74
61
  }
62
+ /**
63
+ * Get the appropriate endpoint base path for API calls.
64
+ * - "public": /auth/public (for client-side flows)
65
+ * - "external": /auth (for server-side flows with API key)
66
+ * @internal
67
+ */
68
+ getEndpointPath(operation) {
69
+ if (this.apiEndpoint === "public") {
70
+ if (!this.dbId) {
71
+ throw new Error(`dbId is required for public routes. Pass it when initializing Auth with apiEndpoint: "public".`);
72
+ }
73
+ // For public routes, append dbId as query param and operation path
74
+ return `/public${operation}?dbId=${encodeURIComponent(this.dbId)}`;
75
+ }
76
+ return operation;
77
+ }
75
78
  // ── Internals ─────────────────────────────────────────────────────────────
76
79
  notify(user) {
77
80
  for (const cb of this.listeners) {
@@ -96,104 +99,185 @@ class Auth {
96
99
  return s ? { Authorization: `Bearer ${s.token}` } : {};
97
100
  }
98
101
  // ── Public API ────────────────────────────────────────────────────────────
99
- /** The currently signed-in user, or null. */
100
102
  get currentUser() { return this.store.load()?.user ?? null; }
101
- /** The raw bearer token for the active session, or null. */
102
103
  get currentToken() { return this.store.load()?.token ?? null; }
103
104
  // ── Email / password ──────────────────────────────────────────────────────
104
- /**
105
- * Create a new account with email + password.
106
- *
107
- * @example
108
- * const { user } = await auth.signUp("alice@example.com", "pass123", {
109
- * displayName: "Alice",
110
- * metadata: { role: "admin" },
111
- * });
112
- */
113
105
  async signUp(email, password, profile) {
114
106
  const result = await this.http.post("/signup", { email, password, ...profile });
115
107
  return this.handleResult(result);
116
108
  }
117
- /**
118
- * Sign in with email + password.
119
- *
120
- * @example
121
- * const { user, token } = await auth.signIn("alice@example.com", "pass");
122
- */
123
109
  async signIn(email, password) {
124
110
  const result = await this.http.post("/signin", { email, password });
125
111
  return this.handleResult(result);
126
112
  }
127
- // ── Gateway OAuth (redirect flow) ─────────────────────────────────────────
113
+ // ── Auth UI modal ─────────────────────────────────────────────────────────
128
114
  /**
129
- * Redirect the user to the Cleforyx auth gateway to sign in with an
130
- * OAuth provider (currently `"google"`).
115
+ * Open the Cleforyx hosted auth UI as a modal overlay on top of your page.
116
+ * Handles sign-in via email/password, Google, and magic link seamlessly.
131
117
  *
132
- * The gateway handles the OAuth flow and redirects back to your app with
133
- * `?cfx_token=<token>&cfx_project=<projectId>` in the URL.
134
- * Call `auth.handleGatewayCallback()` on page load to complete sign-in.
135
- *
136
- * **This method never returns** — it calls `window.location.href`.
118
+ * The modal closes automatically on successful sign-in and fires `onSuccess`.
137
119
  *
138
120
  * @example
139
- * // On button click:
140
- * await auth.signInWithGateway("google");
141
- *
142
- * // With a custom return URL:
143
- * await auth.signInWithGateway("google", {
144
- * redirectUrl: "https://myapp.cleforyx.com/dashboard",
121
+ * auth.openAuthUI({
122
+ * onSuccess: (result) => {
123
+ * console.log("Signed in:", result.user.email);
124
+ * navigate("/dashboard");
125
+ * },
145
126
  * });
146
127
  */
147
- async signInWithGateway(provider, options) {
128
+ openAuthUI(options = {}) {
148
129
  if (typeof window === "undefined") {
149
- throw new types_1.ClefbaseError("signInWithGateway() requires a browser environment.", "UNSUPPORTED_ENVIRONMENT");
130
+ throw new types_1.ClefbaseError("openAuthUI() requires a browser environment.", "UNSUPPORTED_ENVIRONMENT");
150
131
  }
151
- const redirectUrl = options?.redirectUrl ?? window.location.origin;
132
+ const { mode = "modal", onSuccess, onDismiss, redirectUrl } = options;
133
+ const currentUrl = window.location.href;
134
+ const returnUrl = redirectUrl ?? currentUrl;
152
135
  const params = new URLSearchParams({
153
136
  project: this.projectId,
154
- redirect: redirectUrl,
137
+ redirect: returnUrl,
138
+ embed: "popup",
155
139
  });
156
- window.location.href = `${this.gatewayUrl}/${provider}?${params.toString()}`;
157
- // This line is unreachable but TypeScript needs it for `never` return type
158
- return new Promise(() => { });
140
+ const authUrl = `${this.gatewayUrl}/login?${params}`;
141
+ if (mode === "redirect") {
142
+ window.location.href = authUrl;
143
+ return;
144
+ }
145
+ // ── Modal mode ──────────────────────────────────────────────────────────
146
+ // Inject styles once
147
+ if (!document.getElementById("cfx-modal-styles")) {
148
+ const style = document.createElement("style");
149
+ style.id = "cfx-modal-styles";
150
+ style.textContent = `
151
+ @keyframes cfxFadeIn { from { opacity: 0 } to { opacity: 1 } }
152
+ @keyframes cfxSlideUp { from { transform: translateY(20px); opacity: 0 } to { transform: none; opacity: 1 } }
153
+ #cfx-auth-overlay {
154
+ position: fixed; inset: 0; z-index: 999999;
155
+ background: rgba(15,23,42,0.6); backdrop-filter: blur(4px);
156
+ display: flex; align-items: center; justify-content: center;
157
+ animation: cfxFadeIn 0.18s ease;
158
+ padding: 16px; box-sizing: border-box;
159
+ }
160
+ #cfx-auth-card {
161
+ background: white; border-radius: 20px;
162
+ box-shadow: 0 24px 64px rgba(0,0,0,0.2);
163
+ overflow: hidden; width: 100%; max-width: 460px;
164
+ animation: cfxSlideUp 0.22s ease; position: relative;
165
+ }
166
+ #cfx-auth-frame {
167
+ width: 100%; height: 560px; border: none; display: block;
168
+ }
169
+ #cfx-auth-close {
170
+ position: absolute; top: 14px; right: 16px;
171
+ background: rgba(0,0,0,0.06); border: none;
172
+ width: 28px; height: 28px; border-radius: 50%;
173
+ font-size: 16px; line-height: 1; cursor: pointer;
174
+ display: flex; align-items: center; justify-content: center;
175
+ color: #64748b; z-index: 1; transition: background 0.15s;
176
+ }
177
+ #cfx-auth-close:hover { background: rgba(0,0,0,0.12); }
178
+ `;
179
+ document.head.appendChild(style);
180
+ }
181
+ // Create overlay
182
+ const overlay = document.createElement("div");
183
+ overlay.id = "cfx-auth-overlay";
184
+ const card = document.createElement("div");
185
+ card.id = "cfx-auth-card";
186
+ const closeBtn = document.createElement("button");
187
+ closeBtn.id = "cfx-auth-close";
188
+ closeBtn.innerHTML = "&#x2715;";
189
+ closeBtn.setAttribute("aria-label", "Close");
190
+ const iframe = document.createElement("iframe");
191
+ iframe.id = "cfx-auth-frame";
192
+ iframe.src = authUrl;
193
+ iframe.setAttribute("allow", "identity-credentials-get");
194
+ card.appendChild(closeBtn);
195
+ card.appendChild(iframe);
196
+ overlay.appendChild(card);
197
+ document.body.appendChild(overlay);
198
+ // Prevent body scroll
199
+ const prevOverflow = document.body.style.overflow;
200
+ document.body.style.overflow = "hidden";
201
+ const cleanup = () => {
202
+ overlay.remove();
203
+ document.body.style.overflow = prevOverflow;
204
+ window.removeEventListener("message", messageHandler);
205
+ this._modalCleanup = null;
206
+ };
207
+ const messageHandler = async (e) => {
208
+ if (e.data?.source !== "cleforyx-auth" || e.data?.type !== "auth_success")
209
+ return;
210
+ const { token, user: rawUser } = e.data;
211
+ if (!token)
212
+ return;
213
+ cleanup();
214
+ // Build a full AuthResult — fetch fresh user data to get expiresAt
215
+ // Use the public /me endpoint (no API key needed, just dbId + bearer)
216
+ try {
217
+ const serverUrl = this.http.getBaseUrl().replace(/\/auth$/, "");
218
+ const res = await fetch(`${serverUrl}/auth/public/me?dbId=${encodeURIComponent(this.projectId)}`, {
219
+ headers: { Authorization: `Bearer ${token}` },
220
+ });
221
+ const user = res.ok ? await res.json() : rawUser;
222
+ const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
223
+ const result = { user, token, session: { token, expiresAt } };
224
+ this.handleResult(result);
225
+ onSuccess?.(result);
226
+ }
227
+ catch {
228
+ // Fallback: use the user data from postMessage directly
229
+ const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
230
+ const result = { user: rawUser, token, session: { token, expiresAt } };
231
+ this.handleResult(result);
232
+ onSuccess?.(result);
233
+ }
234
+ };
235
+ closeBtn.onclick = () => { cleanup(); onDismiss?.(); };
236
+ overlay.addEventListener("click", (e) => {
237
+ if (e.target === overlay) {
238
+ cleanup();
239
+ onDismiss?.();
240
+ }
241
+ });
242
+ window.addEventListener("message", messageHandler);
243
+ this._modalCleanup = cleanup;
244
+ }
245
+ /** Close the auth modal programmatically if open. */
246
+ closeAuthUI() {
247
+ this._modalCleanup?.();
159
248
  }
249
+ // ── Gateway redirect flow ─────────────────────────────────────────────────
160
250
  /**
161
- * Check if the current URL contains a gateway callback (`?cfx_token=`).
162
- * Useful for rendering a "Finishing sign-in…" loading state before calling
163
- * `handleGatewayCallback()`.
251
+ * Redirect to the Cleforyx auth gateway for OAuth sign-in.
252
+ * Use `openAuthUI()` for a modal experience instead.
164
253
  *
165
254
  * @example
166
- * if (auth.isGatewayCallbackPending()) {
167
- * showLoadingSpinner();
168
- * }
255
+ * await auth.signInWithGateway("google");
169
256
  */
257
+ async signInWithGateway(provider, options) {
258
+ if (typeof window === "undefined") {
259
+ throw new types_1.ClefbaseError("signInWithGateway() requires a browser environment.", "UNSUPPORTED_ENVIRONMENT");
260
+ }
261
+ const redirectUrl = options?.redirectUrl ?? window.location.href;
262
+ const params = new URLSearchParams({ project: this.projectId, redirect: redirectUrl });
263
+ window.location.href = `${this.gatewayUrl}/google?${params}`;
264
+ return new Promise(() => { });
265
+ }
170
266
  isGatewayCallbackPending() {
171
267
  if (typeof window === "undefined")
172
268
  return false;
173
269
  return new URLSearchParams(window.location.search).has("cfx_token");
174
270
  }
175
271
  /**
176
- * Call this on every page load (ideally before rendering your app).
177
- * If the URL contains `?cfx_token=`, the token is validated against
178
- * the server, the session is saved, and the signed-in `AuthResult` is
179
- * returned. The query params are cleaned from the URL automatically.
180
- *
181
- * Returns `null` if there is no pending callback.
272
+ * Call on every page load to handle the gateway redirect callback.
273
+ * Detects `?cfx_token=` in the URL, validates it, and returns the signed-in user.
182
274
  *
183
275
  * @example
184
- * // In your app's root component or entry point:
185
276
  * useEffect(() => {
186
277
  * auth.handleGatewayCallback().then(result => {
187
- * if (result) {
188
- * console.log("Signed in:", result.user.email);
189
- * navigate("/dashboard");
190
- * }
278
+ * if (result) navigate("/dashboard");
191
279
  * });
192
280
  * }, []);
193
- *
194
- * // Or outside React:
195
- * const result = await auth.handleGatewayCallback();
196
- * if (result) showDashboard(result.user);
197
281
  */
198
282
  async handleGatewayCallback() {
199
283
  if (typeof window === "undefined")
@@ -202,33 +286,25 @@ class Auth {
202
286
  const token = params.get("cfx_token");
203
287
  if (!token)
204
288
  return null;
205
- // Clean the token + project params from the URL immediately so they
206
- // aren't reprocessed on refresh or shared in links
289
+ // Clean URL
207
290
  params.delete("cfx_token");
208
291
  params.delete("cfx_project");
209
- const cleanSearch = params.toString();
210
- const cleanUrl = window.location.pathname + (cleanSearch ? `?${cleanSearch}` : "");
211
- window.history.replaceState({}, "", cleanUrl);
212
- // Validate the token against the server and fetch the full user object
292
+ window.history.replaceState({}, "", window.location.pathname + (params.toString() ? `?${params}` : ""));
293
+ // Use /auth/public/me no API key needed, just dbId + bearer
294
+ const serverUrl = this.http.getBaseUrl().replace(/\/auth$/, "");
213
295
  try {
214
- const user = await this.http.get("/me", { Authorization: `Bearer ${token}` });
215
- // We don't have an expiresAt from the URL — fetch it from /me is enough
216
- // for the session; use a generous 30-day default matching server config
296
+ const res = await fetch(`${serverUrl}/auth/public/me?dbId=${encodeURIComponent(this.projectId)}`, { headers: { Authorization: `Bearer ${token}` } });
297
+ if (!res.ok)
298
+ throw new Error(`${res.status} ${res.statusText}`);
299
+ const user = await res.json();
217
300
  const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
218
- const result = {
219
- user,
220
- token,
221
- session: { token, expiresAt },
222
- };
223
- return this.handleResult(result);
301
+ return this.handleResult({ user, token, session: { token, expiresAt } });
224
302
  }
225
303
  catch (err) {
226
- // Token was invalid or expired — don't sign in
227
304
  throw new types_1.ClefbaseError(`Gateway callback failed: ${err.message}`, "GATEWAY_CALLBACK_ERROR");
228
305
  }
229
306
  }
230
307
  // ── Session management ────────────────────────────────────────────────────
231
- /** Sign out and clear the local session. */
232
308
  async signOut() {
233
309
  const token = this.currentToken;
234
310
  if (token) {
@@ -240,7 +316,6 @@ class Auth {
240
316
  this.store.clear();
241
317
  this.notify(null);
242
318
  }
243
- /** Re-fetch the current user's profile from the server and update the cache. */
244
319
  async refreshCurrentUser() {
245
320
  const token = this.currentToken;
246
321
  if (!token)
@@ -257,72 +332,45 @@ class Auth {
257
332
  return null;
258
333
  }
259
334
  }
260
- /**
261
- * Update the signed-in user's profile.
262
- *
263
- * @example
264
- * await auth.updateProfile({ displayName: "Bob", metadata: { theme: "dark" } });
265
- */
266
335
  async updateProfile(updates) {
267
336
  const token = this.currentToken;
268
337
  if (!token)
269
338
  throw new Error("No user is currently signed in.");
270
- const user = await this.http.patch("/me", updates, {
271
- Authorization: `Bearer ${token}`,
272
- });
339
+ const user = await this.http.patch("/me", updates, { Authorization: `Bearer ${token}` });
273
340
  const session = this.store.load();
274
341
  if (session)
275
342
  this.store.save({ ...session, user });
276
343
  this.notify(user);
277
344
  return user;
278
345
  }
279
- /** Change password. All other sessions are invalidated server-side. */
280
346
  async changePassword(currentPassword, newPassword) {
281
347
  const token = this.currentToken;
282
348
  if (!token)
283
349
  throw new Error("No user is currently signed in.");
284
350
  await this.http.post("/change-password", { currentPassword, newPassword }, { Authorization: `Bearer ${token}` });
285
351
  }
286
- /** Request a password-reset email. Safe to call even for unregistered addresses. */
287
352
  async sendPasswordResetEmail(email) {
288
353
  await this.http.post("/forgot-password", { email });
289
354
  }
290
- /** Complete a password reset with the token from the reset email. */
291
355
  async confirmPasswordReset(resetToken, newPassword) {
292
356
  await this.http.post("/reset-password", { token: resetToken, newPassword });
293
357
  }
294
- /** Send an email verification code to the currently signed-in user. */
295
358
  async sendEmailVerification() {
296
359
  const token = this.currentToken;
297
360
  if (!token)
298
361
  throw new Error("No user is currently signed in.");
299
362
  await this.http.post("/send-verification", undefined, { Authorization: `Bearer ${token}` });
300
363
  }
301
- /** Verify email with the code the user received. */
302
364
  async verifyEmail(code) {
303
365
  return this.http.post("/verify-email", { code });
304
366
  }
305
- /**
306
- * Subscribe to auth state changes.
307
- * Fires immediately with the current user, then on every sign-in or sign-out.
308
- * Returns an unsubscribe function.
309
- *
310
- * @example
311
- * const unsubscribe = auth.onAuthStateChanged(user => {
312
- * if (user) console.log("signed in:", user.email);
313
- * else console.log("signed out");
314
- * });
315
- * unsubscribe();
316
- */
317
367
  onAuthStateChanged(callback) {
318
368
  this.listeners.push(callback);
319
369
  try {
320
370
  callback(this.currentUser);
321
371
  }
322
372
  catch { /* ignore */ }
323
- return () => {
324
- this.listeners = this.listeners.filter(cb => cb !== callback);
325
- };
373
+ return () => { this.listeners = this.listeners.filter(cb => cb !== callback); };
326
374
  }
327
375
  }
328
376
  exports.Auth = Auth;