expo-passkey
Version:
Passkey authentication for Expo apps with Better Auth integration
178 lines • 8.53 kB
JavaScript
/**
* @file List passkeys endpoint
* @description Implementation of the endpoint to list user passkeys with WebAuthn info
*/
import { createAuthEndpoint, sessionMiddleware } from "better-auth/api";
import { APIError } from "better-call";
import { ERROR_CODES, ERROR_MESSAGES } from "../../types/errors";
import { listPasskeysParamsSchema, listPasskeysQuerySchema, } from "../utils/schema";
/**
* Create endpoint to list user passkeys
*/
export const createListEndpoint = (options) => {
const { logger, schemaConfig } = options;
return createAuthEndpoint("/expo-passkey/list/:userId", {
method: "GET",
params: listPasskeysParamsSchema,
query: listPasskeysQuerySchema,
use: [sessionMiddleware],
metadata: {
openapi: {
description: "Retrieve a list of registered passkeys for the user",
tags: ["Authentication"],
responses: {
200: {
description: "List of passkeys",
content: {
"application/json": {
schema: {
type: "object",
properties: {
passkeys: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
userId: { type: "string" },
credentialId: { type: "string" },
platform: { type: "string" },
lastUsed: {
type: "string",
format: "date-time",
},
status: {
type: "string",
enum: ["active", "revoked"],
},
aaguid: {
type: "string",
nullable: true,
},
createdAt: {
type: "string",
format: "date-time",
},
updatedAt: {
type: "string",
format: "date-time",
},
revokedAt: {
type: "string",
format: "date-time",
nullable: true,
},
revokedReason: {
type: "string",
nullable: true,
},
metadata: {
type: "object",
additionalProperties: true,
},
},
},
},
nextOffset: { type: "number", nullable: true },
},
},
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: {
type: "object",
properties: {
code: { type: "string" },
message: { type: "string" },
},
},
},
},
},
},
},
},
},
},
}, async (ctx) => {
try {
const userId = ctx.params.userId;
const limit = parseInt(ctx.query.limit || "10", 10);
const offset = ctx.query.offset ? parseInt(ctx.query.offset, 10) : 0;
logger.debug("Passkey list request received:", {
userId,
limit,
offset,
});
// Verify session matches userId for security
if (ctx.context.session?.user?.id !== userId) {
logger.warn("Unauthorized attempt to list passkeys", {
requestedUserId: userId,
sessionUserId: ctx.context.session?.user?.id,
});
throw new APIError("UNAUTHORIZED", {
code: "UNAUTHORIZED_ACCESS",
message: "You can only view your own passkeys",
});
}
// Fetch passkeys with pagination
const passkeys = await ctx.context.adapter.findMany({
model: schemaConfig.authPasskeyModel,
where: [
{ field: "userId", operator: "eq", value: userId },
{ field: "status", operator: "eq", value: "active" },
],
sortBy: { field: "lastUsed", direction: "desc" },
limit: limit + 1,
offset,
});
// Check if there are more results
const hasMore = passkeys.length > limit;
const results = hasMore ? passkeys.slice(0, limit) : passkeys;
// Format passkeys for response
const formattedPasskeys = results.map((passkey) => ({
id: passkey.id,
userId: passkey.userId,
credentialId: passkey.credentialId,
platform: passkey.platform,
lastUsed: passkey.lastUsed,
status: passkey.status,
aaguid: passkey.aaguid || null,
createdAt: passkey.createdAt,
updatedAt: passkey.updatedAt,
revokedAt: passkey.revokedAt,
revokedReason: passkey.revokedReason,
metadata: passkey.metadata
? JSON.parse(passkey.metadata)
: {},
}));
logger.debug("Returning passkeys:", {
count: formattedPasskeys.length,
hasMore,
nextOffset: hasMore ? offset + limit : undefined,
});
return ctx.json({
passkeys: formattedPasskeys,
nextOffset: hasMore ? offset + limit : undefined,
});
}
catch (error) {
logger.error("Error listing passkeys:", error);
if (error instanceof APIError) {
throw error;
}
throw new APIError("INTERNAL_SERVER_ERROR", {
code: ERROR_CODES.SERVER.PASSKEYS_RETRIEVAL_FAILED,
message: ERROR_MESSAGES[ERROR_CODES.SERVER.PASSKEYS_RETRIEVAL_FAILED],
});
}
});
};
//# sourceMappingURL=list.js.map