Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/young-cats-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/shared': minor
'@clerk/types': minor
---

[Experimental] Signals: Add support for calling `signIn.password()` without an identifier.
27 changes: 25 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,12 +503,18 @@ class SignInFuture implements SignInFutureResource {
submitPassword: this.submitResetPassword.bind(this),
};

fetchStatus: 'idle' | 'fetching' = 'idle';

Comment on lines +506 to +507
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

fetchStatus is never updated; wire it to network calls.

The property is added but never transitions to 'fetching'/'idle', limiting its usefulness as an observable.

As a pattern, set to 'fetching' before a request and back to 'idle' in finally blocks. See the password/finalize suggestions below for concrete diffs; apply similarly across other methods in SignInFuture.

🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 506-507, the
fetchStatus property is initialized but never updated; update SignInFuture
methods that perform network requests to set this.fetchStatus = 'fetching'
immediately before the async call and reset this.fetchStatus = 'idle' in a
finally block after the call (and also in error branches where applicable).
Apply this pattern to all SignInFuture methods that call the network (e.g.,
start, attempt, finalize, password/finalize variants), ensuring the status is
set to 'fetching' before awaiting the request and always cleared to 'idle' in
finally so observers reliably see transitions.

constructor(readonly resource: SignIn) {}

get status() {
return this.resource.status;
}

get availableStrategies() {
return this.resource.supportedFirstFactors ?? [];
}

