Repository: codemonger-io/passquito
Pull request #5: Introduce Passquito client
Author: login: kikuomax
Created: 2025-04-27T04:09:45Z | Merged: 2025-04-28T18:16:05Z

### Proposed changes

`passquito.ts` no longer holds a reference to the global instance of `CredentialsApi`, and an instance of `CredentialsApi` has to be passed around through registration and authentication ceremonies.

Introduces a new class `PassquitoClient`, and moves all the core implementations of registration and authentication of passkeys to it. The motivation of the introduction of `PassquitoClient` is that passing around `CredentialsApi` felt tedious.

Also introduces the Pinia stores for `CredentialsApi` and `PassquitoClient`. Views use these stores to conduct registration and authentication ceremonies.

Removes `refreshTokens` because it is just an alias to `CredentialsApi.refreshTokens`.

### Related issues

na

## Summary by Sourcery

Refactor passkey handling by introducing a `PassquitoClient` class to manage registration and authentication ceremonies, decoupling it from direct API interactions. Introduce Pinia stores for managing client and API instances.

Enhancements:
- Create `PassquitoClient` class to encapsulate passkey registration and authentication logic.
- Create `CredentialsApiImpl` class for interacting with the backend credentials API.
- Update views to use the new client and API instances via Pinia stores.
- Reorganize passkey-related types into `passquito-types.ts`.

Chores:
- Add Pinia stores (`useCredentialsApiStore`, `usePassquitoClientStore`) to provide client and API instances.
- Remove the previous global `CredentialsApi` object and ceremony functions from `passquito.ts`.
- Remove the redundant `refreshTokens` function.
- Delete the unused `credential.ts` store file.


Diff:
diff --git a/app/src/stores/credential.ts b/app/src/stores/credential.ts
index 561a3db..6657121 100644
--- a/app/src/stores/credential.ts
+++ b/app/src/stores/credential.ts
@@ -2,8 +2,8 @@ import { StorageSerializers, useStorage } from '@vueuse/core';
 import { defineStore } from 'pinia';
 import { computed, ref } from 'vue';
 
-import { isCognitoTokens, isPublicKeyInfo } from '../utils/passquito';
-import type { CognitoTokens, PublicKeyInfo } from '../utils/passquito';
+import { isCognitoTokens, isPublicKeyInfo } from '../utils/passquito-types';
+import type { CognitoTokens, PublicKeyInfo } from '../utils/passquito-types';
 import { makeValidatingSerializer } from '../utils/serializer';
 
 // possible states of the credential.
