diff --git a/.agents/plans/swift-pink-sky-simplewebauthn-v13-upgrade.md b/.agents/plans/swift-pink-sky-simplewebauthn-v13-upgrade.md
new file mode 100644
index 000000000..8ca225c13
--- /dev/null
+++ b/.agents/plans/swift-pink-sky-simplewebauthn-v13-upgrade.md
@@ -0,0 +1,186 @@
+---
+date: 2026-01-14
+title: Simplewebauthn V13 Upgrade
+---
+
+## Overview
+
+Upgrade SimpleWebAuthn packages from v9.x to v13.x to address the deprecation of `@simplewebauthn/types` and take advantage of new features and improvements.
+
+## Current State
+
+The codebase currently uses:
+- `@simplewebauthn/browser@9.x`
+- `@simplewebauthn/server@9.x`
+- `@simplewebauthn/types@9.x`
+
+## Breaking Changes Summary (v9 → v13)
+
+### v10.0.0 Breaking Changes
+1. **Minimum Node version raised to Node v20**
+2. **`generateRegistrationOptions()` now expects `Base64URLString` for `excludeCredentials` IDs** (no more `type: 'public-key'` needed)
+3. **`generateAuthenticationOptions()` now expects `Base64URLString` for `allowCredentials` IDs**
+4. **`credentialID` returned from verification methods is now `Base64URLString`** instead of `Uint8Array`
+5. **`AuthenticatorDevice.credentialID` is now `Base64URLString`**
+6. **`rpID` is now required when calling `generateAuthenticationOptions()`**
+7. **`generateRegistrationOptions()` will generate random user IDs** if not provided
+8. **`user.id` is treated as base64url string in `startRegistration()`**
+9. **`userHandle` is treated as base64url string in `startAuthentication()`**
+
+### v11.0.0 Breaking Changes
+1. **Positional arguments in `startRegistration()` and `startAuthentication()` replaced by object**
+ - Before: `startRegistration(options)`
+ - After: `startRegistration({ optionsJSON: options })`
+ - Before: `startAuthentication(options)`
+ - After: `startAuthentication({ optionsJSON: options })`
+2. **`AuthenticatorDevice` type renamed to `WebAuthnCredential`**
+ - `credentialID` → `credential.id`
+ - `credentialPublicKey` → `credential.publicKey`
+3. **`verifyRegistrationResponse()` returns `registrationInfo.credential` instead of individual properties**
+ - `credentialID` → `credential.id`
+ - `credentialPublicKey` → `credential.publicKey`
+ - `counter` → `credential.counter`
+ - `transports` are now in `credential.transports`
+4. **`verifyAuthenticationResponse()` uses `credential` argument instead of `authenticator`**
+
+### v13.0.0 Breaking Changes
+1. **`@simplewebauthn/types` package is retired**
+ - Types are now exported from `@simplewebauthn/browser` and `@simplewebauthn/server`
+ - Import types from `@simplewebauthn/server` instead
+
+## Files to Update
+
+### Package Changes
+1. Remove `@simplewebauthn/types` dependency
+2. Update `@simplewebauthn/browser` to `^13.2.2`
+3. Update `@simplewebauthn/server` to `^13.2.2`
+
+### Server-side Files
+
+#### 1. `packages/lib/server-only/auth/create-passkey-registration-options.ts`
+- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
+- Remove `type: 'public-key'` from `excludeCredentials` items
+- Update `userID` to use `isoUint8Array.fromUTF8String()` for proper encoding
+
+#### 2. `packages/lib/server-only/auth/create-passkey-authentication-options.ts`
+- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
+- Remove `type: 'public-key'` from `allowCredentials` items
+
+#### 3. `packages/lib/server-only/auth/create-passkey-signin-options.ts`
+- No changes needed (already using correct options)
+
+#### 4. `packages/lib/server-only/auth/create-passkey.ts`
+- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
+- Update to use new `registrationInfo.credential` structure:
+ - `credentialID` → `credential.id`
+ - `credentialPublicKey` → `credential.publicKey`
+ - `counter` → `credential.counter`
+- Note: `credential.id` is now a `Base64URLString`, so `Buffer.from(credentialID)` needs updating
+
+#### 5. `packages/lib/server-only/document/is-recipient-authorized.ts`
+- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`:
+ - Change `authenticator: { credentialID, credentialPublicKey, counter }` to `credential: { id, publicKey, counter }`
+- Since `credential.id` is now base64url string, convert stored `credentialId` buffer to base64url
+
+#### 6. `packages/auth/server/routes/passkey.ts`
+- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`
+- Same changes as `is-recipient-authorized.ts`
+
+#### 7. `packages/trpc/server/auth-router/create-passkey.ts`
+- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
+
+### Browser-side Files
+
+#### 8. `apps/remix/app/components/dialogs/passkey-create-dialog.tsx`
+- Update `startRegistration()` call:
+ - Before: `startRegistration(passkeyRegistrationOptions)`
+ - After: `startRegistration({ optionsJSON: passkeyRegistrationOptions })`
+
+#### 9. `apps/remix/app/components/forms/signin.tsx`
+- Update `startAuthentication()` call:
+ - Before: `startAuthentication(options)`
+ - After: `startAuthentication({ optionsJSON: options })`
+
+#### 10. `apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx`
+- Update `startAuthentication()` call:
+ - Before: `startAuthentication(options)`
+ - After: `startAuthentication({ optionsJSON: options })`
+
+### Database/Schema Considerations
+
+The database stores `credentialId` as `Bytes`. The new API returns `credential.id` as `Base64URLString`. We need to:
+1. When **storing** a new passkey: Convert from `Base64URLString` to `Buffer`
+2. When **passing to verification**: Convert from `Buffer` to `Base64URLString`
+
+Use `isoBase64URL` helper from `@simplewebauthn/server/helpers` for these conversions.
+
+## Implementation Steps
+
+### Step 1: Update package.json dependencies
+```bash
+npm uninstall @simplewebauthn/types
+npm install @simplewebauthn/browser@^13.2.2 @simplewebauthn/server@^13.2.2
+```
+
+### Step 2: Update type imports
+Replace all `@simplewebauthn/types` imports with `@simplewebauthn/server`
+
+### Step 3: Update browser-side API calls
+- `startRegistration(options)` → `startRegistration({ optionsJSON: options })`
+- `startAuthentication(options)` → `startAuthentication({ optionsJSON: options })`
+
+### Step 4: Update server-side registration
+- Update `excludeCredentials` format (remove `type: 'public-key'`)
+- Update `userID` encoding if needed
+- Update `verifyRegistrationResponse()` result handling for new `credential` structure
+
+### Step 5: Update server-side authentication
+- Update `allowCredentials` format (remove `type: 'public-key'`)
+- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`
+- Handle `Base64URLString` for `credential.id`
+
+### Step 6: Update credential storage/retrieval
+- When storing: Convert `Base64URLString` to `Buffer`
+- When reading: Convert `Buffer` to `Base64URLString`
+
+### Step 7: Test passkey flows
+1. Test passkey creation
+2. Test passkey sign-in
+3. Test passkey authentication for document signing
+4. Test passkey deletion
+
+## Code Examples
+
+### Converting stored Buffer to Base64URLString for verification
+```typescript
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
+
+// When reading from database (Buffer) and passing to verification
+const credential = {
+ id: isoBase64URL.fromBuffer(passkey.credentialId),
+ publicKey: new Uint8Array(passkey.credentialPublicKey),
+ counter: Number(passkey.counter),
+ transports: passkey.transports,
+};
+```
+
+### Converting Base64URLString to Buffer for storage
+```typescript
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
+
+// When storing from registration response
+const credentialIdBuffer = Buffer.from(
+ isoBase64URL.toBuffer(registrationInfo.credential.id)
+);
+```
+
+## Risks and Mitigations
+
+1. **Database compatibility**: The `credentialId` is stored as `Bytes` in the database. The new API uses `Base64URLString`. We need proper conversion functions.
+ - **Mitigation**: Use `isoBase64URL.fromBuffer()` and `isoBase64URL.toBuffer()` for conversions
+
+2. **Existing passkeys**: Existing passkeys should continue to work as long as conversion is done correctly.
+ - **Mitigation**: Test with existing passkeys after upgrade
+
+3. **Browser compatibility**: v10+ requires newer browser APIs.
+ - **Mitigation**: `browserSupportsWebAuthn()` already handles this check
\ No newline at end of file
diff --git a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx
index 77bd54b57..541d2d4c7 100644
--- a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx
+++ b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx
@@ -73,7 +73,9 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
try {
const passkeyRegistrationOptions = await createPasskeyRegistrationOptions();
- const registrationResult = await startRegistration(passkeyRegistrationOptions);
+ const registrationResult = await startRegistration({
+ optionsJSON: passkeyRegistrationOptions,
+ });
await createPasskey({
passkeyName,
diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx
index 625b92af5..6338512fe 100644
--- a/apps/remix/app/components/forms/signin.tsx
+++ b/apps/remix/app/components/forms/signin.tsx
@@ -171,7 +171,7 @@ export const SignInForm = ({
const { options, sessionId } = await createPasskeySigninOptions();
- const credential = await startAuthentication(options);
+ const credential = await startAuthentication({ optionsJSON: options });
await authClient.passkey.signIn({
credential: JSON.stringify(credential),
diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
index 930738c74..9cdb2e28a 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
@@ -90,7 +90,7 @@ export const DocumentSigningAuthPasskey = ({
preferredPasskeyId: passkeyId,
});
- const authenticationResponse = await startAuthentication(options);
+ const authenticationResponse = await startAuthentication({ optionsJSON: options });
await onReauthFormSubmit({
type: DocumentAuth.PASSKEY,
@@ -146,7 +146,7 @@ export const DocumentSigningAuthPasskey = ({
if (passkeyData.isInitialLoading || (passkeyData.isError && passkeyData.passkeys.length === 0)) {
return (
-
+
);
}
diff --git a/apps/remix/package.json b/apps/remix/package.json
index 1308dfc46..23be27c2d 100644
--- a/apps/remix/package.json
+++ b/apps/remix/package.json
@@ -38,8 +38,8 @@
"@oslojs/encoding": "^1.1.0",
"@react-router/node": "^7.12.0",
"@react-router/serve": "^7.12.0",
- "@simplewebauthn/browser": "^9.0.1",
- "@simplewebauthn/server": "^9.0.3",
+ "@simplewebauthn/browser": "^13.2.2",
+ "@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"autoprefixer": "^10.4.22",
"colord": "^2.9.3",
@@ -88,7 +88,6 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
- "@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^3.4.6",
"@types/luxon": "^3.7.1",
diff --git a/package-lock.json b/package-lock.json
index 6e9d47501..d898bd9ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -135,8 +135,8 @@
"@oslojs/encoding": "^1.1.0",
"@react-router/node": "^7.12.0",
"@react-router/serve": "^7.12.0",
- "@simplewebauthn/browser": "^9.0.1",
- "@simplewebauthn/server": "^9.0.3",
+ "@simplewebauthn/browser": "^13.2.2",
+ "@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"autoprefixer": "^10.4.22",
"colord": "^2.9.3",
@@ -185,7 +185,6 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
- "@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^3.4.6",
"@types/luxon": "^3.7.1",
@@ -11944,6 +11943,31 @@
"tslib": "^2.8.1"
}
},
+ "node_modules/@peculiar/asn1-cms": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz",
+ "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "@peculiar/asn1-x509-attr": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-csr": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz",
+ "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
"node_modules/@peculiar/asn1-ecc": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz",
@@ -11956,6 +11980,48 @@
"tslib": "^2.8.1"
}
},
+ "node_modules/@peculiar/asn1-pfx": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz",
+ "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.0",
+ "@peculiar/asn1-pkcs8": "^2.6.0",
+ "@peculiar/asn1-rsa": "^2.6.0",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs8": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz",
+ "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs9": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz",
+ "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.0",
+ "@peculiar/asn1-pfx": "^2.6.0",
+ "@peculiar/asn1-pkcs8": "^2.6.0",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "@peculiar/asn1-x509-attr": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
"node_modules/@peculiar/asn1-rsa": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz",
@@ -11991,6 +12057,40 @@
"tslib": "^2.8.1"
}
},
+ "node_modules/@peculiar/asn1-x509-attr": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz",
+ "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/x509": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
+ "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.0",
+ "@peculiar/asn1-csr": "^2.6.0",
+ "@peculiar/asn1-ecc": "^2.6.0",
+ "@peculiar/asn1-pkcs9": "^2.6.0",
+ "@peculiar/asn1-rsa": "^2.6.0",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "pvtsutils": "^1.3.6",
+ "reflect-metadata": "^0.2.2",
+ "tslib": "^2.8.1",
+ "tsyringe": "^4.10.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@pinojs/redact": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
@@ -15177,18 +15277,15 @@
"license": "MIT"
},
"node_modules/@simplewebauthn/browser": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz",
- "integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==",
- "license": "MIT",
- "dependencies": {
- "@simplewebauthn/types": "^9.0.1"
- }
+ "version": "13.2.2",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz",
+ "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==",
+ "license": "MIT"
},
"node_modules/@simplewebauthn/server": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-9.0.3.tgz",
- "integrity": "sha512-FMZieoBosrVLFxCnxPFD9Enhd1U7D8nidVDT4MsHc6l4fdVcjoeHjDueeXCloO1k5O/fZg1fsSXXPKbY2XTzDA==",
+ "version": "13.2.2",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz",
+ "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==",
"license": "MIT",
"dependencies": {
"@hexagon/base64": "^1.1.27",
@@ -15198,20 +15295,12 @@
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
- "@simplewebauthn/types": "^9.0.1",
- "cross-fetch": "^4.0.0"
+ "@peculiar/x509": "^1.13.0"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">=20.0.0"
}
},
- "node_modules/@simplewebauthn/types": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz",
- "integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==",
- "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
- "license": "MIT"
- },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -32199,6 +32288,12 @@
"@babel/runtime": "^7.9.2"
}
},
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "license": "Apache-2.0"
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -35224,6 +35319,24 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/tsyringe": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
+ "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/tsyringe/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
"node_modules/turbo": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.13.4.tgz",
@@ -37005,6 +37118,7 @@
"@hono/standard-validator": "^0.2.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
+ "@simplewebauthn/server": "^13.2.2",
"arctic": "^3.7.0",
"hono": "4.10.6",
"luxon": "^3.7.2",
@@ -37359,6 +37473,7 @@
"@node-rs/bcrypt": "^1.10.7",
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.2.6",
+ "@simplewebauthn/server": "^13.2.2",
"@sindresorhus/slugify": "^3.0.0",
"@team-plain/typescript-sdk": "^5.11.0",
"@vvo/tzdb": "^6.196.0",
@@ -37461,6 +37576,7 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
+ "@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"@trpc/client": "11.8.1",
"@trpc/react-query": "11.8.1",
diff --git a/package.json b/package.json
index 3c38f26bc..7dff318d6 100644
--- a/package.json
+++ b/package.json
@@ -103,4 +103,4 @@
"typescript": "5.6.2",
"zod": "^3.25.76"
}
-}
+}
\ No newline at end of file
diff --git a/packages/auth/package.json b/packages/auth/package.json
index 7566d6eb8..0c3dda9a1 100644
--- a/packages/auth/package.json
+++ b/packages/auth/package.json
@@ -15,6 +15,7 @@
"@hono/standard-validator": "^0.2.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
+ "@simplewebauthn/server": "^13.2.2",
"arctic": "^3.7.0",
"hono": "4.10.6",
"luxon": "^3.7.2",
@@ -22,4 +23,4 @@
"ts-pattern": "^5.9.0",
"zod": "^3.25.76"
}
-}
\ No newline at end of file
+}
diff --git a/packages/auth/server/routes/passkey.ts b/packages/auth/server/routes/passkey.ts
index 0d0b43854..1bb3e16de 100644
--- a/packages/auth/server/routes/passkey.ts
+++ b/packages/auth/server/routes/passkey.ts
@@ -1,6 +1,7 @@
import { sValidator } from '@hono/standard-validator';
import { UserSecurityAuditLogType } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { Hono } from 'hono';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@@ -80,9 +81,9 @@ export const passkeyRoute = new Hono()
expectedChallenge: challengeToken.token,
expectedOrigin: origin,
expectedRPID: rpId,
- authenticator: {
- credentialID: new Uint8Array(Array.from(passkey.credentialId)),
- credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
+ credential: {
+ id: isoBase64URL.fromBuffer(passkey.credentialId),
+ publicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
},
}).catch(() => null);
diff --git a/packages/lib/package.json b/packages/lib/package.json
index b48a77fb7..f324f258e 100644
--- a/packages/lib/package.json
+++ b/packages/lib/package.json
@@ -34,6 +34,7 @@
"@node-rs/bcrypt": "^1.10.7",
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.2.6",
+ "@simplewebauthn/server": "^13.2.2",
"@sindresorhus/slugify": "^3.0.0",
"@team-plain/typescript-sdk": "^5.11.0",
"@vvo/tzdb": "^6.196.0",
diff --git a/packages/lib/server-only/auth/create-passkey-authentication-options.ts b/packages/lib/server-only/auth/create-passkey-authentication-options.ts
index 2c35f46d7..3efe84041 100644
--- a/packages/lib/server-only/auth/create-passkey-authentication-options.ts
+++ b/packages/lib/server-only/auth/create-passkey-authentication-options.ts
@@ -1,6 +1,7 @@
import type { Passkey } from '@prisma/client';
import { generateAuthenticationOptions } from '@simplewebauthn/server';
-import type { AuthenticatorTransportFuture } from '@simplewebauthn/types';
+import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
@@ -53,8 +54,7 @@ export const createPasskeyAuthenticationOptions = async ({
allowCredentials: preferredPasskey
? [
{
- id: preferredPasskey.credentialId,
- type: 'public-key',
+ id: isoBase64URL.fromBuffer(preferredPasskey.credentialId),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
transports: preferredPasskey.transports as AuthenticatorTransportFuture[],
},
diff --git a/packages/lib/server-only/auth/create-passkey-registration-options.ts b/packages/lib/server-only/auth/create-passkey-registration-options.ts
index 8f2b3d53a..8c8895ed4 100644
--- a/packages/lib/server-only/auth/create-passkey-registration-options.ts
+++ b/packages/lib/server-only/auth/create-passkey-registration-options.ts
@@ -1,5 +1,6 @@
import { generateRegistrationOptions } from '@simplewebauthn/server';
-import type { AuthenticatorTransportFuture } from '@simplewebauthn/types';
+import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
@@ -32,14 +33,13 @@ export const createPasskeyRegistrationOptions = async ({
const options = await generateRegistrationOptions({
rpName,
rpID,
- userID: userId.toString(),
+ userID: Buffer.from(userId.toString()),
userName: user.email,
userDisplayName: user.name ?? undefined,
timeout: PASSKEY_TIMEOUT,
attestationType: 'none',
excludeCredentials: passkeys.map((passkey) => ({
- id: passkey.credentialId,
- type: 'public-key',
+ id: isoBase64URL.fromBuffer(passkey.credentialId),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
transports: passkey.transports as AuthenticatorTransportFuture[],
})),
diff --git a/packages/lib/server-only/auth/create-passkey.ts b/packages/lib/server-only/auth/create-passkey.ts
index 5aa6a138c..7ac53d1d8 100644
--- a/packages/lib/server-only/auth/create-passkey.ts
+++ b/packages/lib/server-only/auth/create-passkey.ts
@@ -1,6 +1,7 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { verifyRegistrationResponse } from '@simplewebauthn/server';
-import type { RegistrationResponseJSON } from '@simplewebauthn/types';
+import type { RegistrationResponseJSON } from '@simplewebauthn/server';
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { prisma } from '@documenso/prisma';
@@ -83,20 +84,19 @@ export const createPasskey = async ({
});
}
- const { credentialPublicKey, credentialID, counter, credentialDeviceType, credentialBackedUp } =
- verification.registrationInfo;
+ const { credentialDeviceType, credentialBackedUp, credential } = verification.registrationInfo;
await prisma.$transaction(async (tx) => {
await tx.passkey.create({
data: {
userId,
name: passkeyName,
- credentialId: Buffer.from(credentialID),
- credentialPublicKey: Buffer.from(credentialPublicKey),
- counter,
+ credentialId: Buffer.from(isoBase64URL.toBuffer(credential.id)),
+ credentialPublicKey: Buffer.from(credential.publicKey),
+ counter: credential.counter,
credentialDeviceType,
credentialBackedUp,
- transports: verificationResponse.response.transports,
+ transports: credential.transports,
},
});
diff --git a/packages/lib/server-only/document/is-recipient-authorized.ts b/packages/lib/server-only/document/is-recipient-authorized.ts
index d19880372..affdcc0d5 100644
--- a/packages/lib/server-only/document/is-recipient-authorized.ts
+++ b/packages/lib/server-only/document/is-recipient-authorized.ts
@@ -1,5 +1,6 @@
import type { Envelope, Recipient } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
@@ -252,9 +253,9 @@ const verifyPasskey = async ({
expectedChallenge: verificationToken.token,
expectedOrigin: origin,
expectedRPID: rpId,
- authenticator: {
- credentialID: new Uint8Array(Array.from(passkey.credentialId)),
- credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
+ credential: {
+ id: isoBase64URL.fromBuffer(passkey.credentialId),
+ publicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
},
}).catch(() => null); // May want to log this for insights.
diff --git a/packages/trpc/package.json b/packages/trpc/package.json
index fadc8d658..139ee58f2 100644
--- a/packages/trpc/package.json
+++ b/packages/trpc/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
+ "@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"@trpc/client": "11.8.1",
"@trpc/react-query": "11.8.1",
diff --git a/packages/trpc/server/auth-router/create-passkey.ts b/packages/trpc/server/auth-router/create-passkey.ts
index 42b97b45b..0eaa369ea 100644
--- a/packages/trpc/server/auth-router/create-passkey.ts
+++ b/packages/trpc/server/auth-router/create-passkey.ts
@@ -1,4 +1,4 @@
-import type { RegistrationResponseJSON } from '@simplewebauthn/types';
+import type { RegistrationResponseJSON } from '@simplewebauthn/server';
import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';