async sendResetPasswordEmailCode(): Promise<{ error: unknown }> {
eventBus.emit('resource:error', { resource: this.resource, error: null });
try {
Expand Down Expand Up @@ -593,12 +599,13 @@ class SignInFuture implements SignInFutureResource {
}
}

async password({ identifier, password }: { identifier: string; password: string }): Promise<{ error: unknown }> {
async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
eventBus.emit('resource:error', { resource: this.resource, error: null });
const previousIdentifier = this.resource.identifier;
try {
await this.resource.__internal_basePost({
path: this.resource.pathRoot,
body: { identifier, password },
body: { identifier: identifier || previousIdentifier, password },
});
Comment on lines +602 to 609
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Bug: password() misses strategy and wrong endpoint; add identifier guard and use attempt_first_factor when applicable.

Posting to /client/sign_ins without action and without strategy: 'password' will likely be treated as a create-with-identifier request and ignore the password. Also, when neither an explicit nor prior identifier exists, fail fast with a clear error. Finally, use fetchStatus to reflect request lifecycle.

Apply this diff:

-  async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
-    eventBus.emit('resource:error', { resource: this.resource, error: null });
-    const previousIdentifier = this.resource.identifier;
-    try {
-      await this.resource.__internal_basePost({
-        path: this.resource.pathRoot,
-        body: { identifier: identifier || previousIdentifier, password },
-      });
-    } catch (err: unknown) {
-      eventBus.emit('resource:error', { resource: this.resource, error: err });
-      return { error: err };
-    }
-
-    return { error: null };
-  }
+  async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
+    eventBus.emit('resource:error', { resource: this.resource, error: null });
+    this.fetchStatus = 'fetching';
+    const previousIdentifier = this.resource.identifier;
+    const idToUse = identifier ?? previousIdentifier ?? undefined;
+    try {
+      if (!this.resource.id) {
+        if (!idToUse) {
+          throw new Error('Cannot attempt password without an identifier. Call create() first or provide one.');
+        }
+        // Create + password in one call
+        await this.resource.__internal_basePost({
+          path: this.resource.pathRoot,
+          body: { strategy: 'password', identifier: idToUse, password },
+        });
+      } else {
+        // Attempt first factor on existing sign-in
+        await this.resource.__internal_basePost({
+          body: { strategy: 'password', password },
+          action: 'attempt_first_factor',
+        });
+      }
+    } catch (err: unknown) {
+      eventBus.emit('resource:error', { resource: this.resource, error: err });
+      return { error: err };
+    } finally {
+      this.fetchStatus = 'idle';
+    }
+
+    return { error: null };
+  }

Additionally, consider a unit/integration test to cover:

  • password() with explicit identifier (no prior create)
  • password() without identifier after a prior create()
  • password() without identifier and without prior create() should return { error }

🏁 Script executed:

#!/bin/bash
set -e
# Show the current password() implementation with surrounding context
rg -n 'async password' -C10 packages/clerk-js/src/core/resources/SignIn.ts

Length of output: 952


🏁 Script executed:

#!/bin/bash
set -e
# Search for other uses of `strategy` in SignIn.ts
echo "=== strategy occurrences ==="
rg -n 'strategy' -C3 packages/clerk-js/src/core/resources/SignIn.ts || true

# Search for `action` parameter usages in SignIn.ts
echo "=== action occurrences ==="
rg -n 'action:' -C3 packages/clerk-js/src/core/resources/SignIn.ts || true

Length of output: 9787


🏁 Script executed:

#!/bin/bash
set -e
# Find definition of __internal_basePost across the repo
rg -n 'function __internal_basePost' -C3
rg -n '__internal_basePost' -C3

Length of output: 51


🏁 Script executed:

#!/bin/bash
set -e
# Search for fetchStatus in SignIn.ts
rg -n 'fetchStatus' -C3 packages/clerk-js/src/core/resources/SignIn.ts || true

# Search for fetchStatus in the entire repo
rg -n 'fetchStatus' -C3 .

Length of output: 23302


🏁 Script executed:

#!/bin/bash
set -e
# Show first 50 lines of SignIn.ts to inspect class context
sed -n '1,50p' packages/clerk-js/src/core/resources/SignIn.ts

Length of output: 1430


Implement proper strategy, action, identifier guard, and fetchStatus in password()

  • Set fetchStatus to "fetching" at the start and reset to "idle" in a finally block.
  • Throw a clear error if neither an explicit identifier nor an existing session ID is available.
  • Include strategy: 'password' in the request body.
  • Use action: 'attempt_first_factor' when a session ID already exists.
  • (Optional) specify path: this.resource.pathRoot for the initial create.
  • Add tests for:
    • Explicit identifier without prior create()
    • Implicit identifier after a prior create()
    • Missing identifier before any create()

Diff:

--- a/packages/clerk-js/src/core/resources/SignIn.ts
+++ b/packages/clerk-js/src/core/resources/SignIn.ts
@@ -602,14 +602,38 @@
   async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
     eventBus.emit('resource:error', { resource: this.resource, error: null });
+    this.fetchStatus = 'fetching';
     const previousIdentifier = this.resource.identifier;
-    try {
-      await this.resource.__internal_basePost({
-        path: this.resource.pathRoot,
-        body: { identifier: identifier || previousIdentifier, password },
-      });
-    } catch (err: unknown) {
-      eventBus.emit('resource:error', { resource: this.resource, error: err });
-      return { error: err };
-    }
-
-    return { error: null };
+    const idToUse = identifier ?? previousIdentifier;
+    try {
+      if (!this.resource.id) {
+        if (!idToUse) {
+          throw new Error('Cannot attempt password without an identifier. Call create() first or provide one.');
+        }
+        // Create + password in one call
+        await this.resource.__internal_basePost({
+          path: this.resource.pathRoot,
+          body: { strategy: 'password', identifier: idToUse, password },
+        });
+      } else {
+        // Attempt first factor on existing sign-in
+        await this.resource.__internal_basePost({
+          body: { strategy: 'password', password },
+          action: 'attempt_first_factor',
+        });
+      }
+    } catch (err: unknown) {
+      eventBus.emit('resource:error', { resource: this.resource, error: err });
+      return { error: err };
+    } finally {
+      this.fetchStatus = 'idle';
+    }
+
+    return { error: null };
+  }