diff --git a/app/src/stores/credentials-api.ts b/app/src/stores/credentials-api.ts
new file mode 100644
index 0000000..f90efcf
--- /dev/null
+++ b/app/src/stores/credentials-api.ts
@@ -0,0 +1,14 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+import { credentialsApiUrl } from '../auth-config';
+import { CredentialsApiImpl } from '../utils/credentials-api-impl';
+
+export const useCredentialsApiStore = defineStore('credentials-api', () => {
+  // credentials API
+  const api = ref(new CredentialsApiImpl(credentialsApiUrl));
+
+  return {
+    api,
+  };
+});
diff --git a/app/src/stores/passquito-client.ts b/app/src/stores/passquito-client.ts
new file mode 100644
index 0000000..a1a756f
--- /dev/null
+++ b/app/src/stores/passquito-client.ts
@@ -0,0 +1,18 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+import { PassquitoClient } from '../utils/passquito-client';
+
+import { useCredentialsApiStore } from './credentials-api';
+
+export const usePassquitoClientStore = defineStore('passquito-client', () => {
+  // Credentials API access
+  const credentialsApiStore = useCredentialsApiStore();
+
+  // Passquito client
+  const client = ref(new PassquitoClient(credentialsApiStore.api));
+
+  return {
+    client,
+  };
+});
diff --git a/app/src/utils/credentials-api-impl.ts b/app/src/utils/credentials-api-impl.ts
new file mode 100644
index 0000000..4c6f4ad
--- /dev/null
+++ b/app/src/utils/credentials-api-impl.ts
@@ -0,0 +1,191 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON ,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+// raw Cognito tokens returned from the API
+type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
+
+/**
+ * Default implementation of {@link CredentialsApi}.
+ *
+ * @beta
+ */
+export class CredentialsApiImpl implements CredentialsApi {
+  /** Base URL of the Credentials API. */
+  readonly baseUrl: string;
+
+  /** Initializes with a given base URL of the Credentials API. */
+  constructor(baseUrl: string) {
+    // removes a trailing slash if it exists
+    this.baseUrl = baseUrl.replace(/\/$/, '');
+  }
+
+  async startRegistration(userInfo: UserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(userInfo),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
+    };
+  }
+
+  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start-verified`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': userInfo.idToken,
+      },
+      body: JSON.stringify(userInfo.userInfo),
+    });
+    return await res.json();
+  }
+
+  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
+    const endpoint = `${this.baseUrl}/registration/finish`;
+    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        publicKeyCredential: encodedCredential,
+      }),
+    });
+    if (!res.ok) {
+      throw new Error(
+        `credential registration failed with ${res.status}: ${await res.text()}`,
+      );
+    }
+  }
+
+  async getDiscoverableCredentialRequestOptions() {
+    const endpoint = `${this.baseUrl}/authentication/discover`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+    });
+    return getRequestFromJSON(await res.json());
+  }
+
+  async startAuthentication(userId: string) {
+    const res = await fetch(`${this.baseUrl}/authentication/start`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ userId }),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
+    };
+  }
+
+  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const res = await fetch(`${this.baseUrl}/authentication/finish`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        userId,
+        publicKey: encodedCredential,
+      }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      throw new Error('invalid Cognito tokens');
+    }
+    return activateCognitoTokens(tokens);
+  }
+
+  async refreshTokens(refreshToken: string) {
+    const endpoint = `${this.baseUrl}/authentication/refresh`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ refreshToken }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      return undefined;
+    }
+    return activateCognitoTokens(tokens);
+  }
+}
+
+// encodes `PublicKeyCredential` for a registration API request body.
+//
+// replaces `ArrayBuffer`s with Base64-URL-encoded strings.
+function encodePublicKeyCredentialForCreation(
+  publicKey: PublicKeyCredential,
+): PublicKeyCredentialWithAttestationJSON {
+  return convert(
+    bufferToBase64url,
+    schema.publicKeyCredentialWithAttestation,
+    publicKey,
+  );
+}
+
+// encodes `PublicKeyCredential` for an authentication API request body.
+//
+// replaces `ArrayBuffer`s with Base64-URL-encoded strings.
+function encodePublicKeyCredentialForAuthentication(
+  publicKey: PublicKeyCredential,
+): PublicKeyCredentialWithAssertionJSON {
+  return convert(
+    bufferToBase64url,
+    schema.publicKeyCredentialWithAssertion,
+    publicKey,
+  );
+}
+
+// returns if a given value is a `RawCognitoTokens`.
+function isRawCognitoTokens(value: unknown): value is RawCognitoTokens {
+  if (value == null || typeof value !== 'object') {
+    return false;
+  }
+  const maybeTokens = value as RawCognitoTokens;
+  return typeof maybeTokens.idToken === 'string' &&
+    typeof maybeTokens.accessToken === 'string' &&
+    typeof maybeTokens.refreshToken === 'string' &&
+    typeof maybeTokens.expiresIn === 'number';
+}
+
+// activates a given `RawCognitoTokens` and obtains a `CognitoTokens`.
+//
+// you should call this function as soon as you obtain Cognito tokens.
+function activateCognitoTokens(tokens: RawCognitoTokens): CognitoTokens {
+  return {
+    ...tokens,
+    activatedAt: Date.now(),
+  };
+}
diff --git a/app/src/utils/passquito-client.ts b/app/src/utils/passquito-client.ts
new file mode 100644
index 0000000..de236af
--- /dev/null
+++ b/app/src/utils/passquito-client.ts
@@ -0,0 +1,200 @@
+import { bufferToBase64url } from '@github/webauthn-json/extended';
+
+import type { CredentialsApi, RegistrationSession } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
+export interface Credentials {
+  publicKeyInfo: PublicKeyInfo;
+  tokens: CognitoTokens;
+}
+
+/**
+ * Passquito client.
+ *
+ * @beta
+ */
+export class PassquitoClient {
+  /**
+   * Initializes with a given {@link CredentialsApi} instance.
+   *
+   * @param credentialsApi - Credentials API access.
+   */
+  constructor(private readonly credentialsApi: CredentialsApi) {}
+
+  /**
+   * Conducts a registration ceremony.
+   *
+   * @remarks
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremony(userInfo: UserInfo) {
+    await this.runRegistrationSession(
+      await this.credentialsApi.startRegistration(userInfo),
+    );
+  }
+
+  /**
+   * Conducts a registration ceremony for a verified user.
+   *
+   * @remarks
+   *
+   * This ceremony is for a user who has been authenticated through a cross-device
+   * authentication and wants to register a new device (credential).
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
+    await this.runRegistrationSession(
+      await this.credentialsApi.startRegistrationForVerifiedUser(userInfo),
+    );
+  }
+
+  // runs a given registration session.
+  private async runRegistrationSession(session: RegistrationSession) {
+    const credential = await navigator.credentials.create(session.credentialCreationOptions);
+    if (credential == null) {
+      throw new Error('failed to create a new credential');
+    }
+    await this.credentialsApi.finishRegistration(
+      session.sessionId,
+      credential as PublicKeyCredential,
+    );
+  }
+
+  /**
+   * Conducts an authentication ceremony.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a function to abort the
+   * ceremony and a `Promise` of the credentials.
+   *
+   * Reference:
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion>
+   */
+  doAuthenticationCeremony() {
+    return runAbortableAuthentication((abortController) => {
+      return this.doAbortableAuthenticationCeremony(abortController);
+    });
+  }
+
+  /**
+   * Conducts an authentication ceremony for a given user.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a function to abort the
+   * ceremony and a `Promise` of the credentials.
+   */
+  doAuthenticationCeremonyForUser(userId: string) {
+    return runAbortableAuthentication((abortController) => {
+      return this.doAbortableAuthenticationCeremonyForUser(userId, abortController);
+    });
+  }
+
+  // conducts an authentication ceremony with a given AbortController.
+  private async doAbortableAuthenticationCeremony(
+    abortController: AbortController,
+  ) {
+    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();
+    const credential = await navigator.credentials.get({
+      ...options,
+      mediation: 'conditional',
+      signal: abortController.signal,
+    }) as (PublicKeyCredential | null);
+    if (credential == null) {
+      throw new Error('public key credential must be provided');
+    }
+    const publicKeyInfo = extractPublicKeyInfo(credential);
+    const { userHandle } = publicKeyInfo;
+    if (userHandle == null) {
+      throw new Error('authenticator must return userHandle');
+    }
+    const { sessionId } = await this.credentialsApi.startAuthentication(userHandle);
+    // ignores other parameters for discoverable credentials
+    const tokens = await this.credentialsApi.finishAuthentication(
+      sessionId,
+      userHandle,
+      credential,
+    );
+    return {
+      publicKeyInfo,
+      tokens,
+    };
+  }
+
+  // conducts an authentication ceremony for a given user with a given
+  // AbortController.
+  private async doAbortableAuthenticationCeremonyForUser(
+    userId: string,
+    abortController: AbortController,
+  ) {
+    const session = await this.credentialsApi.startAuthentication(userId);
+    const credential = await navigator.credentials.get({
+      ...session.credentialRequestOptions,
+      mediation: 'conditional',
+      signal: abortController.signal,
+    }) as (PublicKeyCredential | null);
+    if (credential == null) {
+      throw new Error('public key credential must be provided');
+    }
+    const tokens = await this.credentialsApi.finishAuthentication(
+      session.sessionId,
+      userId,
+      credential,
+    );
+    return {
+      publicKeyInfo: extractPublicKeyInfo(credential),
+      tokens,
+    };
+  }
+}
+
+// runs an abortable authentication operation.
+//
+// this function synchronously returns a function to abort the operation and
+// a `Promise` of the credentials.
+function runAbortableAuthentication(
+  authenticate: (a: AbortController) => Promise<Credentials>,
+) {
+  let abortController: AbortController | undefined = new AbortController();
+  const credentials = authenticate(abortController).finally(() => {
+    abortController = undefined;
+  });
+  return {
+    abort: () => {
+      if (abortController != null) {
+        abortController.abort();
+        abortController = undefined;
+      }
+    },
+    credentials,
+  };
+}
+
+// extracts public key information from an encoded public key credential.
+function extractPublicKeyInfo(publicKey: PublicKeyCredential): PublicKeyInfo {
+  const { userHandle } = publicKey.response as AuthenticatorAssertionResponse;
+  return {
+    id: publicKey.id,
+    userHandle: userHandle != null ? bufferToBase64url(userHandle) : null,
+    authenticatorAttachment: publicKey.authenticatorAttachment,
+  };
+}
diff --git a/app/src/utils/passquito-types.ts b/app/src/utils/passquito-types.ts
index e697885..4c5e3f9 100644
--- a/app/src/utils/passquito-types.ts
+++ b/app/src/utils/passquito-types.ts
@@ -29,6 +29,22 @@ export interface VerifiedUserInfo {
   userInfo: UserInfo;
 }
 
+/**
+ * Information about a public key.
+ *
+ * @beta
+ */
+export interface PublicKeyInfo {
+  /** Unique ID of the public key. */
+  id: string;
+
+  /** User handle. Should be the Passquito user ID. */
+  userHandle?: string | null;
+
+  /** Authenticator attachment status. */
+  authenticatorAttachment?: string | null;
+}
+
 /**
  * Cognito tokens.
  *
@@ -58,3 +74,41 @@ export interface CognitoTokens {
    */
   activatedAt: number;
 }
+
+/**
+ * Returns if a given value is a {@link PublicKeyInfo}.
+ *
+ * @beta
+ */
+export function isPublicKeyInfo(value: unknown): value is PublicKeyInfo {
+  if (value == null || typeof value !== 'object') {
+    return false;
+  }
+  const maybeKeyInfo = value as PublicKeyInfo;
+  if (typeof maybeKeyInfo.id !== 'string') {
+    return false;
+  }
+  if (maybeKeyInfo.userHandle != null && typeof maybeKeyInfo.userHandle !== 'string') {
+    return false;
+  }
+  if (maybeKeyInfo.authenticatorAttachment != null && typeof maybeKeyInfo.authenticatorAttachment !== 'string') {
+    return false;
+  }
+  return true;
+}
+
+/**
+ * Returns if a given value is a {@link CognitoTokens}.
+ *
+ * @beta
+ */
+export function isCognitoTokens(value: unknown): value is CognitoTokens {
+  if (value == null || typeof value !== 'object') {
+    return false;
+  }
+  const maybeTokens = value as CognitoTokens;
+  return typeof maybeTokens.idToken === 'string' &&
+    typeof maybeTokens.accessToken === 'string' &&
+    typeof maybeTokens.refreshToken === 'string' &&
+    typeof maybeTokens.activatedAt === 'number';
+}
diff --git a/app/src/utils/passquito.ts b/app/src/utils/passquito.ts
index 2631ebe..2432395 100644
--- a/app/src/utils/passquito.ts
+++ b/app/src/utils/passquito.ts
@@ -1,156 +1,3 @@
-import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
-import {
-  bufferToBase64url,
-  convert,
-  getRequestFromJSON,
-  schema,
-  type PublicKeyCredentialWithAssertionJSON,
-  type PublicKeyCredentialWithAttestationJSON,
-} from '@github/webauthn-json/extended';
-
-import { credentialsApiUrl, isCognito } from '../auth-config';
-import type { CredentialsApi } from './credentials-api';
-import type { CognitoTokens, UserInfo, VerifiedUserInfo } from './passquito-types';
-export type { CognitoTokens } from './passquito-types';
-
-// passkey registration session.
-interface RegistrationSession {
-  sessionId: string;
-  options: CredentialCreationOptions;
-}
-
-/** Raw Cognito tokens. */
-export type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
-
-/**
- * Information about a public key.
- *
- * @beta
- */
-export interface PublicKeyInfo {
-  id: string;
-  userHandle?: string | null;
-  authenticatorAttachment?: string | null;
-}
-
-/**
- * Cognito tokens and the key info.
- *
- * @beta
- */
-export interface Credentials {
-  publicKeyInfo: PublicKeyInfo;
-  tokens: CognitoTokens;
-}
-
-// global Credentials API object.
-const credentialsApi: CredentialsApi = {
-  async startRegistration(userInfo: UserInfo) {
-    const endpoint = `${credentialsApiUrl.replace(/\/$/, '')}/registration/start`;
-    const res = await fetch(endpoint, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify(userInfo),
-    });
-    const session = await res.json();
-    return {
-      sessionId: session.sessionId,
-      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
-    };
-  },
-  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
-    const endpoint = `${credentialsApiUrl.replace(/\/$/, '')}/registration/start-verified`;
-    const res = await fetch(endpoint, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        'Authorization': userInfo.idToken,
-      },
-      body: JSON.stringify(userInfo.userInfo),
-    });
-    return await res.json();
-  },
-  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
-    const endpoint = `${credentialsApiUrl.replace(/\/$/, '')}/registration/finish`;
-    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
-    console.log('encoded credential:', encodedCredential);
-    const res = await fetch(endpoint, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({
-        sessionId,
-        publicKeyCredential: encodedCredential,
-      }),
-    });
-    if (!res.ok) {
-      throw new Error(
-        `credential registration failed with ${res.status}: ${await res.text()}`,
-      );
-    }
-  },
-  async getDiscoverableCredentialRequestOptions() {
-    const endpoint =
-      `${credentialsApiUrl.replace(/\/$/, '')}/authentication/discover`;
-    const res = await fetch(endpoint, {
-      method: 'POST',
-    });
-    return getRequestFromJSON(await res.json());
-  },
-  async startAuthentication(userId: string) {
-    const res = await fetch(`${credentialsApiUrl.replace(/\/$/, '')}/authentication/start`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({ userId }),
-    });
-    const session = await res.json();
-    return {
-      sessionId: session.sessionId,
-      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
-    };
-  },
-  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
-    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
-    const res = await fetch(`${credentialsApiUrl.replace(/\/$/, '')}/authentication/finish`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({
-        sessionId,
-        userId,
-        publicKey: encodedCredential,
-      }),
-    });
-    const tokens = await res.json();
-    if (!isRawCognitoTokens(tokens)) {
-      throw new Error('invalid Cognito tokens');
-    }
-    return activateCognitoTokens(tokens);
-  },
-  async refreshTokens(refreshToken: string) {
-    const endpoint = `${credentialsApiUrl.replace(/\/$/, '')}/authentication/refresh`;
-    const res = await fetch(endpoint, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({ refreshToken }),
-    });
-    // TODO: handle errors
-    const tokens = await res.json();
-    if (!isRawCognitoTokens(tokens)) {
-      return undefined;
-    }
-    return activateCognitoTokens(tokens);
-  },
-};
-
 /**
  * Checks if passkey registration is supported on the current device.
  *
@@ -228,340 +75,6 @@ export async function checkPasskeyAuthenticationSupported(): Promise<boolean> {
   return true;
 }
 
-/**
- * Conducts a registration ceremony.
- *
- * @remarks
- *
- * References:
- * - <https://web.dev/articles/passkey-registration>
- * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
- *
- * @beta
- */
-export async function doRegistrationCeremony(userInfo: UserInfo) {
-    const { sessionId, options } = await startRegistration(userInfo);
-    console.log('CredentialCreationOptions:', options);
-    const credential = await navigator.credentials.create(options);
-    if (credential == null) {
-      throw new Error('failed to create a new credential');
-    }
-    console.log('registering new credential:', credential);
-    await registerPublicKeyCredential(sessionId, credential as PublicKeyCredential);
-}
-
-/**
- * Conducts a registration ceremony for a verified user.
- *
- * @remarks
- *
- * This ceremony is for a user who has been authenticated through a cross-device
- * authentication and wants to register a new device (credential).
- *
- * References:
- * - <https://web.dev/articles/passkey-registration>
- * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
- *
- * @beta
- */
-export async function doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
-  const { sessionId, options } = await startRegistrationForVerifiedUser(userInfo);
-  console.log('CredentialCreationOptions:', options);
-  const credential = await navigator.credentials.create(options);
-  if (credential == null) {
-    throw new Error('failed to create a new credential');
-  }
-  console.log('registering new credential:', credential);
-  await registerPublicKeyCredential(sessionId, credential as PublicKeyCredential);
-}
-
-/**
- * Conducts an authentication ceremony.
- *
- * @remarks
- *
- * Reference:
- * - <https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion>
- *
- * @beta
- */
-export function doAuthenticationCeremony() {
-  let abortController: AbortController | undefined = new AbortController();
-  const credentials = doAbortableAuthenticationCeremony(abortController).finally(() => {
-    abortController = undefined;
-  });
-  return {
-    abort: () => {
-      if (abortController != null) {
-        abortController.abort();
-        abortController = undefined;
-      }
-    },
-    credentials,
-  };
-}
-
-/**
- * Conducts an authentication ceremony for a given user.
- *
- * @beta
- */
-export function doAuthenticationCeremonyForUser(userId: string) {
-  let abortController: AbortController | undefined = new AbortController();
-  const credentials = doAbortableAuthenticationCeremonyForUser(userId, abortController).finally(() => {
-    abortController = undefined;
-  });
-  return {
-    abort: () => {
-      if (abortController != null) {
-        abortController.abort();
-        abortController = undefined;
-      }
-    },
-    credentials,
-  };
-}
-
-// obtains the public key credential creation options
-//
-// throws if an error occurs.
-async function startRegistration(userInfo: UserInfo): Promise<RegistrationSession> {
-  const session = await credentialsApi.startRegistration(userInfo);
-  return {
-    sessionId: session.sessionId,
-    options: session.credentialCreationOptions,
-  };
-}
-
-// obtains the public key credential creation options for a verified user
-//
-// throws if an error occurs.
-async function startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo): Promise<RegistrationSession> {
-  const session = await credentialsApi.startRegistrationForVerifiedUser(userInfo);
-  return {
-    sessionId: session.sessionId,
-    options: session.credentialCreationOptions,
-  };
-}
-
-// registers a public key credential.
-//
-// throws if an error occurs.
-async function registerPublicKeyCredential(
-  sessionId: string,
-  credential: PublicKeyCredential,
-) {
-  await credentialsApi.finishRegistration(sessionId, credential);
-}
-
-// conducts an authentication ceremony with a give AbortController.
-async function doAbortableAuthenticationCeremony(abortController: AbortController) {
-  const options = await getCredentialRequestOptions();
-  console.log('credential request options:', options);
-  const credential = await navigator.credentials.get({
-    ...options,
-    mediation: 'conditional',
-    signal: abortController.signal,
-  });
-  console.log('assertion:', credential);
-  return await authenticateDiscoverablePublicKeyCredential(
-    credential as PublicKeyCredential,
-  );
-}
-
-// conducts an authentication ceremony for a given user with a given AbortController.
-async function doAbortableAuthenticationCeremonyForUser(userId: string, abortController: AbortController) {
-  const session = await startAuthenticationSessionForUser(userId);
-  const { credentialRequestOptions } = session;
-  console.log('credential request options:', credentialRequestOptions);
-  const credential = await navigator.credentials.get({
-    ...credentialRequestOptions,
-    mediation: 'conditional',
-    signal: abortController.signal,
-  });
-  console.log('assertion:', credential);
-  return await finishAuthenticationSession(session, credential as PublicKeyCredential);
-}
-
-// obtains the public key credential request options
-//
-// throws if an error occurs.
-async function getCredentialRequestOptions(username?: string) {
-  // let res;
-  if (username != null) {
-    // TODO: ask Cognito for sign-in of a specific user
-    throw new Error("not implemented yet");
-  } else {
-    return await credentialsApi.getDiscoverableCredentialRequestOptions();
-  }
-}
-
-// authenticates a given public key credential in a discoverable manner.
-async function authenticateDiscoverablePublicKeyCredential(credential: PublicKeyCredential): Promise<Credentials> {
-  const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
-  console.log('encoded credential:', encodedCredential);
-  const publicKeyInfo = extractPublicKeyInfo(encodedCredential);
-  if (isCognito) {
-    const userHandle = encodedCredential.response.userHandle;
-    if (userHandle == null) {
-      throw new Error("authenticator must return userHandle");
-    }
-    const session = await credentialsApi.startAuthentication(userHandle);
-    console.log('initiated authentication session:', session);
-    // ignores challenge parameters for discoverable credentials
-    const tokens = await credentialsApi.finishAuthentication(session.sessionId, userHandle, credential);
-    return {
-      publicKeyInfo,
-      tokens,
-    };
-  } else {
-    const endpoint =
-      `${credentialsApiUrl.replace(/\/$/, '')}/discoverable/finish`;
-    const res = await fetch(endpoint, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify(encodedCredential),
-    });
-    return {
-      publicKeyInfo,
-      tokens: await res.json(),
-    };
-  }
-}
-
-// starts an authentication session.
-async function startAuthenticationSessionForUser(userId: string) {
-  return await credentialsApi.startAuthentication(userId);
-}
-
-// authenticates a given public key credential.
-async function finishAuthenticationSession(
-  session: any,
-  credential: PublicKeyCredential,
-): Promise<Credentials> {
-  const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
-  console.log('encoded credential:', encodedCredential);
-  const userId = encodedCredential.response.userHandle;
-  if (userId == null) {
-    throw new Error("authenticator must return userHandle");
-  }
-  const publicKeyInfo = extractPublicKeyInfo(encodedCredential);
-  const tokens = await credentialsApi.finishAuthentication(session.sessionId, userId, credential);
-  return {
-    publicKeyInfo,
-    tokens,
-  };
-}
-
-// encodes `PublicKeyCredential` for a registration API request body.
-//
-// "base64url"-encodes `ArrayBuffer`s.
-function encodePublicKeyCredentialForCreation(publicKey: PublicKeyCredential): PublicKeyCredentialWithAttestationJSON {
-  return convert(
-    bufferToBase64url,
-    schema.publicKeyCredentialWithAttestation,
-    publicKey,
-  );
-}
-
-// encodes `PublicKeyCredential` for an authentication API request body.
-//
-// "base64url"-encodes `ArrayBuffer`s.
-function encodePublicKeyCredentialForAuthentication(publicKey: PublicKeyCredential): PublicKeyCredentialWithAssertionJSON {
-  return convert(
-    bufferToBase64url,
-    schema.publicKeyCredentialWithAssertion,
-    publicKey,
-  );
-}
-
-// Extracts public key information from an encoded public key credential.
-function extractPublicKeyInfo(publicKey: PublicKeyCredentialWithAssertionJSON): PublicKeyInfo {
-  return {
-    id: publicKey.id,
-    userHandle: publicKey.response.userHandle,
-    authenticatorAttachment: publicKey.authenticatorAttachment,
-  };
-}
-
-/**
- * Refreshes ID and access tokens with a given refresh token.
- *
- * @beta
- */
-export async function refreshTokens(refreshToken: string): Promise<CognitoTokens | undefined> {
-  return await credentialsApi.refreshTokens(refreshToken);
-}
-
-/**
- * Returns if a given value is a `PublicKeyInfo`.
- *
- * @beta
- */
-export function isPublicKeyInfo(value: unknown): value is PublicKeyInfo {
-  if (value == null || typeof value !== 'object') {
-    return false;
-  }
-  const maybeKeyInfo = value as PublicKeyInfo;
-  if (typeof maybeKeyInfo.id !== 'string') {
-    return false;
-  }
-  if (maybeKeyInfo.userHandle != null && typeof maybeKeyInfo.userHandle !== 'string') {
-    return false;
-  }
-  if (maybeKeyInfo.authenticatorAttachment != null && typeof maybeKeyInfo.authenticatorAttachment !== 'string') {
-    return false;
-  }
-  return true;
-}
-
-/**
- * Returns if a given value is a `RawCognitoTokens`.
- *
- * @beta
- */
-export function isRawCognitoTokens(value: unknown): value is RawCognitoTokens {
-  if (value == null || typeof value !== 'object') {
-    return false;
-  }
-  const maybeTokens = value as RawCognitoTokens;
-  return typeof maybeTokens.idToken === 'string' &&
-    typeof maybeTokens.accessToken === 'string' &&
-    typeof maybeTokens.refreshToken === 'string' &&
-    typeof maybeTokens.expiresIn === 'number';
-}
-
-/**
- * Returns if a given value is a `CognitoTokens`.
- *
- * @beta
- */
-export function isCognitoTokens(value: unknown): value is CognitoTokens {
-  if (!isRawCognitoTokens(value)) {
-    return false;
-  }
-  const maybeTokens = value as CognitoTokens;
-  return typeof maybeTokens.activatedAt === 'number';
-}
-
-/**
- * Activates a given `RawCognitoTokens` and obtains a `CognitoTokens`.
- *
- * @remarks
- *
- * You should call this function as soon as you obtain Cognito tokens.
- *
- * @beta
- */
-export function activateCognitoTokens(tokens: RawCognitoTokens): CognitoTokens {
-  return {
-    ...tokens,
-    activatedAt: Date.now(),
-  };
-}
-
 /**
  * Returns if a given error object is an `AbortError`.
  *
diff --git a/app/src/views/RegisterDevice.vue b/app/src/views/RegisterDevice.vue
index ff27f58..a8d7fb3 100644
--- a/app/src/views/RegisterDevice.vue
+++ b/app/src/views/RegisterDevice.vue
@@ -5,10 +5,8 @@ import { useRouter } from 'vue-router';
 
 import { useCredentialStore } from '../stores/credential';
 import { usePasskeyCapabilityStore } from '../stores/passkey-capability';
-import {
-  doRegistrationCeremonyForVerifiedUser,
-  getErrorName,
-} from '../utils/passquito';
+import { usePassquitoClientStore } from '../stores/passquito-client';
+import { getErrorName } from '../utils/passquito';
 
 // router
 const router = useRouter();
@@ -21,6 +19,9 @@ onMounted(() => {
   passkeyCapabilityStore.askForCapabilities();
 });
 
+// Passquito client
+const passquitoClientStore = usePassquitoClientStore();
+
 // credential
 const credentialStore = useCredentialStore();
 
@@ -69,7 +70,7 @@ const onSubmit = async () => {
   try {
     console.log('starting registration...');
     // TODO: deal with a token expiration
-    await doRegistrationCeremonyForVerifiedUser({
+    await passquitoClientStore.client.doRegistrationCeremonyForVerifiedUser({
       idToken,
       userInfo: {
         username: username.value,
diff --git a/app/src/views/Secured.vue b/app/src/views/Secured.vue
index b1cb9ba..5413639 100644
--- a/app/src/views/Secured.vue
+++ b/app/src/views/Secured.vue
@@ -5,11 +5,14 @@ import { useRouter } from 'vue-router';
 
 import { credentialsApiUrl } from '../auth-config';
 import { useCredentialStore } from '../stores/credential';
-import { refreshTokens as doRefreshTokens } from '../utils/passquito';
+import { useCredentialsApiStore } from '../stores/credentials-api';
 
 // router
 const router = useRouter();
 
+// Credentials API access
+const credentialsApiStore = useCredentialsApiStore();
+
 // credential
 const credentialStore = useCredentialStore();
 
@@ -43,7 +46,7 @@ const refreshTokens = async () => {
   if (refreshToken == null) {
     return false;
   }
-  const newTokens = await doRefreshTokens(refreshToken);
+  const newTokens = await credentialsApiStore.api.refreshTokens(refreshToken);
   if (newTokens == null) {
     return false;
   }
diff --git a/app/src/views/SignIn.vue b/app/src/views/SignIn.vue
index 5b0b23f..8c734a0 100644
--- a/app/src/views/SignIn.vue
+++ b/app/src/views/SignIn.vue
@@ -6,11 +6,8 @@ import { RouterLink, useRouter } from 'vue-router';
 import { useWebauthn } from '../composables/webauthn';
 import { useCredentialStore } from '../stores/credential';
 import { usePasskeyCapabilityStore } from '../stores/passkey-capability';
-import {
-  doAuthenticationCeremony,
-  doAuthenticationCeremonyForUser,
-  getErrorName
-} from '../utils/passquito';
+import { usePassquitoClientStore } from '../stores/passquito-client';
+import { getErrorName } from '../utils/passquito';
 
 // router
 //
@@ -21,6 +18,9 @@ const router = useRouter();
 // passkey capabilities
 const passkeyCapabilityStore = usePasskeyCapabilityStore();
 
+// Passquito client
+const passquitoClientStore = usePassquitoClientStore();
+
 // credential
 const credentialStore = useCredentialStore();
 
@@ -55,8 +55,8 @@ watch(
     abortAuthentication.value('starting authentication');
     const userId = credentialStore.userId;
     const { abort, credentials: futureCredentials } = userId != null
-      ? doAuthenticationCeremonyForUser(userId)
-      : doAuthenticationCeremony();
+      ? passquitoClientStore.client.doAuthenticationCeremonyForUser(userId)
+      : passquitoClientStore.client.doAuthenticationCeremony();
     abortAuthentication.value = abort;
     try {
       const { publicKeyInfo, tokens } = await futureCredentials;
diff --git a/app/src/views/SignUp.vue b/app/src/views/SignUp.vue
index 583f9a5..b391e82 100644
--- a/app/src/views/SignUp.vue
+++ b/app/src/views/SignUp.vue
@@ -4,7 +4,7 @@ import { computed, onMounted, ref } from 'vue';
 import { RouterLink, useRouter } from 'vue-router';
 
 import { usePasskeyCapabilityStore } from '../stores/passkey-capability';
-import { doRegistrationCeremony } from '../utils/passquito';
+import { usePassquitoClientStore } from '../stores/passquito-client';
 
 defineProps({
   message: String
@@ -20,6 +20,9 @@ const router = useRouter();
 // passkey capabilities
 const passkeyCapabilityStore = usePasskeyCapabilityStore();
 
+// Passquito client
+const passquitoClientStore = usePassquitoClientStore();
+
 // checks if passkeys are supported 
 onMounted(async () => {
   passkeyCapabilityStore.askForCapabilities();
@@ -32,7 +35,7 @@ const displayName = ref('');
 const onSubmit = async () => {
   try {
     console.log('starting registration...');
-    await doRegistrationCeremony({
+    await passquitoClientStore.client.doRegistrationCeremony({
       username: username.value,
       displayName: displayName.value,
     });


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new PassquitoClient to encapsulate registration and authentication logic, thereby reducing the need to pass around the CredentialsApi. Key changes include:  
- Replacing direct calls to utility functions in views with calls to methods on the PassquitoClient instance.  
- Introducing Pinia stores for both CredentialsApi and PassquitoClient.  
- Moving related type definitions and helper functions into dedicated utility files.

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                  | Description                                              |
| ------------------------------------- | -------------------------------------------------------- |
| app/src/views/SignUp.vue               | Updated to use PassquitoClient for registration ceremony.|
| app/src/views/SignIn.vue               | Updated to use PassquitoClient for authentication ceremony.|
| app/src/views/Secured.vue              | Switched to using the CredentialsApi from the new store for token refresh.|
| app/src/views/RegisterDevice.vue       | Updated to use PassquitoClient for verified user registration.|
| app/src/utils/passquito-types.ts       | Added new types and helper functions for public key info.|
| app/src/utils/passquito-client.ts      | Introduced PassquitoClient encapsulating registration and authentication logic.|
| app/src/utils/credentials-api-impl.ts  | Implementation of CredentialsApi using the updated API endpoints.|
| app/src/stores/passquito-client.ts     | Created a store that provides reactive access to PassquitoClient.|
| app/src/stores/credentials-api.ts      | Created a store for providing CredentialsApi.|
| app/src/stores/credential.ts           | Updated to import types from the new passquito-types file.|
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T04:10:14Z
user: login: sourcery-ai[bot]
body: Hey @kikuomax - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider if the `PassquitoClient` store is necessary, or if `PassquitoClient` could be instantiated directly where needed using the `CredentialsApi` store.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T04:10:38Z
user: login: kikuomax
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T04:27:00Z
user: login: sourcery-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T04:27:07Z
user: login: kikuomax
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T04:36:19Z
user: login: kikuomax
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T04:44:40Z
user: login: sourcery-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T04:44:58Z
user: login: kikuomax
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T04:48:22Z
user: login: sourcery-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T04:48:52Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces the PassquitoClient to encapsulate passkey registration and authentication logic and replaces direct references to the global CredentialsApi with new Pinia stores.  
- Refactor Vue views (SignUp, SignIn, Secured, RegisterDevice) to use the PassquitoClient via new Pinia stores.  
- Implement core ceremonies for registration and authentication in the PassquitoClient class.  
- Remove redundant legacy functions and global instances.

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                              | Description                                             |
| --------------------------------- | ------------------------------------------------------- |
| app/src/views/SignUp.vue           | Replace legacy registration with PassquitoClient usage  |
| app/src/views/SignIn.vue           | Update authentication calls to use PassquitoClient      |
| app/src/views/Secured.vue          | Replace token refresh with PassquitoClient API usage     |
| app/src/views/RegisterDevice.vue   | Update registration for verified user with new client    |
| app/src/utils/passquito-types.ts   | Add new types for public key and tokens                  |
| app/src/utils/passquito-client.ts   | Implement PassquitoClient and supporting auth ceremonies |
| app/src/utils/credentials-api-impl.ts | Implement API calls for registration and authentication  |
| app/src/stores/passquito-client.ts  | Create new Pinia store to expose PassquitoClient and API   |
| app/src/stores/credentials-api.ts   | Create store for CredentialsApi instance                  |
| app/src/stores/credential.ts        | Update type imports to align with new types               |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**app/src/views/Secured.vue:49**
* [nitpick] For consistency with other views that use passquitoClientStore.client for authentication ceremonies, consider exposing a refreshTokens method on the PassquitoClient and accessing it via passquitoClientStore.client instead of directly using the api property.
```
const newTokens = await passquitoClientStore.api.refreshTokens(refreshToken);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T05:37:01Z
user: login: sourcery-ai[bot]
body: Hey @kikuomax - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider consolidating helper functions (like encoding, type guards, and `extractPublicKeyInfo`) into dedicated utility files for clearer organization instead of spreading them across implementation and type files.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 3 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T05:38:14Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new Passquito client architecture by refactoring registration and authentication flows into a dedicated PassquitoClient class and integrating it with Pinia stores while removing the legacy global CredentialsApi instance.  
- Introduces PassquitoClient and CredentialsApiImpl for managing passkey ceremonies and backend communication  
- Updates Vue components (SignIn, SignUp, RegisterDevice, Secured) to use the new stores and client  
- Moves type definitions (PublicKeyInfo and related guards) to a dedicated types file and removes redundant functions

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                                                                             |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------- |
| app/src/views/SignUp.vue             | Replaced legacy passquito API calls with invocations via the new passquitoClientStore.                  |
| app/src/views/SignIn.vue             | Updated authentication ceremony calls to use PassquitoClient and aligned error retrieval imports.      |
| app/src/views/Secured.vue            | Removed the refreshTokens alias and now using CredentialsApi store for token refreshes.                |
| app/src/views/RegisterDevice.vue     | Migrated registration ceremony for verified users to the new PassquitoClient implementation.            |
| app/src/utils/passquito-types.ts     | Added PublicKeyInfo type and updated type guard functions to support the new client architecture.        |
| app/src/utils/passquito-client.ts      | Introduced the PassquitoClient class encapsulating the passkey operations.                              |
| app/src/utils/credentials-api-impl.ts | Implemented a new CredentialsApiImpl class for backend REST calls and token activation logic.           |
| app/src/stores/passquito-client.ts     | Created a new Pinia store wrapping the PassquitoClient instance.                                      |
| app/src/stores/credentials-api.ts      | Added a new Pinia store providing access to a CredentialsApiImpl instance.                             |
| app/src/stores/credential.ts           | Updated import paths to use the new passquito-types file for credential type validations.               |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**app/src/utils/passquito-client.ts:170**
* [nitpick] Consider replacing the 'any' type for the 'session' parameter with a well-defined interface that represents the expected structure of the authentication session.
```
private async finishAuthenticationSession(session: any, credential: PublicKeyCredential): Promise<Credentials> {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T05:51:02Z
user: login: sourcery-ai[bot]
body: Hey @kikuomax - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider standardizing the error handling approach for `fetch` calls within `CredentialsApiImpl` to ensure non-ok responses are consistently handled before attempting to parse JSON.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T05:52:11Z
user: login: kikuomax
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T05:55:52Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the passkey handling flow by introducing a PassquitoClient class and replacing the global CredentialsApi reference with dedicated Pinia stores. Key changes include moving registration and authentication logic into PassquitoClient, replacing legacy utility functions with store-based client methods, and updating related views to use the new stores.

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                   | Description                                                       |
| -------------------------------------- | ----------------------------------------------------------------- |
| app/src/views/SignUp.vue                | Updated to use the PassquitoClient store for registration ceremony. |
| app/src/views/SignIn.vue                | Updated to use the PassquitoClient store for authentication ceremony. |
| app/src/views/Secured.vue               | Updated token refreshing to use the CredentialsApi store.         |
| app/src/views/RegisterDevice.vue        | Updated to use the PassquitoClient store for verified registration. |
| app/src/utils/passquito-types.ts         | Added new types and helper functions for passkey operations.        |
| app/src/utils/passquito-client.ts         | Introduced the PassquitoClient class encapsulating core ceremonies. |
| app/src/utils/credentials-api-impl.ts     | Added a concrete CredentialsApi implementation with improved encoding. |
| app/src/stores/passquito-client.ts         | Created a Pinia store to manage the PassquitoClient instance.         |
| app/src/stores/credentials-api.ts         | Created a Pinia store to manage the CredentialsApi instance.         |
| app/src/stores/credential.ts              | Updated type imports to use the new passquito-types module.          |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T08:42:33Z
user: login: sourcery-ai[bot]
body: Hey @kikuomax - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T08:43:20Z
user: login: kikuomax
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T18:15:17Z

Review comments:
diff_hunk: @@ -0,0 +1,20 @@
+import { defineStore } from 'pinia';
+import { computed } from 'vue';
+
+import { PassquitoClient } from '../utils/passquito-client';
+
+import { useCredentialsApiStore } from './credentials-api';
+
+export const usePassquitoClientStore = defineStore('passquito-client', () => {
+  // Credentials API access
+  const credentialsApiStore = useCredentialsApiStore();
+  const api = computed(() => credentialsApiStore.api);
+
+  // Passquito client
+  const client = computed(() => new PassquitoClient(api.value));
+
path: app/src/stores/passquito-client.ts
user: login: Copilot
body: [nitpick] Instantiating PassquitoClient within a computed may lead to unexpected behavior if the client maintains internal state. Consider instantiating it once (e.g., during store initialization) if the API instance is not expected to change frequently.
```suggestion
  const client = new PassquitoClient(api.value);

  // Watch for changes in the API and update the client
  watch(
    () => api.value,
    (newApi) => {
      client.setApi(newApi); // Assuming PassquitoClient has a setApi method
    }
  );
