From a4caa71621d55c862761ebf7e347172a4088fafe Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Feb 2026 09:35:58 -0700 Subject: [PATCH] feat: migrate auth to EncryptID SDK client Replace duplicated WebAuthn ceremony code with SDK EncryptIDClient. Add @encryptid/sdk dependency and cookie persistence. Co-Authored-By: Claude Opus 4.6 --- package.json | 1 + src/stores/auth.ts | 113 ++++----------------------------------------- 2 files changed, 9 insertions(+), 105 deletions(-) diff --git a/package.json b/package.json index f0523c1..32a33a3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@encryptid/sdk": "file:../encryptid-sdk", "jszip": "^3.10.1", "maplibre-gl": "^5.0.0", "nanoid": "^5.0.9", diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 4a6b7e9..1355ac3 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -3,12 +3,15 @@ * * Optional authentication — anonymous access remains the default. * When authenticated, the user gets a persistent DID-based identity. + * Uses Zustand with localStorage persistence, delegates WebAuthn ceremony to @encryptid/sdk. */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { EncryptIDClient } from '@encryptid/sdk/client'; const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com'; +const client = new EncryptIDClient(ENCRYPTID_SERVER); interface AuthState { /** Whether the user is authenticated via EncryptID */ @@ -30,17 +33,6 @@ interface AuthState { logout: () => void; } -function toBase64url(buffer: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buffer))) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); -} - -function fromBase64url(str: string): Uint8Array { - return Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); -} - export const useAuthStore = create()( persist( (set) => ({ @@ -53,51 +45,8 @@ export const useAuthStore = create()( login: async () => { set({ loading: true }); try { - // Step 1: Get auth options - const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }); - const { options } = await startRes.json(); - - // Step 2: WebAuthn ceremony - const publicKeyOptions: PublicKeyCredentialRequestOptions = { - challenge: fromBase64url(options.challenge).buffer as ArrayBuffer, - rpId: options.rpId, - userVerification: options.userVerification as UserVerificationRequirement, - timeout: options.timeout, - allowCredentials: options.allowCredentials?.map((c: any) => ({ - type: c.type as PublicKeyCredentialType, - id: fromBase64url(c.id).buffer as ArrayBuffer, - transports: c.transports as AuthenticatorTransport[], - })), - }; - - const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }) as PublicKeyCredential; - if (!assertion) throw new Error('Authentication cancelled'); - - const response = assertion.response as AuthenticatorAssertionResponse; - - // Step 3: Complete auth - const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/complete`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - challenge: options.challenge, - credential: { - credentialId: assertion.id, - authenticatorData: toBase64url(response.authenticatorData), - clientDataJSON: toBase64url(response.clientDataJSON), - signature: toBase64url(response.signature), - userHandle: response.userHandle ? toBase64url(response.userHandle) : null, - }, - }), - }); - - const result = await completeRes.json(); - if (!result.success) throw new Error(result.error || 'Authentication failed'); - + const result = await client.authenticate(); + document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`; set({ isAuthenticated: true, token: result.token, @@ -114,55 +63,8 @@ export const useAuthStore = create()( register: async (username: string) => { set({ loading: true }); try { - // Step 1: Get registration options - const startRes = await fetch(`${ENCRYPTID_SERVER}/api/register/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, displayName: username }), - }); - const { options, userId } = await startRes.json(); - - // Step 2: WebAuthn ceremony - const publicKeyOptions: PublicKeyCredentialCreationOptions = { - challenge: fromBase64url(options.challenge).buffer as ArrayBuffer, - rp: options.rp, - user: { - id: fromBase64url(options.user.id).buffer as ArrayBuffer, - name: options.user.name, - displayName: options.user.displayName, - }, - pubKeyCredParams: options.pubKeyCredParams, - authenticatorSelection: options.authenticatorSelection, - timeout: options.timeout, - attestation: options.attestation as AttestationConveyancePreference, - }; - - const credential = await navigator.credentials.create({ publicKey: publicKeyOptions }) as PublicKeyCredential; - if (!credential) throw new Error('Registration cancelled'); - - const response = credential.response as AuthenticatorAttestationResponse; - - // Step 3: Complete registration - const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/register/complete`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - challenge: options.challenge, - userId, - username, - credential: { - credentialId: credential.id, - publicKey: toBase64url(response.getPublicKey?.() || response.attestationObject), - attestationObject: toBase64url(response.attestationObject), - clientDataJSON: toBase64url(response.clientDataJSON), - transports: (response as any).getTransports?.() || [], - }, - }), - }); - - const result = await completeRes.json(); - if (!result.success) throw new Error(result.error || 'Registration failed'); - + const result = await client.register(username); + document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`; set({ isAuthenticated: true, token: result.token, @@ -177,6 +79,7 @@ export const useAuthStore = create()( }, logout: () => { + document.cookie = 'encryptid_token=;path=/;max-age=0;SameSite=Lax'; set({ isAuthenticated: false, token: null,