Add corresponding unit/integration tests to cover all scenarios.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
eventBus.emit('resource:error', { resource: this.resource, error: null });
const previousIdentifier = this.resource.identifier;
try {
await this.resource.__internal_basePost({
path: this.resource.pathRoot,
body: { identifier, password },
body: { identifier: identifier || previousIdentifier, password },
});
async password({ identifier, password }: { identifier?: string; password: string }): Promise<{ error: unknown }> {
eventBus.emit('resource:error', { resource: this.resource, error: null });
this.fetchStatus = 'fetching';
const previousIdentifier = this.resource.identifier;
const idToUse = identifier ?? previousIdentifier;
try {
if (!this.resource.id) {
if (!idToUse) {
throw new Error(
'Cannot attempt password without an identifier. Call create() first or provide one.'
);
}
// Create + password in one call
await this.resource.__internal_basePost({
path: this.resource.pathRoot,
body: { strategy: 'password', identifier: idToUse, password },
});
} else {
// Attempt first factor on existing sign-in
await this.resource.__internal_basePost({
body: { strategy: 'password', password },
action: 'attempt_first_factor',
});
}
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
} finally {
this.fetchStatus = 'idle';
}
return { error: null };
}
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 602 to 609, set
this.resource.fetchStatus = "fetching" at the start of password() and ensure you
reset it to "idle" in a finally block; validate that either the explicit
identifier param or this.resource.identifier (session id/previous identifier)
exists and throw a clear error if neither is available; include strategy:
"password" in the request body and, when a previous identifier/session id
exists, add action: "attempt_first_factor" to the body (otherwise omit action
for initial create), and keep path: this.resource.pathRoot for the post; update
tests to cover explicit identifier without prior create(), implicit identifier
after prior create(), and missing identifier before any create().

} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
Expand Down Expand Up @@ -688,4 +695,20 @@ class SignInFuture implements SignInFutureResource {

return { error: null };
}

async finalize(): Promise<{ error: unknown }> {
eventBus.emit('resource:error', { resource: this.resource, error: null });
try {
if (!this.resource.createdSessionId) {
throw new Error('Cannot finalize sign-in without a created session.');
}

await SignIn.clerk.setActive({ session: this.resource.createdSessionId });
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}

return { error: null };
}
}
2 changes: 1 addition & 1 deletion packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function isKnownError(error: any): error is ClerkAPIResponseError | Clerk
* @internal
*/
export function isClerkAPIResponseError(err: any): err is ClerkAPIResponseError {
return 'clerkError' in err;
return err && 'clerkError' in err;
}
Comment on lines 84 to 86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard all type-narrowing helpers against falsy inputs to prevent runtime TypeErrors.

Good fix on isClerkAPIResponseError. However, isKnownError can still throw via isMetamaskError / isClerkRuntimeError when passed null/undefined, because the in operator on a non-object throws. Harden these as well.

Apply this diff:

 export function isKnownError(error: any): error is ClerkAPIResponseError | ClerkRuntimeError | MetamaskError {
-  return isClerkAPIResponseError(error) || isMetamaskError(error) || isClerkRuntimeError(error);
+  // Guard against falsy input to avoid TypeError from `in` operator in downstream checks
+  if (!error) {
+    return false;
+  }
+  return isClerkAPIResponseError(error) || isMetamaskError(error) || isClerkRuntimeError(error);
 }
 
 export function isClerkAPIResponseError(err: any): err is ClerkAPIResponseError {
-  return err && 'clerkError' in err;
+  return !!err && typeof err === 'object' && 'clerkError' in err;
 }
 
 export function isClerkRuntimeError(err: any): err is ClerkRuntimeError {
-  return 'clerkRuntimeError' in err;
+  return !!err && typeof err === 'object' && 'clerkRuntimeError' in err;
 }
 
 export function isMetamaskError(err: any): err is MetamaskError {
-  return 'code' in err && [4001, 32602, 32603].includes(err.code) && 'message' in err;
+  return (
+    !!err &&
+    typeof err === 'object' &&
+    'code' in err &&
+    [4001, 32602, 32603].includes((err as any).code) &&
+    'message' in err
+  );
 }

Also applies to: 105-106, 123-124, 75-77