```
created_at: 2025-04-27T04:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the common abortable flow into a single helper function to reduce redundancy and improve code maintainability.

You can extract the common abortable flow into a single helper that accepts parameters (or callbacks) for session retrieval, credential fetching, and authentication finalization. For example, you might create a helper like this:

```ts
private performAbortableAuthenticationCeremony<T>(
  sessionFetcher: (userParam?: string) => Promise<T>,
  credentialGetter: (session: T, signal: AbortSignal) => Promise<PublicKeyCredential>,
  sessionFinisher: (session: T, credential: PublicKeyCredential) => Promise<Credentials>,
  userParam?: string
) {
  const abortController = new AbortController();
  const credentials = (async () => {
    const session = await sessionFetcher(userParam);
    const credential = await credentialGetter(session, abortController.signal);
    return await sessionFinisher(session, credential);
  })().finally(() => {
    // Clear for later calls if needed.
  });

  return {
    abort: () => abortController.abort(),
    credentials,
  };
}
```

Then both ceremonies would simply call this helper. For example, for the discoverable case:

```ts
doAuthenticationCeremony() {
  return this.performAbortableAuthenticationCeremony(
    async () => await this.credentialsApi.getDiscoverableCredentialRequestOptions(),
    async (options, signal) =>
      await navigator.credentials.get({ ...options, mediation: 'conditional', signal }),
    async (_, credential) => await this.authenticateDiscoverablePublicKeyCredential(credential)
  );
}
```

And for a user-specific ceremony:

```ts
doAuthenticationCeremonyForUser(userId: string) {
  return this.performAbortableAuthenticationCeremony(
    async (user) => await this.credentialsApi.startAuthentication(user!),
    async (session, signal) =>
      await navigator.credentials.get({ 
        ...session.credentialRequestOptions, 
        mediation: 'conditional', 
        signal 
      }),
    async (session, credential) => await this.finishAuthenticationSession(session, credential),
    userId
  );
}
```

This reduces redundancy and keeps the abort-controller logic in one place while preserving all functionality.
created_at: 2025-04-27T04:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,191 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON ,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+// raw Cognito tokens returned from the API
+type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
+
+/**
+ * Default implementation of {@link CredentialsApi}.
+ *
+ * @beta
+ */
+export class CredentialsApiImpl implements CredentialsApi {
+  /** Base URL of the Credentials API. */
+  readonly baseUrl: string;
+
+  /** Initializes with a given base URL of the Credentials API. */
+  constructor(baseUrl: string) {
+    // removes a trailing slash if it exists
+    this.baseUrl = baseUrl.replace(/\/$/, '');
+  }
+
+  async startRegistration(userInfo: UserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(userInfo),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
+    };
+  }
+
+  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start-verified`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': userInfo.idToken,
+      },
+      body: JSON.stringify(userInfo.userInfo),
+    });
+    return await res.json();
+  }
+
+  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
+    const endpoint = `${this.baseUrl}/registration/finish`;
+    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        publicKeyCredential: encodedCredential,
+      }),
+    });
+    if (!res.ok) {
+      throw new Error(
+        `credential registration failed with ${res.status}: ${await res.text()}`,
+      );
+    }
+  }
+
+  async getDiscoverableCredentialRequestOptions() {
+    const endpoint = `${this.baseUrl}/authentication/discover`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+    });
+    return getRequestFromJSON(await res.json());
+  }
+
+  async startAuthentication(userId: string) {
+    const res = await fetch(`${this.baseUrl}/authentication/start`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ userId }),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
+    };
+  }
+
+  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const res = await fetch(`${this.baseUrl}/authentication/finish`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        userId,
+        publicKey: encodedCredential,
+      }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      throw new Error('invalid Cognito tokens');
+    }
+    return activateCognitoTokens(tokens);
+  }
+
+  async refreshTokens(refreshToken: string) {
+    const endpoint = `${this.baseUrl}/authentication/refresh`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ refreshToken }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      return undefined;
+    }
+    return activateCognitoTokens(tokens);
+  }
+}
+
+// encodes `PublicKeyCredential` for a registration API request body.
+//
+// replaces `ArrayBuffer`s with Base64-URL-encoded strings.
+function encodePublicKeyCredentialForCreation(
+  publicKey: PublicKeyCredential,
+): PublicKeyCredentialWithAttestationJSON {
path: app/src/utils/credentials-api-impl.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the `encodePublicKeyCredentialForCreation` and `encodePublicKeyCredentialForAuthentication` functions into a shared helper module to reduce code duplication.

```markdown
Consider extracting and centralizing the similar encoding functions into a shared helper module. This avoids duplicating logic and makes future updates easier. For example, create a new file (`credential-encoding.ts`) with:

```ts
import { bufferToBase64url, convert, schema } from '@github/webauthn-json/extended';
import type { PublicKeyCredentialWithAttestationJSON, PublicKeyCredentialWithAssertionJSON } from '@github/webauthn-json/extended';

export function encodeCredentialForCreation(
  publicKey: PublicKeyCredential,
): PublicKeyCredentialWithAttestationJSON {
  return convert(bufferToBase64url, schema.publicKeyCredentialWithAttestation, publicKey);
}

export function encodeCredentialForAuthentication(
  publicKey: PublicKeyCredential,
): PublicKeyCredentialWithAssertionJSON {
  return convert(bufferToBase64url, schema.publicKeyCredentialWithAssertion, publicKey);
}
```

Then update your class to import and use these functions:

```ts
import { encodeCredentialForCreation, encodeCredentialForAuthentication } from './credential-encoding';

...

async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
  const endpoint = `${this.baseUrl}/registration/finish`;
  const encodedCredential = encodeCredentialForCreation(credential);
  // rest of the code...
}