🤖 Prompt for AI Agents
In packages/shared/src/error.ts around lines 75-77, 84-86, 105-106 and 123-124,
the type-narrowing helpers (isMetamaskError, isClerkAPIResponseError,
isClerkRuntimeError and isKnownError) use the `in` operator without first
checking for falsy/non-object inputs which can throw; update each helper to
first confirm the value is truthy and typeof value === 'object' (or use value !=
null) before using `in` or other object checks, and adjust isKnownError to call
the hardened helpers safely so it never calls `in` on null/undefined.


/**
Expand Down
12 changes: 10 additions & 2 deletions packages/types/src/signIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,16 @@ export interface SignInResource extends ClerkResource {
}

export interface SignInFutureResource {
fetchStatus: 'idle' | 'fetching';
availableStrategies: SignInFirstFactor[];
status: SignInStatus | null;
Comment on lines +129 to 131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add JSDoc for new public APIs (fetchStatus, availableStrategies, password identifier fallback, finalize).

Ensure discoverability and clarity for consumers.

Apply this diff to document the additions:

 export interface SignInFutureResource {
-  fetchStatus: 'idle' | 'fetching';
-  availableStrategies: SignInFirstFactor[];
+  /**
+   * Reflects ongoing network activity for the future resource.
+   * 'fetching' while an API call is in flight, otherwise 'idle'.
+   * @experimental
+   */
+  fetchStatus: 'idle' | 'fetching';
+  /**
+   * Convenience mirror of SignIn.supportedFirstFactors. Always an array.
+   * @experimental
+   */
+  availableStrategies: SignInFirstFactor[];
   status: SignInStatus | null;
-  create: (params: { identifier: string }) => Promise<{ error: unknown }>;
-  password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>;
+  /**
+   * Attempts password auth. When identifier is omitted, falls back to the identifier
+   * set during a previous create() call.
+   * @experimental
+   */
+  password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>;
   ...
-  finalize: () => Promise<{ error: unknown }>;
+  /**
+   * Activates the created session (SignIn.createdSessionId) via Clerk.setActive.
+   * Returns `{ error: null }` on success or `{ error }` on failure.
+   * @experimental
+   */
+  finalize: () => Promise<{ error: unknown }>;
 }

Also applies to: 149-150, 133-134

🤖 Prompt for AI Agents
In packages/types/src/signIn.ts around lines 129-131 (also apply to lines
133-134 and 149-150), the new public API surface (fetchStatus,
availableStrategies, the password identifier fallback, and finalize) lacks JSDoc
comments; add clear JSDoc blocks above each exported property/function
describing its purpose, type/possible values (e.g., fetchStatus:
'idle'|'fetching'; availableStrategies: SignInFirstFactor[]), when it is
set/used, any fallback behavior for the password identifier, expected return
values/errors for finalize, and example usage/notes on stability so consumers
can discover and understand these members. Ensure each JSDoc includes a one-line
summary, @returns or @type where appropriate, and any relevant @default or
@remarks details.

create: (params: { identifier: string }) => Promise<{ error: unknown }>;
password: (params: { identifier: string; password: string }) => Promise<{ error: unknown }>;
create: (params: {
identifier?: string;
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
redirectUrl?: string;
actionCompleteRedirectUrl?: string;
}) => Promise<{ error: unknown }>;
Comment on lines +132 to +137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Document the expanded create() params and verify redirect param naming consistency.

The widened signature looks correct; please add JSDoc describing each param and its behavior. Additionally, the naming differs from sso() which uses redirectUrlComplete while create() uses actionCompleteRedirectUrl. If this is intentional, document the distinction; otherwise consider aligning or supporting both (runtime-dependent).

Apply this diff to document the params:

   status: SignInStatus | null;
-  create: (params: {
+  /**
+   * Starts a sign-in with optional identifier and/or federated SSO strategy.
+   * When `strategy` is provided (OAuth/SAML/enterprise SSO), `redirectUrl` and
+   * optionally `actionCompleteRedirectUrl` control the return locations.
+   * `identifier` is optional here to support flows that defer it to a later step.
+   * @experimental
+   */
+  create: (params: {
     identifier?: string;
     strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
     redirectUrl?: string;
     actionCompleteRedirectUrl?: string;
   }) => Promise<{ error: unknown }>;

Run this to check for naming consistency across the repo:


🏁 Script executed:

#!/bin/bash
# Find occurrences of these redirect params to confirm intent and consistency.
rg -n -S 'redirectUrlComplete|actionCompleteRedirectUrl' -A2 -B2

Length of output: 50926


Document create() params and unify redirect param naming

Please add JSDoc to the new create() signature in packages/types/src/signIn.ts and resolve the discrepancy between its actionCompleteRedirectUrl and the rest of the APIs’ redirectUrlComplete. Either rename actionCompleteRedirectUrl to redirectUrlComplete (or alias it), or clearly document why this method diverges.

• File: packages/types/src/signIn.ts (lines 132–137)
• Compare with sso() at lines 150–152 (uses redirectUrlComplete)

Proposed diff for JSDoc and aliasing:

   status: SignInStatus | null;
+ /**
+  * Starts a sign-in flow, optionally with an identifier or federated SSO strategy.
+  * @param params.identifier  Optional user identifier for credentialed sign-in.
+  * @param params.strategy    OAuth/SAML/enterprise SSO strategy.
+  * @param params.redirectUrl URL to return to after initiating the flow.
+  * @param params.redirectUrlComplete
+  *   Full URL or path to navigate to once sign-in completes. Alias for
+  *   `actionCompleteRedirectUrl` to align with other APIs.
+  * @experimental
+  */
   create: (params: {
     identifier?: string;
     strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
     redirectUrl?: string;
-    actionCompleteRedirectUrl?: string;
+    actionCompleteRedirectUrl?: string;
+    redirectUrlComplete?: string;
   }) => Promise<{ error: unknown }>;

Additionally, review and update the same pattern in:

  • packages/types/src/signUp.ts
  • packages/types/src/factors.ts
  • Any other user-facing methods that accept completion URLs
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
create: (params: {
identifier?: string;
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
redirectUrl?: string;
actionCompleteRedirectUrl?: string;
}) => Promise<{ error: unknown }>;
status: SignInStatus | null;
/**
* Starts a sign-in flow, optionally with an identifier or federated SSO strategy.
* @param params.identifier Optional user identifier for credentialed sign-in.
* @param params.strategy OAuth/SAML/enterprise SSO strategy.
* @param params.redirectUrl URL to return to after initiating the flow.
* @param params.redirectUrlComplete Full URL or path to navigate to once sign-in completes.
* Alias for `actionCompleteRedirectUrl` to align with other APIs.
* @experimental
*/
create: (params: {
identifier?: string;
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
redirectUrl?: string;
actionCompleteRedirectUrl?: string;
redirectUrlComplete?: string;
}) => Promise<{ error: unknown }>;
🤖 Prompt for AI Agents
In packages/types/src/signIn.ts around lines 132 to 137, the create() signature
lacks JSDoc and uses actionCompleteRedirectUrl which is inconsistent with other
APIs that use redirectUrlComplete (see sso() at lines 150–152); add a concise
JSDoc block describing each param and the returned Promise, and rename the param
to redirectUrlComplete (or add redirectUrlComplete as an alias that maps to the
existing actionCompleteRedirectUrl) so naming is consistent; apply the same
JSDoc + rename/alias pattern to packages/types/src/signUp.ts,
packages/types/src/factors.ts, and any other user-facing methods that accept
completion/redirect URLs to keep the public API consistent.

password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>;
emailCode: {
sendCode: (params: { email: string }) => Promise<{ error: unknown }>;
verifyCode: (params: { code: string }) => Promise<{ error: unknown }>;
Expand All @@ -144,6 +151,7 @@ export interface SignInFutureResource {
redirectUrl: string;
redirectUrlComplete: string;
}) => Promise<{ error: unknown }>;
finalize: () => Promise<{ error: unknown }>;
}

export type SignInStatus =
Expand Down
Loading