async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
  const encodedCredential = encodeCredentialForAuthentication(credential);
  // rest of the code...
}
```

This refactoring keeps functionality intact while reducing duplication and overall complexity.
```
created_at: 2025-04-27T04:10:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
+export interface Credentials {
+  publicKeyInfo: PublicKeyInfo;
+  tokens: CognitoTokens;
+}
+
+/**
+ * Passquito client.
+ *
+ * @beta
+ */
+export class PassquitoClient {
+  /**
+   * Initializes with a given {@link CredentialsApi} instance.
+   *
+   * @param credentialsApi - Credentials API access.
+   */
+  constructor(private readonly credentialsApi: CredentialsApi) {}
+
+  /**
+   * Conducts a registration ceremony.
+   *
+   * @remarks
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremony(userInfo: UserInfo) {
+      const session = await this.credentialsApi.startRegistration(userInfo);
+      const credential = await navigator.credentials.create(session.credentialCreationOptions);
+      if (credential == null) {
+        throw new Error('failed to create a new credential');
+      }
+      await this.credentialsApi.finishRegistration(
+        session.sessionId,
+        credential as PublicKeyCredential,
+      );
+  }
+
+  /**
+   * Conducts a registration ceremony for a verified user.
+   *
+   * @remarks
+   *
+   * This ceremony is for a user who has been authenticated through a cross-device
+   * authentication and wants to register a new device (credential).
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const session = await this.credentialsApi.startRegistrationForVerifiedUser(userInfo);
+    const credential = await navigator.credentials.create(session.credentialCreationOptions);
+    if (credential == null) {
+      throw new Error('failed to create a new credential');
+    }
+    await this.credentialsApi.finishRegistration(
+      session.sessionId,
+      credential as PublicKeyCredential,
+    );
+  }
+
+  /**
+   * Conducts an authentication ceremony.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a getter of the
+   * `AbortController` and a `Promise` of the credentials.
+   *
+   * Reference:
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion>
+   */
+  doAuthenticationCeremony() {
+    let abortController: AbortController | undefined = new AbortController();
+    const credentials = this.doAbortableAuthenticationCeremony(abortController).finally(() => {
+      abortController = undefined;
+    });
+    return {
+      abort: () => {
+        if (abortController != null) {
+          abortController.abort();
+          abortController = undefined;
+        }
+      },
+      credentials,
+    };
+  }
+
+  /**
+   * Conducts an authentication ceremony for a given user.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a getter of the
+   * `AbortController` and a `Promise` of the credentials.
+   */
+  doAuthenticationCeremonyForUser(userId: string) {
+    let abortController: AbortController | undefined = new AbortController();
+    const credentials = this.doAbortableAuthenticationCeremonyForUser(userId, abortController).finally(() => {
+      abortController = undefined;
+    });
+    return {
+      abort: () => {
+        if (abortController != null) {
+          abortController.abort();
+          abortController = undefined;
+        }
+      },
+      credentials,
+    };
+  }
+
+  // conducts an authentication ceremony with a given AbortController.
+  private async doAbortableAuthenticationCeremony(
+    abortController: AbortController,
+  ) {
+    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();
+    const credential = await navigator.credentials.get({
+      ...options,
+      mediation: 'conditional',
+      signal: abortController.signal,
+    });
+    return await this.authenticateDiscoverablePublicKeyCredential(
+      credential as PublicKeyCredential,
+    );
+  }
+
+  // authenticates a given public key credential in a discoverable manner.
+  private async authenticateDiscoverablePublicKeyCredential(
+    credential: PublicKeyCredential,
+  ): Promise<Credentials> {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const publicKeyInfo = extractPublicKeyInfo(encodedCredential);
+    const userHandle = encodedCredential.response.userHandle;
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {userHandle} = encodedCredential.response;
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
created_at: 2025-04-27T04:10:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
+export interface Credentials {
+  publicKeyInfo: PublicKeyInfo;
+  tokens: CognitoTokens;
+}
+
+/**
+ * Passquito client.
+ *
+ * @beta
+ */
+export class PassquitoClient {
+  /**
+   * Initializes with a given {@link CredentialsApi} instance.
+   *
+   * @param credentialsApi - Credentials API access.
+   */
+  constructor(private readonly credentialsApi: CredentialsApi) {}
+
+  /**
+   * Conducts a registration ceremony.
+   *
+   * @remarks
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremony(userInfo: UserInfo) {
+      const session = await this.credentialsApi.startRegistration(userInfo);
+      const credential = await navigator.credentials.create(session.credentialCreationOptions);
+      if (credential == null) {
+        throw new Error('failed to create a new credential');
+      }
+      await this.credentialsApi.finishRegistration(
+        session.sessionId,
+        credential as PublicKeyCredential,
+      );
+  }
+
+  /**
+   * Conducts a registration ceremony for a verified user.
+   *
+   * @remarks
+   *
+   * This ceremony is for a user who has been authenticated through a cross-device
+   * authentication and wants to register a new device (credential).
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const session = await this.credentialsApi.startRegistrationForVerifiedUser(userInfo);
+    const credential = await navigator.credentials.create(session.credentialCreationOptions);
+    if (credential == null) {
+      throw new Error('failed to create a new credential');
+    }
+    await this.credentialsApi.finishRegistration(
+      session.sessionId,
+      credential as PublicKeyCredential,
+    );
+  }
+
+  /**
+   * Conducts an authentication ceremony.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a getter of the
+   * `AbortController` and a `Promise` of the credentials.
+   *
+   * Reference:
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion>
+   */
+  doAuthenticationCeremony() {
+    let abortController: AbortController | undefined = new AbortController();
+    const credentials = this.doAbortableAuthenticationCeremony(abortController).finally(() => {
+      abortController = undefined;
+    });
+    return {
+      abort: () => {
+        if (abortController != null) {
+          abortController.abort();
+          abortController = undefined;
+        }
+      },
+      credentials,
+    };
+  }
+
+  /**
+   * Conducts an authentication ceremony for a given user.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a getter of the
+   * `AbortController` and a `Promise` of the credentials.
+   */
+  doAuthenticationCeremonyForUser(userId: string) {
+    let abortController: AbortController | undefined = new AbortController();
+    const credentials = this.doAbortableAuthenticationCeremonyForUser(userId, abortController).finally(() => {
+      abortController = undefined;
+    });
+    return {
+      abort: () => {
+        if (abortController != null) {
+          abortController.abort();
+          abortController = undefined;
+        }
+      },
+      credentials,
+    };
+  }
+
+  // conducts an authentication ceremony with a given AbortController.
+  private async doAbortableAuthenticationCeremony(
+    abortController: AbortController,
+  ) {
+    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();
+    const credential = await navigator.credentials.get({
+      ...options,
+      mediation: 'conditional',
+      signal: abortController.signal,
+    });
+    return await this.authenticateDiscoverablePublicKeyCredential(
+      credential as PublicKeyCredential,
+    );
+  }
+
+  // authenticates a given public key credential in a discoverable manner.
+  private async authenticateDiscoverablePublicKeyCredential(
+    credential: PublicKeyCredential,
+  ): Promise<Credentials> {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const publicKeyInfo = extractPublicKeyInfo(encodedCredential);
+    const userHandle = encodedCredential.response.userHandle;
path: app/src/utils/passquito-client.ts
user: login: kikuomax
body: I like it, but I want to add whitespace around `userHandle`.
created_at: 2025-04-27T04:27:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
+export interface Credentials {
+  publicKeyInfo: PublicKeyInfo;
+  tokens: CognitoTokens;
+}
+
+/**
+ * Passquito client.
+ *
+ * @beta
+ */
+export class PassquitoClient {
+  /**
+   * Initializes with a given {@link CredentialsApi} instance.
+   *
+   * @param credentialsApi - Credentials API access.
+   */
+  constructor(private readonly credentialsApi: CredentialsApi) {}
+
+  /**
+   * Conducts a registration ceremony.
+   *
+   * @remarks
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremony(userInfo: UserInfo) {
+      const session = await this.credentialsApi.startRegistration(userInfo);
+      const credential = await navigator.credentials.create(session.credentialCreationOptions);
+      if (credential == null) {
+        throw new Error('failed to create a new credential');
+      }
+      await this.credentialsApi.finishRegistration(
+        session.sessionId,
+        credential as PublicKeyCredential,
+      );
+  }
+
+  /**
+   * Conducts a registration ceremony for a verified user.
+   *
+   * @remarks
+   *
+   * This ceremony is for a user who has been authenticated through a cross-device
+   * authentication and wants to register a new device (credential).
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const session = await this.credentialsApi.startRegistrationForVerifiedUser(userInfo);
+    const credential = await navigator.credentials.create(session.credentialCreationOptions);
+    if (credential == null) {
+      throw new Error('failed to create a new credential');
+    }
+    await this.credentialsApi.finishRegistration(
+      session.sessionId,
+      credential as PublicKeyCredential,
+    );
+  }
+
+  /**
+   * Conducts an authentication ceremony.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a getter of the
+   * `AbortController` and a `Promise` of the credentials.
+   *
+   * Reference:
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion>
+   */
+  doAuthenticationCeremony() {
+    let abortController: AbortController | undefined = new AbortController();
+    const credentials = this.doAbortableAuthenticationCeremony(abortController).finally(() => {
+      abortController = undefined;
+    });
+    return {
+      abort: () => {
+        if (abortController != null) {
+          abortController.abort();
+          abortController = undefined;
+        }
+      },
+      credentials,
+    };
+  }
+
+  /**
+   * Conducts an authentication ceremony for a given user.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a getter of the
+   * `AbortController` and a `Promise` of the credentials.
+   */
+  doAuthenticationCeremonyForUser(userId: string) {
+    let abortController: AbortController | undefined = new AbortController();
+    const credentials = this.doAbortableAuthenticationCeremonyForUser(userId, abortController).finally(() => {
+      abortController = undefined;
+    });
+    return {
+      abort: () => {
+        if (abortController != null) {
+          abortController.abort();
+          abortController = undefined;
+        }
+      },
+      credentials,
+    };
+  }
+
+  // conducts an authentication ceremony with a given AbortController.
+  private async doAbortableAuthenticationCeremony(
+    abortController: AbortController,
+  ) {
+    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();
+    const credential = await navigator.credentials.get({
+      ...options,
+      mediation: 'conditional',
+      signal: abortController.signal,
+    });
+    return await this.authenticateDiscoverablePublicKeyCredential(
+      credential as PublicKeyCredential,
+    );
+  }
+
+  // authenticates a given public key credential in a discoverable manner.
+  private async authenticateDiscoverablePublicKeyCredential(
+    credential: PublicKeyCredential,
+  ): Promise<Credentials> {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const publicKeyInfo = extractPublicKeyInfo(encodedCredential);
+    const userHandle = encodedCredential.response.userHandle;
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: Okay, how about this?

```suggestion
    const { userHandle } = encodedCredential.response;
```
created_at: 2025-04-27T04:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import { defineStore } from 'pinia';
+import { computed } from 'vue';
+
+import { PassquitoClient } from '../utils/passquito-client';
+
+import { useCredentialsApiStore } from './credentials-api';
+
+export const usePassquitoClientStore = defineStore('passquito-client', () => {
+  // Credentials API access
+  const credentialsApiStore = useCredentialsApiStore();
+  const api = computed(() => credentialsApiStore.api);
+
+  // Passquito client
+  const client = computed(() => new PassquitoClient(api.value));
+
path: app/src/stores/passquito-client.ts
user: login: kikuomax
body: I should not assume that the API instance can be replaced for now. I will rewrite the code.
created_at: 2025-04-27T04:36:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,191 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON ,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+// raw Cognito tokens returned from the API
+type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
+
+/**
+ * Default implementation of {@link CredentialsApi}.
+ *
+ * @beta
+ */
+export class CredentialsApiImpl implements CredentialsApi {
+  /** Base URL of the Credentials API. */
+  readonly baseUrl: string;
+
+  /** Initializes with a given base URL of the Credentials API. */
+  constructor(baseUrl: string) {
+    // removes a trailing slash if it exists
+    this.baseUrl = baseUrl.replace(/\/$/, '');
+  }
+
+  async startRegistration(userInfo: UserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(userInfo),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
+    };
+  }
+
+  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start-verified`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': userInfo.idToken,
+      },
+      body: JSON.stringify(userInfo.userInfo),
+    });
+    return await res.json();
+  }
+
+  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
+    const endpoint = `${this.baseUrl}/registration/finish`;
+    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        publicKeyCredential: encodedCredential,
+      }),
+    });
+    if (!res.ok) {
+      throw new Error(
+        `credential registration failed with ${res.status}: ${await res.text()}`,
+      );
+    }
+  }
+
+  async getDiscoverableCredentialRequestOptions() {
+    const endpoint = `${this.baseUrl}/authentication/discover`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+    });
+    return getRequestFromJSON(await res.json());
+  }
+
+  async startAuthentication(userId: string) {
+    const res = await fetch(`${this.baseUrl}/authentication/start`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ userId }),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
+    };
+  }
+
+  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const res = await fetch(`${this.baseUrl}/authentication/finish`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        userId,
+        publicKey: encodedCredential,
+      }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      throw new Error('invalid Cognito tokens');
+    }
+    return activateCognitoTokens(tokens);
+  }
+
+  async refreshTokens(refreshToken: string) {
+    const endpoint = `${this.baseUrl}/authentication/refresh`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ refreshToken }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      return undefined;
+    }
+    return activateCognitoTokens(tokens);
+  }
+}
+
+// encodes `PublicKeyCredential` for a registration API request body.
+//
+// replaces `ArrayBuffer`s with Base64-URL-encoded strings.
+function encodePublicKeyCredentialForCreation(
+  publicKey: PublicKeyCredential,
+): PublicKeyCredentialWithAttestationJSON {
path: app/src/utils/credentials-api-impl.ts
user: login: kikuomax
body: `encodePublicKeyCredentialForAuthentication` in `passquito-client.ts` only exists to produce an input to `extractPublicKeyInfo`. As `extractPublicKeyInfo` does not need the entire `PublicKeyCredential` encoded, I can remove `encodePublicKeyCredentialForAuthentication` from `passquito-client.ts` and make `extractPublicKeyInfo` encode only necessary fields.
created_at: 2025-04-27T04:44:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,191 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON ,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+// raw Cognito tokens returned from the API
+type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
+
+/**
+ * Default implementation of {@link CredentialsApi}.
+ *
+ * @beta
+ */
+export class CredentialsApiImpl implements CredentialsApi {
+  /** Base URL of the Credentials API. */
+  readonly baseUrl: string;
+
+  /** Initializes with a given base URL of the Credentials API. */
+  constructor(baseUrl: string) {
+    // removes a trailing slash if it exists
+    this.baseUrl = baseUrl.replace(/\/$/, '');
+  }
+
+  async startRegistration(userInfo: UserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(userInfo),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
+    };
+  }
+
+  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start-verified`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': userInfo.idToken,
+      },
+      body: JSON.stringify(userInfo.userInfo),
+    });
+    return await res.json();
+  }
+
+  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
+    const endpoint = `${this.baseUrl}/registration/finish`;
+    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        publicKeyCredential: encodedCredential,
+      }),
+    });
+    if (!res.ok) {
+      throw new Error(
+        `credential registration failed with ${res.status}: ${await res.text()}`,
+      );
+    }
+  }
+
+  async getDiscoverableCredentialRequestOptions() {
+    const endpoint = `${this.baseUrl}/authentication/discover`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+    });
+    return getRequestFromJSON(await res.json());
+  }
+
+  async startAuthentication(userId: string) {
+    const res = await fetch(`${this.baseUrl}/authentication/start`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ userId }),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
+    };
+  }
+
+  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const res = await fetch(`${this.baseUrl}/authentication/finish`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        userId,
+        publicKey: encodedCredential,
+      }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      throw new Error('invalid Cognito tokens');
+    }
+    return activateCognitoTokens(tokens);
+  }
+
+  async refreshTokens(refreshToken: string) {
+    const endpoint = `${this.baseUrl}/authentication/refresh`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ refreshToken }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      return undefined;
+    }
+    return activateCognitoTokens(tokens);
+  }
+}
+
+// encodes `PublicKeyCredential` for a registration API request body.
+//
+// replaces `ArrayBuffer`s with Base64-URL-encoded strings.
+function encodePublicKeyCredentialForCreation(
+  publicKey: PublicKeyCredential,
+): PublicKeyCredentialWithAttestationJSON {
path: app/src/utils/credentials-api-impl.ts
user: login: sourcery-ai[bot]
body: Thanks for the clarification regarding the usage in `passquito-client.ts`.

Refactoring `extractPublicKeyInfo` to handle the necessary encoding directly and removing the separate `encodePublicKeyCredentialForAuthentication` function in `passquito-client.ts` sounds like a good optimization for that specific use case. It avoids unnecessary intermediate encoding steps.

Regarding the code in `credentials-api-impl.ts` (which this comment refers to), extracting both `encodePublicKeyCredentialForCreation` and `encodePublicKeyCredentialForAuthentication` into a separate utility module still offers benefits:

1.  **Separation of Concerns:** It cleanly separates the data transformation logic (encoding credentials) from the API interaction logic within `CredentialsApiImpl`.
2.  **Readability:** It makes `CredentialsApiImpl` slightly shorter and more focused on its primary responsibility of making API calls.
3.  **Potential Reusability:** Even if `encodePublicKeyCredentialForAuthentication` isn't needed elsewhere after your refactoring, `encodePublicKeyCredentialForCreation` might still be, and having them colocated in a dedicated module makes sense.

So, I'd recommend proceeding with your plan to refactor `extractPublicKeyInfo` in `passquito-client.ts`, and also consider extracting the encoding functions from `credentials-api-impl.ts` into a helper module as originally suggested, to improve the structure of *this* file.
created_at: 2025-04-27T04:44:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
path: app/src/utils/passquito-client.ts
user: login: kikuomax
body: Yeah, the two implementations look very similar, and I should unify them. But I do not think your suggested code looks better understandable than the current one.
created_at: 2025-04-27T04:48:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: Thanks for the feedback! You're right, understandability is key, and sometimes abstractions, especially with callbacks or generics like in my initial suggestion, can obscure the flow rather than clarify it. My apologies if the example wasn't the clearest.

My main goal was to address the similar structure and particularly the duplicated `AbortController` logic in both `doAuthenticationCeremony` and `doAuthenticationCeremonyForUser`. Handling the abort signal consistently and avoiding repetition makes the code easier to maintain in the long run.

Perhaps the fully generic helper function was aiming too high. What if we take a smaller step and just extract the common pattern of creating an `AbortController` and wrapping the `navigator.credentials.get` call?

We could introduce a private helper method specifically for this:

```typescript
private createAbortableCredentialGetter<T extends PublicKeyCredential>(
    getOptions: PublicKeyCredentialRequestOptions | PublicKeyCredentialCreationOptions
): { abort: () => void; credentialPromise: Promise<T> } {
    const abortController = new AbortController();
    // Assuming 'conditional' mediation is always desired for 'get' here
    // Adjust as needed if creation options are passed
    const optionsWithSignal = { ...getOptions, signal: abortController.signal };
    if ('challenge' in optionsWithSignal) { // Basic check for PublicKeyCredentialRequestOptions
        optionsWithSignal.mediation = 'conditional';
    }

    const credentialPromise = ('publicKey' in getOptions ?
        navigator.credentials.create({ publicKey: optionsWithSignal }) : // Assuming 'create' if publicKey present
        navigator.credentials.get({ publicKey: optionsWithSignal }) // Assuming 'get' otherwise
    ) as Promise<T>; // Need type assertion here

    return {
        abort: () => abortController.abort(),
        credentialPromise,
    };
}

// Then the usage in doAuthenticationCeremony would look like:
async doAuthenticationCeremony() {
    // 1. Start Session
    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();

    // 2. Get Credential (Abortable)
    const { abort, credentialPromise } = this.createAbortableCredentialGetter<PublicKeyCredential>(options);

    // 3. Finish Session (chaining the promise)
    const credentials = credentialPromise.then(credential =>
        this.authenticateDiscoverablePublicKeyCredential(credential)
    );

    return { abort, credentials }; // Return abort handle and the final promise
}

// And in doAuthenticationCeremonyForUser:
async doAuthenticationCeremonyForUser(userId: string) {
    // 1. Start Session
    const session = await this.credentialsApi.startAuthentication(userId);

    // 2. Get Credential (Abortable)
    const { abort, credentialPromise } = this.createAbortableCredentialGetter<PublicKeyCredential>(session.credentialRequestOptions);

    // 3. Finish Session (chaining the promise)
    const credentials = credentialPromise.then(credential =>
        this.finishAuthenticationSession(session, credential)
    );

    return { abort, credentials };
}

// Similar pattern could apply to registration ceremonies too.
```
*(Note: The type handling and distinction between `create` and `get` within the helper might need refinement based on exact usage, but this shows the structural idea)*

This approach keeps the overall `start -> get -> finish` flow clearly visible within each ceremony method but centralizes the `AbortController` creation and the call to `navigator.credentials`.

Would something like this feel like a better balance between reducing redundancy and maintaining clarity? Happy to discuss further or explore other ways to refactor it!
created_at: 2025-04-27T04:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import { defineStore } from 'pinia';
+import { computed } from 'vue';
+
+import { PassquitoClient } from '../utils/passquito-client';
+
+import { useCredentialsApiStore } from './credentials-api';
+
+export const usePassquitoClientStore = defineStore('passquito-client', () => {
+  // Credentials API access
+  const credentialsApiStore = useCredentialsApiStore();
+  const api = computed(() => credentialsApiStore.api);
+
+  // Passquito client
+  const client = computed(() => new PassquitoClient(api.value));
path: app/src/stores/passquito-client.ts
user: login: sourcery-ai[bot]
body: **suggestion (performance):** Consider reusing a single PassquitoClient instance rather than instantiating a new one on each recomputation.

If the underlying API does not change often, a singleton or persistent instance could avoid unnecessary re-instantiation.

```suggestion
  const client = new PassquitoClient(api.value);
```
created_at: 2025-04-27T05:38:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,191 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON ,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+// raw Cognito tokens returned from the API
+type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
+
+/**
+ * Default implementation of {@link CredentialsApi}.
+ *
+ * @beta
+ */
+export class CredentialsApiImpl implements CredentialsApi {
+  /** Base URL of the Credentials API. */
+  readonly baseUrl: string;
+
+  /** Initializes with a given base URL of the Credentials API. */
+  constructor(baseUrl: string) {
+    // removes a trailing slash if it exists
+    this.baseUrl = baseUrl.replace(/\/$/, '');
+  }
+
+  async startRegistration(userInfo: UserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(userInfo),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
+    };
+  }
+
+  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start-verified`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': userInfo.idToken,
+      },
+      body: JSON.stringify(userInfo.userInfo),
+    });
+    return await res.json();
+  }
+
+  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
+    const endpoint = `${this.baseUrl}/registration/finish`;
+    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        publicKeyCredential: encodedCredential,
+      }),
+    });
+    if (!res.ok) {
+      throw new Error(
+        `credential registration failed with ${res.status}: ${await res.text()}`,
+      );
+    }
+  }
+
+  async getDiscoverableCredentialRequestOptions() {
+    const endpoint = `${this.baseUrl}/authentication/discover`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+    });
+    return getRequestFromJSON(await res.json());
+  }
+
+  async startAuthentication(userId: string) {
+    const res = await fetch(`${this.baseUrl}/authentication/start`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ userId }),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
+    };
+  }
+
+  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const res = await fetch(`${this.baseUrl}/authentication/finish`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        userId,
+        publicKey: encodedCredential,
+      }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      throw new Error('invalid Cognito tokens');
+    }
+    return activateCognitoTokens(tokens);
+  }
+
+  async refreshTokens(refreshToken: string) {
path: app/src/utils/credentials-api-impl.ts
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Improve error handling in refreshTokens.

Unlike finishRegistration where non-OK responses are explicitly handled, refreshTokens directly processes the JSON response. It might be worth checking res.ok to handle HTTP errors consistently.

Suggested implementation:

```typescript
    if (!res.ok) {
      throw new Error(`Failed to refresh tokens: ${res.status} ${res.statusText}`);
    }
    const tokens = await res.json();

```

Ensure that consistency is maintained across similar API calls; if finishRegistration applies additional error handling or logging, consider aligning refreshTokens with that strategy.
created_at: 2025-04-27T05:38:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,220 @@
+import { bufferToBase64url } from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
+export interface Credentials {
+  publicKeyInfo: PublicKeyInfo;
+  tokens: CognitoTokens;
+}
+
+/**
+ * Passquito client.
+ *
+ * @beta
+ */
+export class PassquitoClient {
+  /**
+   * Initializes with a given {@link CredentialsApi} instance.
+   *
+   * @param credentialsApi - Credentials API access.
+   */
+  constructor(private readonly credentialsApi: CredentialsApi) {}
+
+  /**
+   * Conducts a registration ceremony.
+   *
+   * @remarks
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremony(userInfo: UserInfo) {
+      const session = await this.credentialsApi.startRegistration(userInfo);
+      const credential = await navigator.credentials.create(session.credentialCreationOptions);
+      if (credential == null) {
+        throw new Error('failed to create a new credential');
+      }
+      await this.credentialsApi.finishRegistration(
+        session.sessionId,
+        credential as PublicKeyCredential,
+      );
+  }
+
+  /**
+   * Conducts a registration ceremony for a verified user.
+   *
+   * @remarks
+   *
+   * This ceremony is for a user who has been authenticated through a cross-device
+   * authentication and wants to register a new device (credential).
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const session = await this.credentialsApi.startRegistrationForVerifiedUser(userInfo);
+    const credential = await navigator.credentials.create(session.credentialCreationOptions);
+    if (credential == null) {
+      throw new Error('failed to create a new credential');
+    }
+    await this.credentialsApi.finishRegistration(
+      session.sessionId,
+      credential as PublicKeyCredential,
+    );
+  }
+
+  /**
+   * Conducts an authentication ceremony.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a function to abort the
+   * ceremony and a `Promise` of the credentials.
+   *
+   * Reference:
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion>
+   */
+  doAuthenticationCeremony() {
+    return runAbortableAuthentication((abortController) => {
+      return this.doAbortableAuthenticationCeremony(abortController);
+    });
+  }
+
+  /**
+   * Conducts an authentication ceremony for a given user.
+   *
+   * @remarks
+   *
+   * While the authentication ceremony itself is conducted in an asynchronous
+   * manner, this function synchronously returns a function to abort the
+   * ceremony and a `Promise` of the credentials.
+   */
+  doAuthenticationCeremonyForUser(userId: string) {
+    return runAbortableAuthentication((abortController) => {
+      return this.doAbortableAuthenticationCeremonyForUser(userId, abortController);
+    });
+  }
+
+  // conducts an authentication ceremony with a given AbortController.
+  private async doAbortableAuthenticationCeremony(
+    abortController: AbortController,
+  ) {
+    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();
+    const credential = await navigator.credentials.get({
+      ...options,
+      mediation: 'conditional',
+      signal: abortController.signal,
+    });
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Consider handling exceptions from navigator.credentials.get.

User cancellation or browser-level issues can throw errors; distinguishing these from other errors may help with clearer error handling downstream.

```suggestion
    const options = await this.credentialsApi.getDiscoverableCredentialRequestOptions();
    let credential;
    try {
      credential = await navigator.credentials.get({
        ...options,
        mediation: 'conditional',
        signal: abortController.signal,
      });
    } catch (err) {
      if (err instanceof DOMException && err.name === 'NotAllowedError') {
        // The error is likely due to a user cancellation or browser gesture issue.
        throw new Error('Authentication cancelled by user');
      }
      throw err;
    }
```
created_at: 2025-04-27T05:38:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,220 @@
+import { bufferToBase64url } from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the common abortable authentication logic into a helper function to reduce code duplication.

You can reduce the duplicated nesting in these similar ceremony flows by extracting the common abortable authentication logic into a helper function. For example, you can consolidate the two different authentication flows (for discoverable credentials and for a specific user) into a single helper that receives the ceremony’s “start” and “finish” functions as parameters. This avoids having near‐duplicate blocks for setting up the abortable call, handling mediator options, and validating the credential.

Below is a small example of such a refactoring for the authentication flows:

```ts
// New shared helper
private async performAbortableAuthCeremony<T>(
  startAuth: () => Promise<{ sessionId: string; credentialOptions: CredentialRequestOptions }>,
  finishAuth: (sessionId: string, credential: PublicKeyCredential) => Promise<Credentials>,
  abortSignal: AbortSignal
): Promise<Credentials> {
  const { sessionId, credentialOptions } = await startAuth();
  const credential = await navigator.credentials.get({
    ...credentialOptions,
    mediation: 'conditional',
    signal: abortSignal
  });
  return finishAuth(sessionId, credential as PublicKeyCredential);
}
```

Then, refactor the two public methods like below:

```ts
doAuthenticationCeremony() {
  return runAbortableAuthentication((abortController) => {
    return this.performAbortableAuthCeremony(
      () => this.credentialsApi.getDiscoverableCredentialRequestOptions().then(options => ({
             sessionId: options.sessionId,  // adapt as needed
             credentialOptions: options
           })),
      (sessionId, credential) => this.authenticateDiscoverablePublicKeyCredential(credential),
      abortController.signal
    );
  });
}

doAuthenticationCeremonyForUser(userId: string) {
  return runAbortableAuthentication((abortController) => {
    return this.performAbortableAuthCeremony(
      () => this.credentialsApi.startAuthentication(userId).then(session => ({
             sessionId: session.sessionId,
             credentialOptions: session.credentialRequestOptions
           })),
      (sessionId, credential) => this.finishAuthenticationSession({ sessionId }, credential),
      abortController.signal
    );
  });
}
```

Steps to adopt this refactor:
1. **Extract a common helper:** Create a method (e.g., `performAbortableAuthCeremony`) that handles fetching options, invoking `navigator.credentials.get`, and invoking a given finish callback.
2. **Parameterize start and finish functions:** This allows you to plug in both discoverable and user-specific flows.
3. **Adapt existing methods:** Refactor your `doAuthenticationCeremony` and `doAuthenticationCeremonyForUser` to simply call the new helper.

This approach centralizes the shared logic and reduces nesting/duplication while preserving the distinct “start” and “finish” behaviors for each flow.
created_at: 2025-04-27T05:38:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,220 @@
+import { bufferToBase64url } from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  PublicKeyInfo,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+/**
+ * Cognito tokens and the key info.
+ *
+ * @beta
+ */
+export interface Credentials {
+  publicKeyInfo: PublicKeyInfo;
+  tokens: CognitoTokens;
+}
+
+/**
+ * Passquito client.
+ *
+ * @beta
+ */
+export class PassquitoClient {
+  /**
+   * Initializes with a given {@link CredentialsApi} instance.
+   *
+   * @param credentialsApi - Credentials API access.
+   */
+  constructor(private readonly credentialsApi: CredentialsApi) {}
+
+  /**
+   * Conducts a registration ceremony.
+   *
+   * @remarks
+   *
+   * References:
+   * - <https://web.dev/articles/passkey-registration>
+   * - <https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential>
+   */
+  async doRegistrationCeremony(userInfo: UserInfo) {
path: app/src/utils/passquito-client.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the duplicate ceremony logic into helper methods to reduce code duplication and nesting.

Consider extracting the duplicate ceremony logic into helper methods. For example, both registration ceremonies share almost identical logic. You might create a generic registration helper that takes a session retrieval callback. Here's a short example:

```ts
// Extract common registration flow
private async runRegistration(
  sessionPromise: Promise<{ sessionId: string; credentialCreationOptions: PublicKeyCredentialCreationOptions }>
) {
  const session = await sessionPromise;
  const credential = await navigator.credentials.create(session.credentialCreationOptions);
  if (!credential) {
    throw new Error('failed to create a new credential');
  }
  await this.credentialsApi.finishRegistration(session.sessionId, credential as PublicKeyCredential);
}

// Then update the public methods
async doRegistrationCeremony(userInfo: UserInfo) {
  return this.runRegistration(this.credentialsApi.startRegistration(userInfo));
}

async doRegistrationCeremonyForVerifiedUser(userInfo: VerifiedUserInfo) {
  return this.runRegistration(this.credentialsApi.startRegistrationForVerifiedUser(userInfo));
}
```

Similarly, you can extract shared logic from the authentication ceremonies (for example, processing the returned credential and validating the user handle) into a common method. This would reduce nesting and duplication without changing the functionality.
created_at: 2025-04-27T05:52:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,191 @@
+import { parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import {
+  bufferToBase64url,
+  convert,
+  getRequestFromJSON,
+  schema,
+  type PublicKeyCredentialWithAssertionJSON ,
+  type PublicKeyCredentialWithAttestationJSON,
+} from '@github/webauthn-json/extended';
+
+import type { CredentialsApi } from './credentials-api';
+import type {
+  CognitoTokens,
+  UserInfo,
+  VerifiedUserInfo,
+} from './passquito-types';
+
+// raw Cognito tokens returned from the API
+type RawCognitoTokens = Omit<CognitoTokens, 'activatedAt'>;
+
+/**
+ * Default implementation of {@link CredentialsApi}.
+ *
+ * @beta
+ */
+export class CredentialsApiImpl implements CredentialsApi {
+  /** Base URL of the Credentials API. */
+  readonly baseUrl: string;
+
+  /** Initializes with a given base URL of the Credentials API. */
+  constructor(baseUrl: string) {
+    // removes a trailing slash if it exists
+    this.baseUrl = baseUrl.replace(/\/$/, '');
+  }
+
+  async startRegistration(userInfo: UserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(userInfo),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialCreationOptions: parseCreationOptionsFromJSON(session.credentialCreationOptions),
+    };
+  }
+
+  async startRegistrationForVerifiedUser(userInfo: VerifiedUserInfo) {
+    const endpoint = `${this.baseUrl}/registration/start-verified`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': userInfo.idToken,
+      },
+      body: JSON.stringify(userInfo.userInfo),
+    });
+    return await res.json();
+  }
+
+  async finishRegistration(sessionId: string, credential: PublicKeyCredential) {
+    const endpoint = `${this.baseUrl}/registration/finish`;
+    const encodedCredential = encodePublicKeyCredentialForCreation(credential);
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        publicKeyCredential: encodedCredential,
+      }),
+    });
+    if (!res.ok) {
+      throw new Error(
+        `credential registration failed with ${res.status}: ${await res.text()}`,
+      );
+    }
+  }
+
+  async getDiscoverableCredentialRequestOptions() {
+    const endpoint = `${this.baseUrl}/authentication/discover`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+    });
+    return getRequestFromJSON(await res.json());
+  }
+
+  async startAuthentication(userId: string) {
+    const res = await fetch(`${this.baseUrl}/authentication/start`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ userId }),
+    });
+    const session = await res.json();
+    return {
+      sessionId: session.sessionId,
+      credentialRequestOptions: getRequestFromJSON(session.credentialRequestOptions),
+    };
+  }
+
+  async finishAuthentication(sessionId: string, userId: string, credential: PublicKeyCredential) {
+    const encodedCredential = encodePublicKeyCredentialForAuthentication(credential);
+    const res = await fetch(`${this.baseUrl}/authentication/finish`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        sessionId,
+        userId,
+        publicKey: encodedCredential,
+      }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      throw new Error('invalid Cognito tokens');
+    }
+    return activateCognitoTokens(tokens);
+  }
+
+  async refreshTokens(refreshToken: string) {
+    const endpoint = `${this.baseUrl}/authentication/refresh`;
+    const res = await fetch(endpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ refreshToken }),
+    });
+    const tokens = await res.json();
+    if (!isRawCognitoTokens(tokens)) {
+      return undefined;
+    }
+    return activateCognitoTokens(tokens);
+  }
+}
+
+// encodes `PublicKeyCredential` for a registration API request body.
+//
+// replaces `ArrayBuffer`s with Base64-URL-encoded strings.
+function encodePublicKeyCredentialForCreation(
+  publicKey: PublicKeyCredential,
+): PublicKeyCredentialWithAttestationJSON {
path: app/src/utils/credentials-api-impl.ts
user: login: kikuomax
body: I think the refactoring of `extractPublicKeyInfo` is enough.
created_at: 2025-04-27T05:55:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+import { PassquitoClient } from '../utils/passquito-client';
+
+import { useCredentialsApiStore } from './credentials-api';
+
+export const usePassquitoClientStore = defineStore('passquito-client', () => {
+  // Credentials API access
+  const credentialsApiStore = useCredentialsApiStore();
+
+  // Passquito client
+  const client = ref(new PassquitoClient(credentialsApiStore.api));
path: app/src/stores/passquito-client.ts
user: login: Copilot
body: The PassquitoClient constructor expects a CredentialsApi instance, but credentialsApiStore.api is returned as a ref. Consider unwrapping the ref (using credentialsApiStore.api.value) to ensure the proper instance is passed.
```suggestion
  const client = ref(new PassquitoClient(credentialsApiStore.api.value));
```
created_at: 2025-04-28T08:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+import { PassquitoClient } from '../utils/passquito-client';
+
+import { useCredentialsApiStore } from './credentials-api';
+
+export const usePassquitoClientStore = defineStore('passquito-client', () => {
+  // Credentials API access
+  const credentialsApiStore = useCredentialsApiStore();
+
+  // Passquito client
+  const client = ref(new PassquitoClient(credentialsApiStore.api));
path: app/src/stores/passquito-client.ts
user: login: kikuomax
body: As `credentialsApiStore` is a Pinia store, we do not have to unwrap its state `api`.
created_at: 2025-04-28T18:15:17Z
author_association: MEMBER

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-27T04:09:49Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request refactors passkey handling by introducing dedicated classes for client-side logic (`PassquitoClient`) and API interaction (`CredentialsApiImpl`). It replaces the previous global API instance with Pinia stores (`usePassquitoClientStore`, `useCredentialsApiStore`) to manage and provide instances of these new classes, improving dependency management. The core passkey registration and authentication ceremony implementations are moved into the `PassquitoClient`. Shared types are consolidated, and a redundant utility function is removed. Views are updated to consume the passkey functionality via the new Pinia stores.

_No diagrams generated as the changes look simple and do not need a visual representation._

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Introduce dedicated classes for passkey client logic and API implementation. | <ul><li>Create a new `PassquitoClient` class to encapsulate passkey registration and authentication ceremony logic.</li><li>Create a new `CredentialsApiImpl` class to handle interactions with the credentials API endpoints.</li></ul> | `app/src/utils/passquito-client.ts`<br/>`app/src/utils/credentials-api-impl.ts` |
| Implement Pinia stores to manage and provide instances of the new classes. | <ul><li>Define `usePassquitoClientStore` to provide access to the `PassquitoClient` instance.</li><li>Define `useCredentialsApiStore` to provide access to the `CredentialsApiImpl` instance, initialized with the API URL.</li></ul> | `app/src/stores/passquito-client.ts`<br/>`app/src/stores/credentials-api.ts` |
| Refactor the main passquito utility file, removing old implementations and the global API instance. | <ul><li>Remove the global `credentialsApi` instance.</li><li>Remove the direct implementations of registration and authentication ceremony functions.</li><li>Remove the redundant `refreshTokens` utility function.</li></ul> | `app/src/utils/passquito.ts` |
| Update views to utilize the new Pinia stores for passkey operations. | <ul><li>Import and use `usePassquitoClientStore` for registration and authentication ceremonies.</li><li>Import and use `useCredentialsApiStore` for refreshing tokens.</li></ul> | `app/src/views/SignIn.vue`<br/>`app/src/views/RegisterDevice.vue`<br/>`app/src/views/Secured.vue`<br/>`app/src/views/SignUp.vue` |
| Consolidate shared passquito-related types. | <ul><li>Move `PublicKeyInfo` and `CognitoTokens` interfaces to the types file.</li><li>Move `isPublicKeyInfo` and `isCognitoTokens` type guard functions to the types file.</li></ul> | `app/src/utils/passquito-types.ts`<br/>`app/src/utils/passquito.ts` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: kikuomax
created_at: 2025-04-27T05:36:59Z
body: @sourcery-ai review.
author_association: MEMBER
user: login: kikuomax
created_at: 2025-04-27T05:50:58Z
body: @sourcery-ai review.
author_association: MEMBER
user: login: kikuomax
created_at: 2025-04-28T08:42:09Z
body: @sourcery-ai review.
author_association: MEMBER