work-in-progress atproto PDS
typescript atproto pds atcute
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: clean stuff up

Mary d31d3ef5 d68615ec

+1160 -638
+1
.gitignore
··· 2 2 node_modules/ 3 3 dist/ 4 4 data/ 5 + packages/danaus/public/ 5 6 6 7 # Deciduous database (local) 7 8 .deciduous/
+2 -1
Procfile
··· 1 1 pds: cd packages/danaus && bun run app:watch 2 - pds-tailwind: cd packages/danaus && bun run css:watch 2 + pds-css: cd packages/danaus && bun run css:watch 3 + pds-js: cd packages/danaus && bun run js:watch 3 4 dev-env: cd packages/dev-env && LOG_ENABLED=true ./with-infra.sh node --import=@poppinss/ts-exec ./src/bin.ts
+9 -3
packages/danaus/package.json
··· 11 11 ".": "./src/index.ts" 12 12 }, 13 13 "scripts": { 14 + "build": "bun run css:build && bun run js:build", 14 15 "test": "bun test", 15 16 "tsc": "tsc -b", 16 17 "app:watch": "bun run --watch src/bin/pds.ts", 17 - "css:build": "tailwindcss -i src/web/styles/main.css -o src/web/styles/main.out.css", 18 - "css:watch": "tailwindcss -i src/web/styles/main.css -o src/web/styles/main.out.css -w", 18 + "css:build": "tailwindcss -i src/web/styles/main.css -o public/style.css", 19 + "css:watch": "tailwindcss -i src/web/styles/main.css -o public/style.css -w", 20 + "js:build": "bun run src/bin/build-web-assets.ts", 21 + "js:watch": "bun run src/bin/build-web-assets.ts --watch", 19 22 "db:generate:account": "drizzle-kit generate --dialect=sqlite --schema=src/accounts/db/schema.ts --out=drizzle/accounts", 20 23 "db:generate:actor": "drizzle-kit generate --dialect=sqlite --schema=src/actors/db/schema.ts --out=drizzle/actors", 21 24 "db:generate:identity": "drizzle-kit generate --dialect=sqlite --schema=src/identity/db/schema.ts --out=drizzle/identity", ··· 44 47 "@atcute/xrpc-server-bun": "^0.1.1", 45 48 "@kelinci/danaus-lexicons": "workspace:*", 46 49 "@oomfware/fetch-router": "^0.2.1", 47 - "@oomfware/forms": "^0.2.2", 50 + "@oomfware/forms": "^0.3.0", 48 51 "@oomfware/jsx": "^0.1.5", 49 52 "@simplewebauthn/server": "^13.2.2", 50 53 "cva": "1.0.0-beta.4", ··· 58 61 }, 59 62 "devDependencies": { 60 63 "@danaus/dev-env": "workspace:*", 64 + "@rollup/plugin-multi-entry": "^7.1.0", 61 65 "@standard-schema/spec": "^1.1.0", 62 66 "@tailwindcss/cli": "^4.1.18", 63 67 "@types/bun": "^1.3.5", 64 68 "@types/qrcode": "^1.5.6", 69 + "chokidar": "^5.0.0", 65 70 "concurrently": "^9.2.1", 66 71 "drizzle-kit": "1.0.0-beta.6-4414a19", 72 + "rolldown": "1.0.0-beta.60", 67 73 "tailwindcss": "^4.1.18" 68 74 } 69 75 }
+31 -3
packages/danaus/src/accounts/manager.ts
··· 46 46 export type WebauthnRegistrationChallenge = typeof t.webauthnRegistrationChallenge.$inferSelect; 47 47 48 48 /** MFA status for an account */ 49 + /** WebAuthn credential type for MFA status */ 50 + export type WebAuthnType = false | 'security-key' | 'passkey' | 'mixed'; 51 + 49 52 export interface MfaStatus { 50 53 /** preferred MFA method */ 51 54 preferred: PreferredMfa; 52 55 /** has TOTP credentials */ 53 56 hasTotp: boolean; 54 - /** has WebAuthn security keys */ 55 - hasWebAuthn: boolean; 57 + /** WebAuthn credential type(s) registered */ 58 + webAuthnType: WebAuthnType; 56 59 /** has recovery codes */ 57 60 hasRecoveryCodes: boolean; 58 61 } ··· 1213 1216 return { 1214 1217 preferred: account.preferred_mfa, 1215 1218 hasTotp: this.#countTotpCredentials(did) > 0, 1216 - hasWebAuthn: this.countWebAuthnCredentials(did) > 0, 1219 + webAuthnType: this.#getWebAuthnType(did), 1217 1220 hasRecoveryCodes: this.getRecoveryCodeCount(did) > 0, 1218 1221 }; 1219 1222 } ··· 1675 1678 .where(eq(t.webauthnCredential.did, did)) 1676 1679 .get()?.count ?? 0 1677 1680 ); 1681 + } 1682 + 1683 + /** 1684 + * get the WebAuthn credential type(s) for an account. 1685 + * @param did account did 1686 + * @returns credential type: false if none, 'security-key', 'passkey', or 'mixed' 1687 + */ 1688 + #getWebAuthnType(did: Did): WebAuthnType { 1689 + const credentials = this.db 1690 + .select({ type: t.webauthnCredential.type }) 1691 + .from(t.webauthnCredential) 1692 + .where(eq(t.webauthnCredential.did, did)) 1693 + .all(); 1694 + 1695 + if (credentials.length === 0) { 1696 + return false; 1697 + } 1698 + 1699 + const hasSecurityKey = credentials.some((c) => c.type === WebAuthnCredentialType.SecurityKey); 1700 + const hasPasskey = credentials.some((c) => c.type === WebAuthnCredentialType.Passkey); 1701 + 1702 + if (hasSecurityKey && hasPasskey) { 1703 + return 'mixed'; 1704 + } 1705 + return hasPasskey ? 'passkey' : 'security-key'; 1678 1706 } 1679 1707 1680 1708 /**
+22 -2
packages/danaus/src/accounts/webauthn.ts
··· 78 78 expectedOrigin: string; 79 79 /** the expected relying party ID */ 80 80 expectedRpId: string; 81 + /** whether user verification is required (true for passkeys, false for security keys) */ 82 + requireUserVerification: boolean; 81 83 } 82 84 83 85 /** ··· 88 90 export const verifyWebAuthnRegistration = async ( 89 91 params: VerifyRegistrationParams, 90 92 ): Promise<VerifiedRegistrationResponse> => { 91 - const { response, expectedChallenge, expectedOrigin, expectedRpId } = params; 93 + const { response, expectedChallenge, expectedOrigin, expectedRpId, requireUserVerification } = params; 92 94 93 95 return await verifyRegistrationResponse({ 94 96 response, 95 97 expectedChallenge, 96 98 expectedOrigin, 97 99 expectedRPID: expectedRpId, 100 + requireUserVerification, 98 101 }); 99 102 }; 100 103 ··· 119 122 export const generateWebAuthnAuthenticationOptions = async (params: GenerateAuthenticationOptionsParams) => { 120 123 const { rpId, allowCredentials, userVerificationRequired = false } = params; 121 124 125 + // determine user verification requirement: 126 + // - required: passkey-only flow (passwordless login) 127 + // - preferred: mixed credentials or MFA (passkeys will do UV, security keys won't) 128 + // - discouraged: security keys only 129 + let userVerification: 'required' | 'preferred' | 'discouraged'; 130 + if (userVerificationRequired) { 131 + userVerification = 'required'; 132 + } else if (allowCredentials?.some((cred) => cred.type === WebAuthnCredentialType.Passkey)) { 133 + userVerification = 'preferred'; 134 + } else { 135 + userVerification = 'discouraged'; 136 + } 137 + 122 138 return await generateAuthenticationOptions({ 123 139 rpID: rpId, 124 - userVerification: userVerificationRequired ? 'required' : 'discouraged', 140 + userVerification, 125 141 allowCredentials: allowCredentials?.map((cred) => ({ 126 142 id: cred.credential_id, 127 143 transports: cred.transports ?? undefined, ··· 152 168 ): Promise<VerifiedAuthenticationResponse> => { 153 169 const { response, expectedChallenge, expectedOrigin, expectedRpId, credential } = params; 154 170 171 + // passkeys require user verification, security keys only need user presence 172 + const requireUserVerification = credential.type === WebAuthnCredentialType.Passkey; 173 + 155 174 return await verifyAuthenticationResponse({ 156 175 response, 157 176 expectedChallenge, 158 177 expectedOrigin, 159 178 expectedRPID: expectedRpId, 179 + requireUserVerification, 160 180 credential: { 161 181 id: credential.credential_id, 162 182 publicKey: new Uint8Array(credential.public_key),
+85
packages/danaus/src/bin/build-web-assets.ts
··· 1 + import fs from 'node:fs/promises'; 2 + 3 + import chokidar from 'chokidar'; 4 + import { rolldown } from 'rolldown'; 5 + 6 + const watchMode = process.argv.includes('--watch'); 7 + 8 + const run = async (): Promise<boolean> => { 9 + try { 10 + const inputs = await Array.fromAsync(fs.glob(`src/web/scripts/*.ts`)); 11 + 12 + const build = await rolldown({ 13 + input: inputs, 14 + }); 15 + 16 + const { output: _output } = await build.write({ 17 + dir: 'public/', 18 + minify: watchMode ? 'dce-only' : true, 19 + }); 20 + 21 + console.log(`built ${inputs.length} scripts`); 22 + 23 + return true; 24 + } catch (err) { 25 + console.error(err); 26 + 27 + return false; 28 + } 29 + }; 30 + 31 + const watch = async () => { 32 + let isBuilding = false; 33 + let rerun = false; 34 + let pendingTimer: NodeJS.Timeout | undefined; 35 + 36 + const rebuild = async () => { 37 + if (isBuilding) { 38 + rerun = true; 39 + return; 40 + } 41 + 42 + isBuilding = true; 43 + const success = await run(); 44 + isBuilding = false; 45 + 46 + if (!success) { 47 + console.error('web assets build failed'); 48 + } 49 + 50 + if (rerun) { 51 + rerun = false; 52 + void rebuild(); 53 + } 54 + }; 55 + 56 + const schedule = () => { 57 + if (pendingTimer) { 58 + clearTimeout(pendingTimer); 59 + } 60 + pendingTimer = setTimeout(() => { 61 + void rebuild(); 62 + }, 50); 63 + }; 64 + 65 + const watcher = chokidar.watch('src/web/scripts/', { ignoreInitial: true, depth: 1 }); 66 + 67 + watcher.on('add', schedule); 68 + watcher.on('change', schedule); 69 + watcher.on('unlink', schedule); 70 + watcher.on('error', (err) => { 71 + console.error('web assets watcher error:', err); 72 + }); 73 + 74 + await run(); 75 + console.log('watching web scripts for changes...'); 76 + }; 77 + 78 + if (watchMode) { 79 + await watch(); 80 + } else { 81 + const success = await run(); 82 + if (!success) { 83 + process.exitCode = 1; 84 + } 85 + }
+2
packages/danaus/src/config.ts
··· 16 16 17 17 did: Did; 18 18 publicUrl: string; 19 + publicAssetsDirectory: string; 19 20 20 21 imports: { 21 22 accepting: boolean; ··· 145 146 146 147 did: env.PDS_SERVICE_DID ?? `did:web:${hostname}`, 147 148 publicUrl: hostname === 'localhost' ? `http://localhost:${port}` : `https://${hostname}`, 149 + publicAssetsDirectory: path.resolve(env.PDS_PUBLIC_ASSETS_DIRECTORY ?? 'public'), 148 150 149 151 imports: { 150 152 accepting: env.PDS_REPO_IMPORT_ACCEPTING ?? true,
+1
packages/danaus/src/environment.ts
··· 23 23 24 24 PDS_SERVICE_DID: v.optional(did), 25 25 PDS_SERVICE_NAME: v.optional(str), 26 + PDS_PUBLIC_ASSETS_DIRECTORY: v.optional(str), 26 27 27 28 PDS_HOME_URL: v.optional(url), 28 29 PDS_LOGO_URL: v.optional(url),
-8
packages/danaus/src/pds-server.ts
··· 3 3 import { createBunWebSocket } from '@atcute/xrpc-server-bun'; 4 4 import { cors } from '@atcute/xrpc-server/middlewares/cors'; 5 5 6 - import webauthnAuthenticateScript from '#web/scripts/webauthn-authenticate.js' with { type: 'file' }; 7 - import webauthnRegisterScript from '#web/scripts/webauthn-register.js' with { type: 'file' }; 8 - import styles from '#web/styles/main.out.css' with { type: 'file' }; 9 - 10 6 import { appBsky } from './api/app.bsky/index.ts'; 11 7 import { comAtproto } from './api/com.atproto/index.ts'; 12 8 import { localDanaus } from './api/local.danaus/index.ts'; ··· 120 116 { headers: corsHeaders }, 121 117 ), 122 118 '/xrpc/*': wrapped.fetch, 123 - 124 - '/assets/style.css': new Response(Bun.file(styles)), 125 - '/assets/webauthn-register.js': new Response(Bun.file(webauthnRegisterScript)), 126 - '/assets/webauthn-authenticate.js': new Response(Bun.file(webauthnAuthenticateScript)), 127 119 128 120 '/*': (request, server) => runWithServer(server, () => web.fetch(request)), 129 121 },
+1
packages/danaus/src/test/test-pds.ts
··· 72 72 hostname: hostname, 73 73 did: cfg.service?.did ?? `did:web:${hostname}`, 74 74 publicUrl: publicUrl, 75 + publicAssetsDirectory: path.resolve('public'), 75 76 imports: { 76 77 accepting: true, 77 78 maxSize: null,
+1 -1
packages/danaus/src/web/controllers/account/security/totp/lib/forms.ts
··· 18 18 export const setupTotpForm = form( 19 19 v.object({ 20 20 name: v.optional(v.pipe(v.string(), normalizeWhitespace, v.maxLength(32, `Name is too long`))), 21 - secret: v.pipe(v.string(), v.minLength(1)), 21 + secret: v.pipe(v.string()), 22 22 _code: v.pipe(v.string(), v.length(6, `Enter the 6-digit code`)), 23 23 }), 24 24 async (data, issue) => {
+9 -9
packages/danaus/src/web/controllers/account/security/webauthn.tsx
··· 88 88 89 89 <div class="w-full max-w-120 rounded-xl bg-neutral-background-1 shadow-64"> 90 90 <danaus-webauthn-register class="contents" data-options={JSON.stringify(options)}> 91 - <form {...completeWebAuthnForm} class="contents"> 91 + <form {...completeWebAuthnForm.with({ preserveParams: true })} class="contents"> 92 92 <Dialog.Body> 93 93 <Dialog.Title>Set up {credentialLabel}</Dialog.Title> 94 94 95 95 <Dialog.Content class="flex flex-col gap-4"> 96 96 <p class="text-base-300"> 97 97 {isPasskey 98 - ? "Follow your browser's prompts to register your passkey." 99 - : "Insert your security key and follow your browser's prompts to register it."} 98 + ? 'When prompted, use your fingerprint, face, or device PIN to create your passkey.' 99 + : 'When prompted, insert your security key and touch it to confirm.'} 100 100 </p> 101 101 102 102 <input {...fields.token.as('hidden', token!)} /> ··· 104 104 {...fields.credentialType.as('hidden', isPasskey ? 'passkey' : 'security-key')} 105 105 /> 106 106 107 - <p 108 - data-target="webauthn-register.status" 109 - class="text-base-300 text-neutral-foreground-3 empty:hidden" 110 - /> 111 - 112 107 <input 113 - {...fields.response.as('hidden', '')} 108 + {...fields.response.as('hidden', '{}')} 114 109 data-target="webauthn-register.response" 115 110 /> 116 111 ··· 124 119 placeholder={accountManager.generateWebAuthnName(session.did, credentialType)} 125 120 /> 126 121 </Field> 122 + 123 + <p 124 + data-target="webauthn-register.status" 125 + class="text-base-300 text-neutral-foreground-3 empty:hidden" 126 + /> 127 127 128 128 {generalError && ( 129 129 <p role="alert" class="text-base-300 text-status-danger-foreground-1">
+3 -3
packages/danaus/src/web/controllers/account/security/webauthn/lib/forms.ts
··· 72 72 v.array(v.picklist(['ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', 'usb'])), 73 73 ), 74 74 }), 75 - clientExtensionResults: v.record(v.string(), v.unknown()), 76 75 authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 76 + clientExtensionResults: v.record(v.string(), v.unknown()), 77 77 }), 78 78 ), 79 79 }), ··· 99 99 expectedChallenge: challenge.challenge, 100 100 expectedOrigin: config.service.publicUrl, 101 101 expectedRpId: config.service.hostname, 102 + requireUserVerification: data.credentialType === 'passkey', 102 103 }); 103 - } catch (err) { 104 - console.error('WebAuthn verification error:', err); 104 + } catch { 105 105 invalid(`Registration failed, please try again`); 106 106 } 107 107
+52
packages/danaus/src/web/controllers/assets.ts
··· 1 + import path from 'node:path'; 2 + 3 + import type { BuildAction } from '@oomfware/fetch-router'; 4 + 5 + import { getAppContext } from '#web/middlewares/app-context.ts'; 6 + import { routes } from '#web/routes.ts'; 7 + 8 + const resolveAssetPath = (assetsDirectory: string, assetPath: string): string | null => { 9 + let decodedPath = assetPath.replace(/^\/+/, ''); 10 + if (!decodedPath) { 11 + return null; 12 + } 13 + 14 + try { 15 + decodedPath = decodeURIComponent(decodedPath); 16 + } catch { 17 + return null; 18 + } 19 + 20 + const resolvedAssetsDir = path.resolve(assetsDirectory); 21 + const resolvedAssetPath = path.resolve(resolvedAssetsDir, decodedPath); 22 + if ( 23 + resolvedAssetPath === resolvedAssetsDir || 24 + !resolvedAssetPath.startsWith(`${resolvedAssetsDir}${path.sep}`) 25 + ) { 26 + return null; 27 + } 28 + 29 + return resolvedAssetPath; 30 + }; 31 + 32 + export default { 33 + middleware: [], 34 + async action({ params, request }) { 35 + if (request.method !== 'GET' && request.method !== 'HEAD') { 36 + return new Response(null, { status: 405 }); 37 + } 38 + 39 + const { config } = getAppContext(); 40 + const assetPath = resolveAssetPath(config.service.publicAssetsDirectory, params.path ?? ''); 41 + if (!assetPath) { 42 + return new Response(null, { status: 404 }); 43 + } 44 + 45 + const response = new Response(Bun.file(assetPath)); 46 + if (config.service.devMode) { 47 + response.headers.set('cache-control', 'no-cache'); 48 + } 49 + 50 + return response; 51 + }, 52 + } satisfies BuildAction<'ANY', typeof routes.assets>;
+37 -13
packages/danaus/src/web/controllers/login.tsx
··· 1 - import type { Controller } from '@oomfware/fetch-router'; 1 + import type { BunRequest } from 'bun'; 2 + 3 + import { redirect, type Controller } from '@oomfware/fetch-router'; 4 + import { getContext } from '@oomfware/fetch-router/middlewares/async-context'; 2 5 import { forms } from '@oomfware/forms'; 3 6 import { render } from '@oomfware/jsx'; 4 7 5 8 import { generateWebAuthnAuthenticationOptions } from '#app/accounts/webauthn.ts'; 9 + import { readWebSessionToken, verifyWebSessionToken, WEB_SESSION_COOKIE } from '#app/auth/web.ts'; 6 10 7 11 import { BaseLayout } from '#web/layouts/base.tsx'; 8 12 import { getAppContext } from '#web/middlewares/app-context.ts'; ··· 20 24 const { fields } = loginForm; 21 25 const passkeyFields = passkeyLoginForm.fields; 22 26 23 - const redirectUrl = url.searchParams.get('redirect') ?? fields.redirect.value(); 27 + const redirectUrl = url.searchParams.get('redirect'); 24 28 25 29 return render( 26 30 <BaseLayout> ··· 29 33 <script type="module" src={routes.assets.href({ path: 'webauthn-passkey-login.js' })} /> 30 34 31 35 <div class="flex flex-1 items-center justify-center p-4"> 32 - <div class="w-full max-w-96 rounded-xl bg-neutral-background-1 p-6 shadow-16"> 33 - <form {...loginForm} class="flex flex-col gap-6"> 36 + <div class="flex w-full max-w-96 flex-col gap-6 rounded-xl bg-neutral-background-1 p-6 shadow-16"> 37 + <form {...loginForm.with({ preserveParams: true })} class="flex flex-col gap-6"> 34 38 <h1 class="text-base-500 font-semibold">Sign in to your account</h1> 35 39 36 40 <input {...fields.redirect.as('hidden', redirectUrl ?? routes.account.overview.href())} /> ··· 57 61 <Input {...fields._password.as('password')} autocomplete="current-password" required /> 58 62 </Field> 59 63 60 - <Checkbox name="remember" value="true"> 64 + <Checkbox name="remember" value="true" class="-m-2"> 61 65 Remember this device 62 66 </Checkbox> 63 67 ··· 70 74 class="contents" 71 75 data-challenge-url={routes.login.passkey.challenge.href()} 72 76 > 73 - <div class="flex items-center gap-4 py-4"> 74 - <div class="h-px flex-1 bg-neutral-stroke-2" /> 77 + <div class="flex items-center gap-4"> 78 + <div class="h-px grow bg-neutral-stroke-2" /> 75 79 <span class="text-base-200 text-neutral-foreground-3">or</span> 76 - <div class="h-px flex-1 bg-neutral-stroke-2" /> 80 + <div class="h-px grow bg-neutral-stroke-2" /> 77 81 </div> 78 82 79 - <form {...passkeyLoginForm} class="contents" data-target="passkey-login.form"> 83 + <form {...passkeyLoginForm} class="flex flex-col" data-target="passkey-login.form"> 80 84 <input 81 85 {...passkeyFields.redirect.as('hidden', redirectUrl ?? routes.account.overview.href())} 82 86 /> 83 87 <input 84 - {...passkeyFields.response.as('hidden', '')} 88 + {...passkeyFields.response.as('hidden', '{}')} 85 89 data-target="passkey-login.response" 86 90 /> 87 91 88 - <Button disabled data-target="passkey-login.start"> 89 - Sign in with passkey 90 - </Button> 92 + <Field validationMessageText={passkeyFields.allIssues()?.at(0)?.message}> 93 + <Button disabled data-target="passkey-login.start"> 94 + Sign in with passkey 95 + </Button> 96 + </Field> 91 97 92 98 <p 93 99 data-target="passkey-login.status" ··· 100 106 </BaseLayout>, 101 107 ); 102 108 }, 109 + }, 110 + logout() { 111 + const { accountManager, config } = getAppContext(); 112 + const { request } = getContext(); 113 + 114 + // read and verify the session token 115 + const token = readWebSessionToken(request); 116 + if (token) { 117 + const sessionId = verifyWebSessionToken(config.secrets.jwtKey, token); 118 + if (sessionId) { 119 + accountManager.deleteWebSession(sessionId); 120 + } 121 + } 122 + 123 + // clear the session cookie 124 + (request as BunRequest).cookies.delete(WEB_SESSION_COOKIE, { path: '/' }); 125 + 126 + redirect(routes.login.index.href()); 103 127 }, 104 128 passkey: { 105 129 async challenge() {
+33 -56
packages/danaus/src/web/controllers/login/lib/forms.ts
··· 9 9 import type { Account } from '#app/accounts/manager.ts'; 10 10 import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '#app/accounts/passwords.ts'; 11 11 import { isRecoveryCode, isTotpCode } from '#app/accounts/totp.ts'; 12 + import { verifyWebAuthnAuthentication } from '#app/accounts/webauthn.ts'; 12 13 import { setWebSessionToken } from '#app/auth/web.ts'; 13 14 14 15 import { getAppContext } from '#web/middlewares/app-context.ts'; ··· 158 159 const VERIFY_ALLOWED_SUDO_MFA_FACTORS: AuthFactor[] = ['totp', 'webauthn', 'recovery']; 159 160 const VERIFY_ALLOWED_SUDO_OFA_FACTORS: AuthFactor[] = ['password']; 160 161 162 + /** WebAuthn authentication response schema */ 163 + const webauthnResponseSchema = v.pipe( 164 + v.string(), 165 + v.parseJson(), 166 + v.object({ 167 + id: v.string(), 168 + rawId: v.string(), 169 + type: v.literal('public-key'), 170 + response: v.object({ 171 + clientDataJSON: v.string(), 172 + authenticatorData: v.string(), 173 + signature: v.string(), 174 + userHandle: v.pipe( 175 + v.nullish(v.string()), 176 + v.transform((val) => val ?? undefined), 177 + ), 178 + }), 179 + authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 180 + clientExtensionResults: v.object({ 181 + appid: v.optional(v.boolean()), 182 + credProps: v.optional( 183 + v.object({ 184 + rk: v.optional(v.boolean()), 185 + }), 186 + ), 187 + hmacCreateSecret: v.optional(v.boolean()), 188 + }), 189 + }), 190 + ); 191 + 161 192 export const verifyForm = form( 162 193 v.object({ 163 194 challenge: v.string(), ··· 222 253 223 254 export const passkeyLoginForm = form( 224 255 v.object({ 225 - response: v.pipe( 226 - v.string(), 227 - v.parseJson(), 228 - v.object({ 229 - id: v.string(), 230 - rawId: v.string(), 231 - response: v.object({ 232 - clientDataJSON: v.string(), 233 - authenticatorData: v.string(), 234 - signature: v.string(), 235 - userHandle: v.optional(v.string()), 236 - }), 237 - authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 238 - clientExtensionResults: v.object({ 239 - appid: v.optional(v.boolean()), 240 - credProps: v.optional( 241 - v.object({ 242 - rk: v.optional(v.boolean()), 243 - }), 244 - ), 245 - hmacCreateSecret: v.optional(v.boolean()), 246 - }), 247 - type: v.literal('public-key'), 248 - }), 249 - ), 256 + response: webauthnResponseSchema, 250 257 redirect: v.string(), 251 258 }), 252 259 async (data) => { ··· 268 275 if (!accountManager.consumePasskeyLoginChallenge(challenge)) { 269 276 invalid(`Invalid or expired challenge`); 270 277 } 271 - 272 - // verify the authentication response 273 - const { verifyWebAuthnAuthentication } = await import('#app/accounts/webauthn.ts'); 274 278 275 279 try { 276 280 const verification = await verifyWebAuthnAuthentication({ ··· 316 320 export const verifyWebAuthnForm = form( 317 321 v.object({ 318 322 challenge: v.string(), 319 - response: v.pipe( 320 - v.string(), 321 - v.parseJson(), 322 - v.object({ 323 - id: v.string(), 324 - rawId: v.string(), 325 - response: v.object({ 326 - clientDataJSON: v.string(), 327 - authenticatorData: v.string(), 328 - signature: v.string(), 329 - userHandle: v.optional(v.string()), 330 - }), 331 - authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 332 - clientExtensionResults: v.object({ 333 - appid: v.optional(v.boolean()), 334 - credProps: v.optional( 335 - v.object({ 336 - rk: v.optional(v.boolean()), 337 - }), 338 - ), 339 - hmacCreateSecret: v.optional(v.boolean()), 340 - }), 341 - type: v.literal('public-key'), 342 - }), 343 - ), 323 + response: webauthnResponseSchema, 344 324 redirect: v.string(), 345 325 }), 346 326 async (data) => { ··· 361 341 if (credential === null || credential.did !== challenge.did) { 362 342 invalid(`Invalid security key`); 363 343 } 364 - 365 - // verify the authentication response 366 - const { verifyWebAuthnAuthentication } = await import('#app/accounts/webauthn.ts'); 367 344 368 345 try { 369 346 const verification = await verifyWebAuthnAuthentication({
+62 -49
packages/danaus/src/web/controllers/verify.tsx
··· 196 196 <BaseLayout> 197 197 <title>{ctx.isSudo ? 'Confirm your identity' : 'Two-factor authentication'} - Danaus</title> 198 198 199 - <script src="/assets/webauthn-authenticate.js" type="module" /> 199 + <script src={routes.assets.href({ path: 'webauthn-authenticate.js' })} type="module" /> 200 200 201 201 <div class="flex flex-1 flex-col items-center justify-center gap-4 p-4"> 202 202 <noscript> ··· 208 208 <div class="w-full max-w-96 rounded-xl bg-neutral-background-1 p-6 shadow-16"> 209 209 <danaus-webauthn-authenticate class="contents" data-options={JSON.stringify(options)}> 210 210 <form 211 - {...verifyWebAuthnForm} 211 + {...verifyWebAuthnForm.with({ preserveParams: true })} 212 212 class="flex flex-col gap-6" 213 213 data-target="webauthn-authenticate.form" 214 214 > ··· 220 220 {ctx.isSudo ? 'Confirm your identity' : 'Two-factor authentication'} 221 221 </h1> 222 222 <p class="text-base-300 text-neutral-foreground-3"> 223 - Insert your security key and touch it 224 - {ctx.isSudo ? ' to continue.' : ' to verify your identity.'} 223 + Authenticate using your{' '} 224 + {ctx.mfaStatus!.webAuthnType === 'security-key' ? `security key` : `passkey`}. 225 225 </p> 226 226 </div> 227 227 228 - <input {...fields.response.as('hidden', '')} data-target="webauthn-authenticate.response" /> 228 + <input 229 + {...fields.response.as('hidden', '{}')} 230 + data-target="webauthn-authenticate.response" 231 + /> 229 232 230 - <Button data-target="webauthn-authenticate.start" variant="primary" disabled> 231 - Use security key 232 - </Button> 233 + <Field validationMessageText={fields.allIssues()?.at(0)?.message}> 234 + <Button data-target="webauthn-authenticate.start" variant="primary" disabled> 235 + Use {ctx.mfaStatus!.webAuthnType === 'security-key' ? `security key` : `passkey`} 236 + </Button> 237 + </Field> 233 238 234 239 <div 235 240 data-target="webauthn-authenticate.status" ··· 355 360 356 361 <div class="flex flex-1 items-center justify-center p-4"> 357 362 <div class="w-full max-w-96 rounded-xl bg-neutral-background-1 p-6 shadow-16"> 358 - <form {...verifyForm} class="flex flex-col gap-6"> 363 + <form {...verifyForm.with({ preserveParams: true })} class="flex flex-col gap-6"> 359 364 <input {...fields.challenge.as('hidden', props.challenge)} /> 360 365 <input {...fields.redirect.as('hidden', props.redirectUrl)} /> 361 366 <input {...fields.factor.as('hidden', props.factor)} /> ··· 391 396 392 397 // count how many other methods are available 393 398 const otherMethodsCount = 394 - (props.factor !== 'webauthn' && mfaStatus.hasWebAuthn ? 1 : 0) + 399 + (props.factor !== 'webauthn' && mfaStatus.webAuthnType ? 1 : 0) + 395 400 (props.factor !== 'totp' && mfaStatus.hasTotp ? 1 : 0) + 396 401 (props.factor !== 'recovery' && mfaStatus.hasRecoveryCodes ? 1 : 0); 397 402 ··· 403 408 const tokenParam = isSudo ? undefined : challenge; 404 409 405 410 return ( 406 - <Menu.Root> 407 - <Menu.Trigger> 408 - <Button>Show other methods</Button> 409 - </Menu.Trigger> 411 + <> 412 + <div class="flex items-center gap-4"> 413 + <div class="h-px grow bg-neutral-stroke-2" /> 414 + <span class="text-base-200 text-neutral-foreground-3">or</span> 415 + <div class="h-px grow bg-neutral-stroke-2" /> 416 + </div> 417 + 418 + <Menu.Root> 419 + <Menu.Trigger> 420 + <Button>Show other methods</Button> 421 + </Menu.Trigger> 410 422 411 - <Menu.Popover> 412 - <Menu.List> 413 - {props.factor !== 'webauthn' && mfaStatus.hasWebAuthn && ( 414 - <Menu.Item 415 - href={routes.verify.webauthn.href(undefined, { 416 - token: tokenParam, 417 - redirect: redirectUrl, 418 - })} 419 - > 420 - Use security key 421 - </Menu.Item> 422 - )} 423 + <Menu.Popover> 424 + <Menu.List> 425 + {props.factor !== 'webauthn' && mfaStatus.webAuthnType && ( 426 + <Menu.Item 427 + href={routes.verify.webauthn.href(undefined, { 428 + token: tokenParam, 429 + redirect: redirectUrl, 430 + })} 431 + > 432 + {mfaStatus.webAuthnType === 'security-key' ? 'Use security key' : 'Use passkey'} 433 + </Menu.Item> 434 + )} 423 435 424 - {props.factor !== 'totp' && mfaStatus.hasTotp && ( 425 - <Menu.Item 426 - href={routes.verify.totp.href(undefined, { 427 - token: tokenParam, 428 - redirect: redirectUrl, 429 - })} 430 - > 431 - Use authenticator app 432 - </Menu.Item> 433 - )} 436 + {props.factor !== 'totp' && mfaStatus.hasTotp && ( 437 + <Menu.Item 438 + href={routes.verify.totp.href(undefined, { 439 + token: tokenParam, 440 + redirect: redirectUrl, 441 + })} 442 + > 443 + Use authenticator app 444 + </Menu.Item> 445 + )} 434 446 435 - {props.factor !== 'recovery' && mfaStatus.hasRecoveryCodes && ( 436 - <Menu.Item 437 - href={routes.verify.recovery.href(undefined, { 438 - token: tokenParam, 439 - redirect: redirectUrl, 440 - })} 441 - > 442 - Use 2FA recovery code 443 - </Menu.Item> 444 - )} 445 - </Menu.List> 446 - </Menu.Popover> 447 - </Menu.Root> 447 + {props.factor !== 'recovery' && mfaStatus.hasRecoveryCodes && ( 448 + <Menu.Item 449 + href={routes.verify.recovery.href(undefined, { 450 + token: tokenParam, 451 + redirect: redirectUrl, 452 + })} 453 + > 454 + Use 2FA recovery code 455 + </Menu.Item> 456 + )} 457 + </Menu.List> 458 + </Menu.Popover> 459 + </Menu.Root> 460 + </> 448 461 ); 449 462 };
+37 -7
packages/danaus/src/web/layouts/account.tsx
··· 1 1 import type { JSXNode } from '@oomfware/jsx'; 2 2 3 - import AsideItem from '../components/aside-item.tsx'; 4 - import Key2Outlined from '../icons/central/key-2-outlined.tsx'; 5 - import PersonOutlined from '../icons/central/person-outlined.tsx'; 6 - import ShieldOutlined from '../icons/central/shield-outlined.tsx'; 7 - import { routes } from '../routes.ts'; 3 + import AsideItem from '#web/components/aside-item.tsx'; 4 + import Key2Outlined from '#web/icons/central/key-2-outlined.tsx'; 5 + import PersonOutlined from '#web/icons/central/person-outlined.tsx'; 6 + import ShieldOutlined from '#web/icons/central/shield-outlined.tsx'; 7 + import { getAppContext } from '#web/middlewares/app-context.ts'; 8 + import { getSession } from '#web/middlewares/session.ts'; 9 + import { Menu } from '#web/primitives/index.ts'; 10 + import { routes } from '#web/routes.ts'; 8 11 9 12 import { BaseLayout } from './base.tsx'; 10 13 ··· 16 19 * account management layout with sidebar navigation. 17 20 */ 18 21 export const AccountLayout = (props: AccountLayoutProps) => { 22 + const { accountManager } = getAppContext(); 23 + const session = getSession(); 24 + 25 + const account = accountManager.getAccount(session.did)!; 26 + 19 27 return ( 20 28 <BaseLayout> 21 - <div class="flex flex-col gap-4 p-4 sm:p-16 sm:pt-24 lg:grid lg:grid-cols-[280px_minmax(0,640px)] lg:justify-center"> 22 - <aside class="-ml-2 flex flex-col gap-4 sm:ml-0"> 29 + <div class="flex min-h-0 grow flex-col gap-4 p-4 sm:p-16 sm:py-24 lg:grid lg:grid-cols-[280px_minmax(0,640px)] lg:justify-center"> 30 + <aside class="-ml-2 flex flex-col gap-4 sm:ml-0 lg:sticky lg:top-24 lg:h-[calc(100dvh-(--spacing(48)))]"> 23 31 <div class="flex h-8 shrink-0 items-center pl-4"> 24 32 <h2 class="text-base-400 font-medium">Account</h2> 25 33 </div> ··· 36 44 <AsideItem href={routes.account.security.overview.href()} icon={<ShieldOutlined size={20} />}> 37 45 Security 38 46 </AsideItem> 47 + </div> 48 + 49 + <div class="mt-auto flex flex-col gap-px"> 50 + <Menu.Root> 51 + <Menu.Trigger> 52 + <button class="ml-2 flex items-center gap-2 rounded-md p-2 outline-2 -outline-offset-2 outline-transparent transition duration-100 ease-fluent select-none hover:bg-subtle-background-hover focus-visible:z-10 focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active"> 53 + <div class="size-8 shrink-0 rounded-full bg-brand-background"></div> 54 + 55 + <div class="flex flex-col"> 56 + <span class="text-base-300">{account.handle ? `@${account.handle}` : account.did}</span> 57 + </div> 58 + </button> 59 + </Menu.Trigger> 60 + 61 + <Menu.Popover> 62 + <Menu.List> 63 + <form action={routes.login.logout.href()} method="post" class="contents"> 64 + <Menu.Item type="submit">Sign out</Menu.Item> 65 + </form> 66 + </Menu.List> 67 + </Menu.Popover> 68 + </Menu.Root> 39 69 </div> 40 70 </aside> 41 71
+4 -1
packages/danaus/src/web/primitives/button.tsx
··· 59 59 command?: InvokerCommand; 60 60 class?: string; 61 61 children?: JSXNode; 62 + 'data-target'?: string; 62 63 } 63 64 64 65 const Button = (props: ButtonProps) => { ··· 70 71 label, 71 72 commandfor, 72 73 command, 74 + 'data-target': targetId, 73 75 class: className, 74 76 children, 75 77 } = props; 76 78 77 79 if (href !== undefined) { 78 80 return ( 79 - <a href={href} class={root({ variant, className })}> 81 + <a href={href} data-target={targetId} class={root({ variant, className })}> 80 82 {children} 81 83 </a> 82 84 ); ··· 89 91 aria-label={label} 90 92 commandfor={commandfor} 91 93 command={command} 94 + data-target={targetId} 92 95 class={root({ variant, className })} 93 96 > 94 97 {children}
+4
packages/danaus/src/web/router.ts
··· 5 5 6 6 import accountController from './controllers/account.tsx'; 7 7 import adminController from './controllers/admin.tsx'; 8 + import assetsController from './controllers/assets.ts'; 8 9 import homeController from './controllers/home.tsx'; 9 10 import loginController from './controllers/login.tsx'; 10 11 import oauthController from './controllers/oauth.tsx'; 12 + import verifyController from './controllers/verify.tsx'; 11 13 import { provideAppContext } from './middlewares/app-context.ts'; 12 14 import { routes } from './routes.ts'; 13 15 ··· 22 24 }); 23 25 24 26 router.map(routes.home, homeController); 27 + router.map(routes.assets, assetsController); 25 28 router.map(routes.admin, adminController); 26 29 router.map(routes.login, loginController); 30 + router.map(routes.verify, verifyController); 27 31 router.map(routes.account, accountController); 28 32 router.map(routes.oauth, oauthController); 29 33
+1
packages/danaus/src/web/routes.ts
··· 16 16 // login routes 17 17 login: { 18 18 index: '/account/login', 19 + logout: { method: 'POST', pattern: '/account/logout' }, 19 20 passkey: { 20 21 challenge: '/account/login/passkey', 21 22 },
-23
packages/danaus/src/web/scripts/base64url.js
··· 1 - // @ts-nocheck 2 - 3 - /** 4 - * decode a base64url string to a Uint8Array. 5 - * @param {string} str 6 - * @returns {Uint8Array<ArrayBuffer>} 7 - */ 8 - export const fromBase64Url = (str) => { 9 - return Uint8Array.fromBase64(str, { alphabet: 'base64url' }); 10 - }; 11 - 12 - /** 13 - * encode an ArrayBuffer to a base64url string. 14 - * @param {ArrayBuffer | Uint8Array} buffer 15 - * @returns {string} 16 - */ 17 - export const toBase64Url = (buffer) => { 18 - if (buffer instanceof ArrayBuffer) { 19 - buffer = new Uint8Array(buffer); 20 - } 21 - 22 - return buffer.toBase64({ alphabet: 'base64url' }); 23 - };
-142
packages/danaus/src/web/scripts/webauthn-authenticate.js
··· 1 - // @ts-check 2 - 3 - import { fromBase64Url, toBase64Url } from './base64url.js'; 4 - 5 - /** 6 - * WebAuthn authentication element. 7 - * 8 - * @attr {string} data-options - JSON PublicKeyCredentialRequestOptions 9 - */ 10 - class WebAuthnAuthenticateElement extends HTMLElement { 11 - /** @type {PublicKeyCredentialRequestOptionsJSON | null} */ 12 - #options = null; 13 - 14 - /** @type {HTMLButtonElement | null} */ 15 - get startButton() { 16 - return this.querySelector('[data-target="webauthn-authenticate.start"]'); 17 - } 18 - 19 - /** @type {HTMLInputElement | null} */ 20 - get responseInput() { 21 - return this.querySelector('[data-target="webauthn-authenticate.response"]'); 22 - } 23 - 24 - /** @type {HTMLElement | null} */ 25 - get statusElement() { 26 - return this.querySelector('[data-target="webauthn-authenticate.status"]'); 27 - } 28 - 29 - /** @type {HTMLFormElement | null} */ 30 - get formElement() { 31 - return this.querySelector('[data-target="webauthn-authenticate.form"]'); 32 - } 33 - 34 - connectedCallback() { 35 - const optionsJson = this.dataset.options; 36 - if (!optionsJson) { 37 - return; 38 - } 39 - 40 - this.#options = JSON.parse(optionsJson); 41 - 42 - const startButton = this.startButton; 43 - if (startButton) { 44 - // enable the button now that JS is loaded 45 - startButton.disabled = false; 46 - startButton.addEventListener('click', (e) => { 47 - e.preventDefault(); 48 - this.#handleAuthentication(); 49 - }); 50 - } 51 - } 52 - 53 - async #handleAuthentication() { 54 - const options = this.#options; 55 - const status = this.statusElement; 56 - const responseInput = this.responseInput; 57 - const startButton = this.startButton; 58 - 59 - if (!options || !status || !responseInput) { 60 - console.error('WebAuthn authenticate: missing required elements'); 61 - return; 62 - } 63 - 64 - try { 65 - if (startButton) { 66 - startButton.disabled = true; 67 - } 68 - status.textContent = 'Waiting for security key...'; 69 - 70 - // convert options to the format expected by navigator.credentials.get 71 - /** @type {PublicKeyCredentialRequestOptions} */ 72 - const publicKeyOptions = { 73 - ...options, 74 - challenge: fromBase64Url(options.challenge), 75 - allowCredentials: options.allowCredentials?.map((cred) => ({ 76 - ...cred, 77 - id: fromBase64Url(cred.id), 78 - })), 79 - }; 80 - 81 - const credential = /** @type {PublicKeyCredential | null} */ ( 82 - await navigator.credentials.get({ publicKey: publicKeyOptions }) 83 - ); 84 - 85 - if (!credential) { 86 - status.textContent = 'Authentication cancelled'; 87 - if (startButton) { 88 - startButton.disabled = false; 89 - } 90 - return; 91 - } 92 - 93 - const response = /** @type {AuthenticatorAssertionResponse} */ (credential.response); 94 - 95 - // serialize the response for the server 96 - const serialized = JSON.stringify({ 97 - id: credential.id, 98 - rawId: toBase64Url(credential.rawId), 99 - type: credential.type, 100 - response: { 101 - clientDataJSON: toBase64Url(response.clientDataJSON), 102 - authenticatorData: toBase64Url(response.authenticatorData), 103 - signature: toBase64Url(response.signature), 104 - userHandle: response.userHandle ? toBase64Url(response.userHandle) : null, 105 - }, 106 - clientExtensionResults: credential.getClientExtensionResults(), 107 - }); 108 - 109 - responseInput.value = serialized; 110 - status.textContent = 'Security key verified!'; 111 - 112 - // auto-submit the form 113 - this.formElement?.submit(); 114 - } catch (err) { 115 - if (startButton) { 116 - startButton.disabled = false; 117 - } 118 - 119 - if (err instanceof Error) { 120 - if (err.name === 'NotAllowedError') { 121 - status.textContent = 'Authentication was cancelled or timed out. Please try again.'; 122 - } else { 123 - status.textContent = `Authentication failed: ${err.message}`; 124 - } 125 - } else { 126 - status.textContent = 'Authentication failed. Please try again.'; 127 - } 128 - console.error('WebAuthn authentication error:', err); 129 - } 130 - } 131 - } 132 - 133 - customElements.define('danaus-webauthn-authenticate', WebAuthnAuthenticateElement); 134 - 135 - /** 136 - * @typedef {object} PublicKeyCredentialRequestOptionsJSON 137 - * @property {string} challenge 138 - * @property {number} [timeout] 139 - * @property {string} [rpId] 140 - * @property {Array<{id: string, type: 'public-key', transports?: AuthenticatorTransport[]}>} [allowCredentials] 141 - * @property {UserVerificationRequirement} [userVerification] 142 - */
+146
packages/danaus/src/web/scripts/webauthn-authenticate.ts
··· 1 + import { fromBase64Url, toBase64Url } from '@atcute/multibase'; 2 + 3 + /** 4 + * webauthn authentication element. 5 + * 6 + * @attr {string} data-options - json PublicKeyCredentialRequestOptions 7 + */ 8 + class WebAuthnAuthenticateElement extends HTMLElement { 9 + #options: PublicKeyCredentialRequestOptionsJson | null = null; 10 + 11 + get startButton(): HTMLButtonElement | null { 12 + return this.querySelector('[data-target="webauthn-authenticate.start"]'); 13 + } 14 + 15 + get responseInput(): HTMLInputElement | null { 16 + return this.querySelector('[data-target="webauthn-authenticate.response"]'); 17 + } 18 + 19 + get statusElement(): HTMLElement | null { 20 + return this.querySelector('[data-target="webauthn-authenticate.status"]'); 21 + } 22 + 23 + get formElement(): HTMLFormElement | null { 24 + return this.querySelector('[data-target="webauthn-authenticate.form"]'); 25 + } 26 + 27 + connectedCallback() { 28 + const optionsJson = this.dataset.options; 29 + if (!optionsJson) { 30 + return; 31 + } 32 + 33 + this.#options = JSON.parse(optionsJson) as PublicKeyCredentialRequestOptionsJson; 34 + 35 + const startButton = this.startButton; 36 + if (startButton) { 37 + // enable the button now that js is loaded 38 + startButton.disabled = false; 39 + startButton.addEventListener('click', (event) => { 40 + event.preventDefault(); 41 + this.#handleAuthentication(); 42 + }); 43 + } 44 + } 45 + 46 + async #handleAuthentication() { 47 + const options = this.#options; 48 + const status = this.statusElement; 49 + const responseInput = this.responseInput; 50 + const startButton = this.startButton; 51 + 52 + console.log('[webauthn-authenticate] starting', { 53 + hasOptions: !!options, 54 + hasStatus: !!status, 55 + hasResponseInput: !!responseInput, 56 + responseInputValue: responseInput?.value, 57 + }); 58 + 59 + if (!options || !status || !responseInput) { 60 + console.error('webauthn authenticate: missing required elements'); 61 + return; 62 + } 63 + 64 + try { 65 + if (startButton) { 66 + startButton.disabled = true; 67 + } 68 + status.textContent = ''; 69 + 70 + const publicKeyOptions: PublicKeyCredentialRequestOptions = { 71 + ...options, 72 + challenge: fromBase64Url(options.challenge), 73 + allowCredentials: options.allowCredentials?.map((credential) => ({ 74 + ...credential, 75 + id: fromBase64Url(credential.id), 76 + })), 77 + }; 78 + 79 + const credential = (await navigator.credentials.get({ 80 + publicKey: publicKeyOptions, 81 + })) as PublicKeyCredential | null; 82 + 83 + if (!credential) { 84 + status.textContent = 'Authentication cancelled'; 85 + if (startButton) { 86 + startButton.disabled = false; 87 + } 88 + return; 89 + } 90 + 91 + const response = credential.response as AuthenticatorAssertionResponse; 92 + 93 + const serialized = JSON.stringify({ 94 + id: credential.id, 95 + rawId: toBase64Url(new Uint8Array(credential.rawId)), 96 + type: credential.type, 97 + response: { 98 + clientDataJSON: toBase64Url(new Uint8Array(response.clientDataJSON)), 99 + authenticatorData: toBase64Url(new Uint8Array(response.authenticatorData)), 100 + signature: toBase64Url(new Uint8Array(response.signature)), 101 + userHandle: response.userHandle ? toBase64Url(new Uint8Array(response.userHandle)) : null, 102 + }, 103 + clientExtensionResults: credential.getClientExtensionResults(), 104 + }); 105 + 106 + responseInput.value = serialized; 107 + console.log('[webauthn-authenticate] response set', { 108 + serialized, 109 + inputValue: responseInput.value, 110 + }); 111 + status.textContent = 'Security key verified!'; 112 + 113 + this.formElement?.submit(); 114 + } catch (err) { 115 + console.error('[webauthn-authenticate] error:', err); 116 + if (startButton) { 117 + startButton.disabled = false; 118 + } 119 + 120 + if (err instanceof Error) { 121 + if (err.name === 'NotAllowedError') { 122 + status.textContent = 'Authentication cancelled'; 123 + return; 124 + } else { 125 + status.textContent = `Authentication failed: ${err.message}`; 126 + } 127 + } else { 128 + status.textContent = 'Authentication failed. Please try again.'; 129 + } 130 + } 131 + } 132 + } 133 + 134 + customElements.define('danaus-webauthn-authenticate', WebAuthnAuthenticateElement); 135 + 136 + type PublicKeyCredentialRequestOptionsJson = { 137 + challenge: string; 138 + timeout?: number; 139 + rpId?: string; 140 + allowCredentials?: Array<{ 141 + id: string; 142 + type: 'public-key'; 143 + transports?: AuthenticatorTransport[]; 144 + }>; 145 + userVerification?: UserVerificationRequirement; 146 + };
-150
packages/danaus/src/web/scripts/webauthn-passkey-login.js
··· 1 - // @ts-check 2 - 3 - import { fromBase64Url, toBase64Url } from './base64url.js'; 4 - 5 - /** 6 - * Passkey login element - fetches challenge and handles discoverable credential authentication. 7 - * 8 - * @attr {string} data-challenge-url - URL to fetch authentication challenge from 9 - */ 10 - class PasskeyLoginElement extends HTMLElement { 11 - /** @type {HTMLButtonElement | null} */ 12 - get startButton() { 13 - return this.querySelector('[data-target="passkey-login.start"]'); 14 - } 15 - 16 - /** @type {HTMLInputElement | null} */ 17 - get responseInput() { 18 - return this.querySelector('[data-target="passkey-login.response"]'); 19 - } 20 - 21 - /** @type {HTMLElement | null} */ 22 - get statusElement() { 23 - return this.querySelector('[data-target="passkey-login.status"]'); 24 - } 25 - 26 - /** @type {HTMLFormElement | null} */ 27 - get formElement() { 28 - return this.querySelector('[data-target="passkey-login.form"]'); 29 - } 30 - 31 - connectedCallback() { 32 - const challengeUrl = this.dataset.challengeUrl; 33 - if (!challengeUrl) { 34 - return; 35 - } 36 - 37 - const startButton = this.startButton; 38 - if (startButton) { 39 - // enable the button now that JS is loaded 40 - startButton.disabled = false; 41 - startButton.addEventListener('click', (e) => { 42 - e.preventDefault(); 43 - this.#handlePasskeyLogin(challengeUrl); 44 - }); 45 - } 46 - } 47 - 48 - /** 49 - * @param {string} challengeUrl 50 - */ 51 - async #handlePasskeyLogin(challengeUrl) { 52 - const status = this.statusElement; 53 - const responseInput = this.responseInput; 54 - const startButton = this.startButton; 55 - 56 - if (!status || !responseInput) { 57 - console.error('Passkey login: missing required elements'); 58 - return; 59 - } 60 - 61 - try { 62 - if (startButton) { 63 - startButton.disabled = true; 64 - } 65 - status.textContent = 'Fetching challenge...'; 66 - 67 - // fetch challenge options from server 68 - const challengeResponse = await fetch(challengeUrl); 69 - if (!challengeResponse.ok) { 70 - throw new Error('Failed to fetch challenge'); 71 - } 72 - 73 - /** @type {PublicKeyCredentialRequestOptionsJSON} */ 74 - const options = await challengeResponse.json(); 75 - 76 - status.textContent = 'Waiting for passkey...'; 77 - 78 - // convert options to the format expected by navigator.credentials.get 79 - // omit allowCredentials for discoverable flow 80 - const { allowCredentials: _, ...rest } = options; 81 - 82 - /** @type {PublicKeyCredentialRequestOptions} */ 83 - const publicKeyOptions = { 84 - ...rest, 85 - challenge: fromBase64Url(options.challenge), 86 - }; 87 - 88 - const credential = /** @type {PublicKeyCredential | null} */ ( 89 - await navigator.credentials.get({ publicKey: publicKeyOptions }) 90 - ); 91 - 92 - if (!credential) { 93 - status.textContent = 'Authentication cancelled'; 94 - if (startButton) { 95 - startButton.disabled = false; 96 - } 97 - return; 98 - } 99 - 100 - const response = /** @type {AuthenticatorAssertionResponse} */ (credential.response); 101 - 102 - // serialize the response for the server 103 - // TODO: investigate if we can avoid double JSON encoding (stringified JSON in form field) 104 - const serialized = JSON.stringify({ 105 - id: credential.id, 106 - rawId: toBase64Url(credential.rawId), 107 - type: credential.type, 108 - response: { 109 - clientDataJSON: toBase64Url(response.clientDataJSON), 110 - authenticatorData: toBase64Url(response.authenticatorData), 111 - signature: toBase64Url(response.signature), 112 - userHandle: response.userHandle ? toBase64Url(response.userHandle) : null, 113 - }, 114 - clientExtensionResults: credential.getClientExtensionResults(), 115 - }); 116 - 117 - responseInput.value = serialized; 118 - status.textContent = 'Passkey verified!'; 119 - 120 - // submit the form 121 - this.formElement?.submit(); 122 - } catch (err) { 123 - if (startButton) { 124 - startButton.disabled = false; 125 - } 126 - 127 - if (err instanceof Error) { 128 - if (err.name === 'NotAllowedError') { 129 - status.textContent = 'Authentication was cancelled or timed out. Please try again.'; 130 - } else { 131 - status.textContent = `Authentication failed: ${err.message}`; 132 - } 133 - } else { 134 - status.textContent = 'Authentication failed. Please try again.'; 135 - } 136 - console.error('Passkey login error:', err); 137 - } 138 - } 139 - } 140 - 141 - customElements.define('danaus-passkey-login', PasskeyLoginElement); 142 - 143 - /** 144 - * @typedef {object} PublicKeyCredentialRequestOptionsJSON 145 - * @property {string} challenge 146 - * @property {number} [timeout] 147 - * @property {string} [rpId] 148 - * @property {Array<{id: string, type: 'public-key', transports?: AuthenticatorTransport[]}>} [allowCredentials] 149 - * @property {UserVerificationRequirement} [userVerification] 150 - */
+134
packages/danaus/src/web/scripts/webauthn-passkey-login.ts
··· 1 + import { fromBase64Url, toBase64Url } from '@atcute/multibase'; 2 + 3 + /** 4 + * passkey login element - fetches challenge and handles discoverable credential authentication. 5 + * 6 + * @attr {string} data-challenge-url - url to fetch authentication challenge from 7 + */ 8 + class PasskeyLoginElement extends HTMLElement { 9 + get startButton(): HTMLButtonElement | null { 10 + return this.querySelector('[data-target="passkey-login.start"]'); 11 + } 12 + 13 + get responseInput(): HTMLInputElement | null { 14 + return this.querySelector('[data-target="passkey-login.response"]'); 15 + } 16 + 17 + get statusElement(): HTMLElement | null { 18 + return this.querySelector('[data-target="passkey-login.status"]'); 19 + } 20 + 21 + get formElement(): HTMLFormElement | null { 22 + return this.querySelector('[data-target="passkey-login.form"]'); 23 + } 24 + 25 + connectedCallback() { 26 + const challengeUrl = this.dataset.challengeUrl; 27 + if (!challengeUrl) { 28 + return; 29 + } 30 + 31 + const startButton = this.startButton; 32 + if (startButton) { 33 + // enable the button now that js is loaded 34 + startButton.disabled = false; 35 + startButton.addEventListener('click', (event) => { 36 + event.preventDefault(); 37 + this.#handlePasskeyLogin(challengeUrl); 38 + }); 39 + } 40 + } 41 + 42 + async #handlePasskeyLogin(challengeUrl: string) { 43 + const status = this.statusElement; 44 + const responseInput = this.responseInput; 45 + const startButton = this.startButton; 46 + 47 + if (!status || !responseInput) { 48 + console.error('passkey login: missing required elements'); 49 + return; 50 + } 51 + 52 + try { 53 + if (startButton) { 54 + startButton.disabled = true; 55 + } 56 + 57 + const challengeResponse = await fetch(challengeUrl); 58 + if (!challengeResponse.ok) { 59 + throw new Error('Failed to fetch challenge'); 60 + } 61 + 62 + const options = (await challengeResponse.json()) as PublicKeyCredentialRequestOptionsJson; 63 + 64 + status.textContent = ''; 65 + 66 + const publicKeyOptions: PublicKeyCredentialRequestOptions = { 67 + ...options, 68 + challenge: fromBase64Url(options.challenge), 69 + allowCredentials: undefined, 70 + }; 71 + 72 + const credential = (await navigator.credentials.get({ 73 + publicKey: publicKeyOptions, 74 + })) as PublicKeyCredential | null; 75 + 76 + if (!credential) { 77 + status.textContent = 'Authentication cancelled'; 78 + if (startButton) { 79 + startButton.disabled = false; 80 + } 81 + return; 82 + } 83 + 84 + const response = credential.response as AuthenticatorAssertionResponse; 85 + 86 + const serialized = JSON.stringify({ 87 + id: credential.id, 88 + rawId: toBase64Url(new Uint8Array(credential.rawId)), 89 + type: credential.type, 90 + response: { 91 + clientDataJSON: toBase64Url(new Uint8Array(response.clientDataJSON)), 92 + authenticatorData: toBase64Url(new Uint8Array(response.authenticatorData)), 93 + signature: toBase64Url(new Uint8Array(response.signature)), 94 + userHandle: response.userHandle ? toBase64Url(new Uint8Array(response.userHandle)) : null, 95 + }, 96 + clientExtensionResults: credential.getClientExtensionResults(), 97 + }); 98 + 99 + responseInput.value = serialized; 100 + 101 + this.formElement?.submit(); 102 + } catch (err) { 103 + if (startButton) { 104 + startButton.disabled = false; 105 + } 106 + 107 + if (err instanceof Error) { 108 + if (err.message.includes('timed out')) { 109 + return; 110 + } else { 111 + status.textContent = `Authentication failed: ${err.message}`; 112 + } 113 + } else { 114 + status.textContent = 'Authentication failed. Please try again.'; 115 + } 116 + 117 + console.error('passkey login error:', err); 118 + } 119 + } 120 + } 121 + 122 + customElements.define('danaus-passkey-login', PasskeyLoginElement); 123 + 124 + type PublicKeyCredentialRequestOptionsJson = { 125 + challenge: string; 126 + timeout?: number; 127 + rpId?: string; 128 + allowCredentials?: Array<{ 129 + id: string; 130 + type: 'public-key'; 131 + transports?: AuthenticatorTransport[]; 132 + }>; 133 + userVerification?: UserVerificationRequirement; 134 + };
-154
packages/danaus/src/web/scripts/webauthn-register.js
··· 1 - // @ts-check 2 - 3 - import { fromBase64Url, toBase64Url } from './base64url.js'; 4 - 5 - /** 6 - * WebAuthn registration element. 7 - * 8 - * @attr {string} data-options - JSON PublicKeyCredentialCreationOptions 9 - */ 10 - class WebAuthnRegisterElement extends HTMLElement { 11 - /** @type {PublicKeyCredentialCreationOptionsJSON | null} */ 12 - #options = null; 13 - 14 - /** @type {HTMLButtonElement | null} */ 15 - get startButton() { 16 - return this.querySelector('[data-target="webauthn-register.start"]'); 17 - } 18 - 19 - /** @type {HTMLInputElement | null} */ 20 - get responseInput() { 21 - return this.querySelector('[data-target="webauthn-register.response"]'); 22 - } 23 - 24 - /** @type {HTMLElement | null} */ 25 - get statusElement() { 26 - return this.querySelector('[data-target="webauthn-register.status"]'); 27 - } 28 - 29 - /** @type {HTMLFormElement | null} */ 30 - get formElement() { 31 - return this.querySelector('form'); 32 - } 33 - 34 - connectedCallback() { 35 - const optionsJson = this.dataset.options; 36 - if (!optionsJson) { 37 - return; 38 - } 39 - 40 - this.#options = JSON.parse(optionsJson); 41 - 42 - const startButton = this.startButton; 43 - if (startButton) { 44 - startButton.disabled = false; 45 - startButton.addEventListener('click', (e) => { 46 - e.preventDefault(); 47 - this.#handleRegistration(); 48 - }); 49 - } 50 - } 51 - 52 - async #handleRegistration() { 53 - const options = this.#options; 54 - const status = this.statusElement; 55 - const startButton = this.startButton; 56 - const responseInput = this.responseInput; 57 - 58 - if (!options || !status || !responseInput) { 59 - console.error('WebAuthn register: missing required elements'); 60 - return; 61 - } 62 - 63 - try { 64 - if (startButton) { 65 - startButton.disabled = true; 66 - } 67 - status.textContent = 'Waiting for security key...'; 68 - 69 - // convert options to the format expected by navigator.credentials.create 70 - /** @type {PublicKeyCredentialCreationOptions} */ 71 - const publicKeyOptions = { 72 - ...options, 73 - challenge: fromBase64Url(options.challenge), 74 - user: { 75 - ...options.user, 76 - id: fromBase64Url(options.user.id), 77 - }, 78 - excludeCredentials: options.excludeCredentials?.map((cred) => ({ 79 - ...cred, 80 - id: fromBase64Url(cred.id), 81 - })), 82 - }; 83 - 84 - const credential = /** @type {PublicKeyCredential | null} */ ( 85 - await navigator.credentials.create({ publicKey: publicKeyOptions }) 86 - ); 87 - 88 - if (!credential) { 89 - status.textContent = 'Registration cancelled'; 90 - if (startButton) { 91 - startButton.disabled = false; 92 - } 93 - return; 94 - } 95 - 96 - const response = /** @type {AuthenticatorAttestationResponse} */ (credential.response); 97 - 98 - // serialize the response for the server 99 - const serialized = JSON.stringify({ 100 - id: credential.id, 101 - rawId: toBase64Url(credential.rawId), 102 - type: credential.type, 103 - response: { 104 - clientDataJSON: toBase64Url(response.clientDataJSON), 105 - attestationObject: toBase64Url(response.attestationObject), 106 - transports: response.getTransports?.() ?? [], 107 - }, 108 - clientExtensionResults: credential.getClientExtensionResults(), 109 - }); 110 - 111 - responseInput.value = serialized; 112 - status.textContent = 'Security key registered!'; 113 - 114 - // auto-submit the form 115 - this.formElement?.submit(); 116 - } catch (err) { 117 - if (startButton) { 118 - startButton.disabled = false; 119 - } 120 - 121 - if (err instanceof Error) { 122 - if (err.name === 'NotAllowedError') { 123 - status.textContent = 'Registration was cancelled or timed out. Please try again.'; 124 - } else if (err.name === 'InvalidStateError') { 125 - status.textContent = 'This security key is already registered.'; 126 - } else { 127 - status.textContent = `Registration failed: ${err.message}`; 128 - } 129 - } else { 130 - status.textContent = 'Registration failed. Please try again.'; 131 - } 132 - console.error('WebAuthn registration error:', err); 133 - } 134 - } 135 - } 136 - 137 - customElements.define('danaus-webauthn-register', WebAuthnRegisterElement); 138 - 139 - /** 140 - * @typedef {object} PublicKeyCredentialCreationOptionsJSON 141 - * @property {string} challenge 142 - * @property {object} rp 143 - * @property {string} rp.name 144 - * @property {string} [rp.id] 145 - * @property {object} user 146 - * @property {string} user.id 147 - * @property {string} user.name 148 - * @property {string} user.displayName 149 - * @property {PublicKeyCredentialParameters[]} pubKeyCredParams 150 - * @property {number} [timeout] 151 - * @property {Array<{id: string, type: 'public-key', transports?: AuthenticatorTransport[]}>} [excludeCredentials] 152 - * @property {AuthenticatorSelectionCriteria} [authenticatorSelection] 153 - * @property {AttestationConveyancePreference} [attestation] 154 - */
+148
packages/danaus/src/web/scripts/webauthn-register.ts
··· 1 + import { fromBase64Url, toBase64Url } from '@atcute/multibase'; 2 + 3 + /** 4 + * webauthn registration element. 5 + * 6 + * @attr {string} data-options - json PublicKeyCredentialCreationOptions 7 + */ 8 + class WebAuthnRegisterElement extends HTMLElement { 9 + #options: PublicKeyCredentialCreationOptionsJson | null = null; 10 + 11 + get startButton(): HTMLButtonElement | null { 12 + return this.querySelector('[data-target="webauthn-register.start"]'); 13 + } 14 + 15 + get responseInput(): HTMLInputElement | null { 16 + return this.querySelector('[data-target="webauthn-register.response"]'); 17 + } 18 + 19 + get statusElement(): HTMLElement | null { 20 + return this.querySelector('[data-target="webauthn-register.status"]'); 21 + } 22 + 23 + get formElement(): HTMLFormElement | null { 24 + return this.querySelector('form'); 25 + } 26 + 27 + connectedCallback() { 28 + const optionsJson = this.dataset.options; 29 + if (!optionsJson) { 30 + return; 31 + } 32 + 33 + this.#options = JSON.parse(optionsJson) as PublicKeyCredentialCreationOptionsJson; 34 + 35 + const startButton = this.startButton; 36 + if (startButton) { 37 + startButton.disabled = false; 38 + startButton.addEventListener('click', (event) => { 39 + event.preventDefault(); 40 + this.#handleRegistration(); 41 + }); 42 + } 43 + } 44 + 45 + async #handleRegistration() { 46 + const options = this.#options; 47 + const status = this.statusElement; 48 + const startButton = this.startButton; 49 + const responseInput = this.responseInput; 50 + 51 + if (!options || !status || !responseInput) { 52 + console.error('webauthn register: missing required elements'); 53 + return; 54 + } 55 + 56 + try { 57 + if (startButton) { 58 + startButton.disabled = true; 59 + } 60 + status.textContent = ''; 61 + 62 + const publicKeyOptions: PublicKeyCredentialCreationOptions = { 63 + ...options, 64 + challenge: fromBase64Url(options.challenge), 65 + user: { 66 + ...options.user, 67 + id: fromBase64Url(options.user.id), 68 + }, 69 + excludeCredentials: options.excludeCredentials?.map((credential) => ({ 70 + ...credential, 71 + id: fromBase64Url(credential.id), 72 + })), 73 + }; 74 + 75 + const credential = (await navigator.credentials.create({ 76 + publicKey: publicKeyOptions, 77 + })) as PublicKeyCredential | null; 78 + 79 + if (!credential) { 80 + status.textContent = 'Registration cancelled'; 81 + if (startButton) { 82 + startButton.disabled = false; 83 + } 84 + return; 85 + } 86 + 87 + const response = credential.response as AuthenticatorAttestationResponse; 88 + 89 + const serialized = JSON.stringify({ 90 + id: credential.id, 91 + rawId: toBase64Url(new Uint8Array(credential.rawId)), 92 + type: credential.type, 93 + response: { 94 + clientDataJSON: toBase64Url(new Uint8Array(response.clientDataJSON)), 95 + attestationObject: toBase64Url(new Uint8Array(response.attestationObject)), 96 + transports: response.getTransports?.() ?? [], 97 + }, 98 + clientExtensionResults: credential.getClientExtensionResults(), 99 + }); 100 + 101 + responseInput.value = serialized; 102 + status.textContent = 'Security key registered!'; 103 + 104 + this.formElement?.submit(); 105 + } catch (err) { 106 + if (startButton) { 107 + startButton.disabled = false; 108 + } 109 + 110 + if (err instanceof Error) { 111 + if (err.name === 'NotAllowedError') { 112 + status.textContent = 'Registration was cancelled or timed out. Please try again.'; 113 + } else if (err.name === 'InvalidStateError') { 114 + status.textContent = 'This security key is already registered.'; 115 + } else { 116 + status.textContent = `Registration failed: ${err.message}`; 117 + } 118 + } else { 119 + status.textContent = 'Registration failed. Please try again.'; 120 + } 121 + console.error('webauthn registration error:', err); 122 + } 123 + } 124 + } 125 + 126 + customElements.define('danaus-webauthn-register', WebAuthnRegisterElement); 127 + 128 + type PublicKeyCredentialCreationOptionsJson = { 129 + challenge: string; 130 + rp: { 131 + name: string; 132 + id?: string; 133 + }; 134 + user: { 135 + id: string; 136 + name: string; 137 + displayName: string; 138 + }; 139 + pubKeyCredParams: PublicKeyCredentialParameters[]; 140 + timeout?: number; 141 + excludeCredentials?: Array<{ 142 + id: string; 143 + type: 'public-key'; 144 + transports?: AuthenticatorTransport[]; 145 + }>; 146 + authenticatorSelection?: AuthenticatorSelectionCriteria; 147 + attestation?: AttestationConveyancePreference; 148 + };
+1 -1
packages/danaus/src/web/styles/main.css
··· 1 1 @import 'tailwindcss' source('../'); 2 - @source not './main.out.css'; 2 + @source not '../../../public/style.css'; 3 3 4 4 @theme { 5 5 --text-*: initial;
+100 -3
packages/danaus/src/web/styles/main.out.css
··· 245 245 container-type: inline-size; 246 246 container-name: dialog-body; 247 247 } 248 + .\@container { 249 + container-type: inline-size; 250 + } 248 251 .pointer-events-none { 249 252 pointer-events: none; 250 253 } ··· 283 286 .right-0 { 284 287 right: calc(var(--spacing) * 0); 285 288 } 289 + .right-2 { 290 + right: calc(var(--spacing) * 2); 291 + } 286 292 .right-2\.5 { 287 293 right: calc(var(--spacing) * 2.5); 288 294 } 295 + .-left-1 { 296 + left: calc(var(--spacing) * -1); 297 + } 289 298 .-left-1\.5 { 290 299 left: calc(var(--spacing) * -1.5); 291 300 } ··· 322 331 .m-2 { 323 332 margin: calc(var(--spacing) * 2); 324 333 } 334 + .-mx-1 { 335 + margin-inline: calc(var(--spacing) * -1); 336 + } 325 337 .-mx-1\.25 { 326 338 margin-inline: calc(var(--spacing) * -1.25); 327 339 } 340 + .-my-0 { 341 + margin-block: calc(var(--spacing) * -0); 342 + } 328 343 .-my-0\.5 { 329 344 margin-block: calc(var(--spacing) * -0.5); 345 + } 346 + .my-0 { 347 + margin-block: calc(var(--spacing) * 0); 330 348 } 331 349 .my-0\.5 { 332 350 margin-block: calc(var(--spacing) * 0.5); ··· 342 360 } 343 361 .mr-2 { 344 362 margin-right: calc(var(--spacing) * 2); 363 + } 364 + .mb-1 { 365 + margin-bottom: calc(var(--spacing) * 1); 345 366 } 346 367 .mb-1\.5 { 347 368 margin-bottom: calc(var(--spacing) * 1.5); ··· 379 400 .inline-flex { 380 401 display: inline-flex; 381 402 } 403 + .table { 404 + display: table; 405 + } 382 406 .size-5 { 383 407 width: calc(var(--spacing) * 5); 384 408 height: calc(var(--spacing) * 5); 385 409 } 410 + .h-2 { 411 + height: calc(var(--spacing) * 2); 412 + } 386 413 .h-2\.5 { 387 414 height: calc(var(--spacing) * 2.5); 388 415 } ··· 406 433 } 407 434 .h-full { 408 435 height: 100%; 436 + } 437 + .h-px { 438 + height: 1px; 409 439 } 410 440 .max-h-\[calc\(100dvh-48px\)\] { 411 441 max-height: calc(100dvh - 48px); ··· 427 457 } 428 458 .w-1 { 429 459 width: calc(var(--spacing) * 1); 460 + } 461 + .w-2 { 462 + width: calc(var(--spacing) * 2); 430 463 } 431 464 .w-2\.5 { 432 465 width: calc(var(--spacing) * 2.5); ··· 485 518 .flex-1 { 486 519 flex: 1; 487 520 } 521 + .flex-shrink { 522 + flex-shrink: 1; 523 + } 488 524 .shrink { 489 525 flex-shrink: 1; 490 526 } 491 527 .shrink-0 { 492 528 flex-shrink: 0; 529 + } 530 + .flex-grow { 531 + flex-grow: 1; 493 532 } 494 533 .grow { 495 534 flex-grow: 1; ··· 497 536 .basis-0 { 498 537 flex-basis: calc(var(--spacing) * 0); 499 538 } 539 + .border-collapse { 540 + border-collapse: collapse; 541 + } 500 542 .popover-animate { 501 543 --_slide-x: 0; 502 544 --_slide-y: 8px; ··· 541 583 } 542 584 } 543 585 } 586 + .transform { 587 + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); 588 + } 544 589 .cursor-pointer { 545 590 cursor: pointer; 546 591 } 592 + .resize { 593 + resize: both; 594 + } 547 595 .list-none { 548 596 list-style-type: none; 549 597 } ··· 565 613 .flex-nowrap { 566 614 flex-wrap: nowrap; 567 615 } 616 + .flex-wrap { 617 + flex-wrap: wrap; 618 + } 568 619 .place-items-center { 569 620 place-items: center; 570 621 } ··· 586 637 .justify-end { 587 638 justify-content: flex-end; 588 639 } 640 + .gap-0 { 641 + gap: calc(var(--spacing) * 0); 642 + } 589 643 .gap-0\.5 { 590 644 gap: calc(var(--spacing) * 0.5); 591 645 } ··· 724 778 .bg-neutral-background-3 { 725 779 background-color: var(--color-neutral-background-3); 726 780 } 781 + .bg-neutral-stroke-2 { 782 + background-color: var(--color-neutral-stroke-2); 783 + } 727 784 .bg-status-danger-background-1 { 728 785 background-color: var(--color-status-danger-background-1); 729 786 } ··· 760 817 .p-8 { 761 818 padding: calc(var(--spacing) * 8); 762 819 } 820 + .px-1 { 821 + padding-inline: calc(var(--spacing) * 1); 822 + } 763 823 .px-1\.5 { 764 824 padding-inline: calc(var(--spacing) * 1.5); 765 825 } ··· 774 834 } 775 835 .px-4 { 776 836 padding-inline: calc(var(--spacing) * 4); 837 + } 838 + .py-0 { 839 + padding-block: calc(var(--spacing) * 0); 777 840 } 778 841 .py-0\.5 { 779 842 padding-block: calc(var(--spacing) * 0.5); ··· 916 979 .no-underline { 917 980 text-decoration-line: none; 918 981 } 982 + .underline { 983 + text-decoration-line: underline; 984 + } 919 985 .dialog-backdrop-animate { 920 986 &::backdrop { 921 987 transition-property: opacity, overlay, display; ··· 937 1003 .opacity-0 { 938 1004 opacity: 0%; 939 1005 } 940 - .opacity-50 { 941 - opacity: 50%; 942 - } 943 1006 .shadow-4 { 944 1007 --tw-shadow: var(--shadow-4); 945 1008 box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); ··· 951 1014 .shadow-64 { 952 1015 --tw-shadow: var(--shadow-64); 953 1016 box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); 1017 + } 1018 + .outline { 1019 + outline-style: var(--tw-outline-style); 1020 + outline-width: 1px; 954 1021 } 955 1022 .outline-2 { 956 1023 outline-style: var(--tw-outline-style); ··· 1200 1267 .open\:flex { 1201 1268 &:is([open], :popover-open, :open) { 1202 1269 display: flex; 1270 + } 1271 + } 1272 + .empty\:hidden { 1273 + &:empty { 1274 + display: none; 1203 1275 } 1204 1276 } 1205 1277 .hover\:border-neutral-stroke-1-hover { ··· 1690 1762 overflow: hidden; 1691 1763 } 1692 1764 } 1765 + @property --tw-rotate-x { 1766 + syntax: "*"; 1767 + inherits: false; 1768 + } 1769 + @property --tw-rotate-y { 1770 + syntax: "*"; 1771 + inherits: false; 1772 + } 1773 + @property --tw-rotate-z { 1774 + syntax: "*"; 1775 + inherits: false; 1776 + } 1777 + @property --tw-skew-x { 1778 + syntax: "*"; 1779 + inherits: false; 1780 + } 1781 + @property --tw-skew-y { 1782 + syntax: "*"; 1783 + inherits: false; 1784 + } 1693 1785 @property --tw-divide-y-reverse { 1694 1786 syntax: "*"; 1695 1787 inherits: false; ··· 1785 1877 @layer properties { 1786 1878 @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { 1787 1879 *, ::before, ::after, ::backdrop { 1880 + --tw-rotate-x: initial; 1881 + --tw-rotate-y: initial; 1882 + --tw-rotate-z: initial; 1883 + --tw-skew-x: initial; 1884 + --tw-skew-y: initial; 1788 1885 --tw-divide-y-reverse: 0; 1789 1886 --tw-border-style: solid; 1790 1887 --tw-font-weight: initial;
+1 -4
packages/danaus/tsconfig.client.json
··· 9 9 10 10 /* Bundler mode */ 11 11 "moduleResolution": "bundler", 12 + "allowImportingTsExtensions": true, 12 13 "verbatimModuleSyntax": true, 13 14 "moduleDetection": "force", 14 15 "noEmit": true, 15 16 "composite": true, 16 - 17 - /* Check JS files with JSDoc (only files with // @ts-check) */ 18 - "allowJs": true, 19 - "checkJs": false, 20 17 21 18 /* Linting */ 22 19 "strict": true,
+233 -5
pnpm-lock.yaml
··· 101 101 specifier: ^0.2.1 102 102 version: 0.2.1 103 103 '@oomfware/forms': 104 - specifier: ^0.2.2 105 - version: 0.2.2(@oomfware/fetch-router@0.2.1) 104 + specifier: ^0.3.0 105 + version: 0.3.0(@oomfware/fetch-router@0.2.1) 106 106 '@oomfware/jsx': 107 107 specifier: ^0.1.5 108 108 version: 0.1.5 ··· 137 137 '@danaus/dev-env': 138 138 specifier: workspace:* 139 139 version: link:../dev-env 140 + '@rollup/plugin-multi-entry': 141 + specifier: ^7.1.0 142 + version: 7.1.0 140 143 '@standard-schema/spec': 141 144 specifier: ^1.1.0 142 145 version: 1.1.0 ··· 149 152 '@types/qrcode': 150 153 specifier: ^1.5.6 151 154 version: 1.5.6 155 + chokidar: 156 + specifier: ^5.0.0 157 + version: 5.0.0 152 158 concurrently: 153 159 specifier: ^9.2.1 154 160 version: 9.2.1 155 161 drizzle-kit: 156 162 specifier: 1.0.0-beta.6-4414a19 157 163 version: 1.0.0-beta.6-4414a19 164 + rolldown: 165 + specifier: 1.0.0-beta.60 166 + version: 1.0.0-beta.60 158 167 tailwindcss: 159 168 specifier: ^4.1.18 160 169 version: 4.1.18 ··· 902 911 '@oomfware/fetch-router@0.2.1': 903 912 resolution: {integrity: sha512-WV0cSeKjyTmM2pXYlRzv1md3Dym1vMR8PnJ/GfZUg8i1GS7RIDezmMkqVaWI/9IpeOHhs+QeDO41q1u+z1EzSg==} 904 913 905 - '@oomfware/forms@0.2.2': 906 - resolution: {integrity: sha512-NEwj0jk8EC5GKAGq8J5GAxbm1rKdvdJkTxER/RGAdW0pCInoyN3gZiSHTuQisJRgoz0DjzF2tor8g6Hb8+em6w==} 914 + '@oomfware/forms@0.3.0': 915 + resolution: {integrity: sha512-iXeY1goUsej9yTXNogXwMbLQU4HO26C1XhLawdfAIW9f0/sJpk+GJjly6LZY3gARPFoqty/vruzc/BtUqpoGAQ==} 907 916 peerDependencies: 908 917 '@oomfware/fetch-router': ^0.2.1 909 918 ··· 1006 1015 engines: {node: ^20.19.0 || >=22.12.0} 1007 1016 cpu: [x64] 1008 1017 os: [win32] 1018 + 1019 + '@oxc-project/types@0.108.0': 1020 + resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==} 1009 1021 1010 1022 '@oxc-project/types@0.99.0': 1011 1023 resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==} ··· 1310 1322 '@remix-run/route-pattern@0.16.0': 1311 1323 resolution: {integrity: sha512-Co6bPtODF7cLYVBweayRXfEb31ybz45WqwT/u72eDQJZgRSVKFf0Ps9fqinSaiX0Xp7jvkRCBAbSUgLuLLjzuw==} 1312 1324 1325 + '@rolldown/binding-android-arm64@1.0.0-beta.60': 1326 + resolution: {integrity: sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ==} 1327 + engines: {node: ^20.19.0 || >=22.12.0} 1328 + cpu: [arm64] 1329 + os: [android] 1330 + 1331 + '@rolldown/binding-darwin-arm64@1.0.0-beta.60': 1332 + resolution: {integrity: sha512-vyDA4HXY2mP8PPtl5UE17uGPxUNG4m1wkfa3kAkR8JWrFbarV97UmLq22IWrNhtBPa89xqerzLK8KoVmz5JqCQ==} 1333 + engines: {node: ^20.19.0 || >=22.12.0} 1334 + cpu: [arm64] 1335 + os: [darwin] 1336 + 1337 + '@rolldown/binding-darwin-x64@1.0.0-beta.60': 1338 + resolution: {integrity: sha512-WnxyqxAKP2BsxouwGY/RCF5UFw/LA4QOHhJ7VEl+UCelHokiwqNHRbryLAyRy3TE1FZ5eae+vAFcaetAu/kWLw==} 1339 + engines: {node: ^20.19.0 || >=22.12.0} 1340 + cpu: [x64] 1341 + os: [darwin] 1342 + 1343 + '@rolldown/binding-freebsd-x64@1.0.0-beta.60': 1344 + resolution: {integrity: sha512-JtyWJ+zXOHof5gOUYwdTWI2kL6b8q9eNwqB/oD4mfUFaC/COEB2+47JMhcq78dey9Ahmec3DZKRDZPRh9hNAMQ==} 1345 + engines: {node: ^20.19.0 || >=22.12.0} 1346 + cpu: [x64] 1347 + os: [freebsd] 1348 + 1349 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': 1350 + resolution: {integrity: sha512-LrMoKqpHx+kCaNSk84iSBd4yVOymLIbxJQtvFjDN2CjQraownR+IXcwYDblFcj9ivmS54T3vCboXBbm3s1zbPQ==} 1351 + engines: {node: ^20.19.0 || >=22.12.0} 1352 + cpu: [arm] 1353 + os: [linux] 1354 + 1355 + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': 1356 + resolution: {integrity: sha512-sqI+Vdx1gmXJMsXN3Fsewm3wlt7RHvRs1uysSp//NLsCoh9ZFEUr4ZzGhWKOg6Rvf+njNu/vCsz96x7wssLejQ==} 1357 + engines: {node: ^20.19.0 || >=22.12.0} 1358 + cpu: [arm64] 1359 + os: [linux] 1360 + 1361 + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': 1362 + resolution: {integrity: sha512-8xlqGLDtTP8sBfYwneTDu8+PRm5reNEHAuI/+6WPy9y350ls0KTFd3EJCOWEXWGW0F35ko9Fn9azmurBTjqOrQ==} 1363 + engines: {node: ^20.19.0 || >=22.12.0} 1364 + cpu: [arm64] 1365 + os: [linux] 1366 + 1367 + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': 1368 + resolution: {integrity: sha512-iR4nhVouVZK1CiGGGyz+prF5Lw9Lmz30Rl36Hajex+dFVFiegka604zBwzTp5Tl0BZnr50ztnVJ30tGrBhDr8Q==} 1369 + engines: {node: ^20.19.0 || >=22.12.0} 1370 + cpu: [x64] 1371 + os: [linux] 1372 + 1373 + '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': 1374 + resolution: {integrity: sha512-HbfNcqNeqxFjSMf1Kpe8itr2e2lr0Bm6HltD2qXtfU91bSSikVs9EWsa1ThshQ1v2ZvxXckGjlVLtah6IoslPg==} 1375 + engines: {node: ^20.19.0 || >=22.12.0} 1376 + cpu: [x64] 1377 + os: [linux] 1378 + 1379 + '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': 1380 + resolution: {integrity: sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==} 1381 + engines: {node: ^20.19.0 || >=22.12.0} 1382 + cpu: [arm64] 1383 + os: [openharmony] 1384 + 1385 + '@rolldown/binding-wasm32-wasi@1.0.0-beta.60': 1386 + resolution: {integrity: sha512-6roXGbHMdR2ucnxXuwbmQvk8tuYl3VGu0yv13KxspyKBxxBd4RS6iykzLD6mX2gMUHhfX8SVWz7n/62gfyKHow==} 1387 + engines: {node: '>=14.0.0'} 1388 + cpu: [wasm32] 1389 + 1390 + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': 1391 + resolution: {integrity: sha512-JBOm8/DC/CKnHyMHoJFdvzVHxUixid4dGkiTqGflxOxO43uSJMpl77pSPXvzwZ/VXwqblU2V0/PanyCBcRLowQ==} 1392 + engines: {node: ^20.19.0 || >=22.12.0} 1393 + cpu: [arm64] 1394 + os: [win32] 1395 + 1396 + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': 1397 + resolution: {integrity: sha512-MKF0B823Efp+Ot8KsbwIuGhKH58pf+2rSM6VcqyNMlNBHheOM0Gf7JmEu+toc1jgN6fqjH7Et+8hAzsLVkIGfA==} 1398 + engines: {node: ^20.19.0 || >=22.12.0} 1399 + cpu: [x64] 1400 + os: [win32] 1401 + 1402 + '@rolldown/pluginutils@1.0.0-beta.60': 1403 + resolution: {integrity: sha512-Jz4aqXRPVtqkH1E3jRDzLO5cgN5JwW+WG0wXGE4NiJd25nougv/AHzxmKCzmVQUYnxLmTM0M4wrZp+LlC2FKLg==} 1404 + 1405 + '@rollup/plugin-multi-entry@7.1.0': 1406 + resolution: {integrity: sha512-dLp8gf+Kb/JvGQOreC6pg2gCFWPNmKaZFUzBRN1KgiOjHMN7ZfVM11dJS0BjmcIpeA6KPbWq2tFB9OwTb2N5Cw==} 1407 + engines: {node: '>=14.0.0'} 1408 + peerDependencies: 1409 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 1410 + peerDependenciesMeta: 1411 + rollup: 1412 + optional: true 1413 + 1414 + '@rollup/plugin-virtual@3.0.2': 1415 + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} 1416 + engines: {node: '>=14.0.0'} 1417 + peerDependencies: 1418 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 1419 + peerDependenciesMeta: 1420 + rollup: 1421 + optional: true 1422 + 1313 1423 '@simplewebauthn/server@13.2.2': 1314 1424 resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==} 1315 1425 engines: {node: '>=20.0.0'} ··· 1725 1835 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1726 1836 engines: {node: '>=10'} 1727 1837 1838 + chokidar@5.0.0: 1839 + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} 1840 + engines: {node: '>= 20.19.0'} 1841 + 1728 1842 cliui@6.0.0: 1729 1843 resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} 1730 1844 ··· 2086 2200 fast-redact@3.5.0: 2087 2201 resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} 2088 2202 engines: {node: '>=6'} 2203 + 2204 + fdir@6.5.0: 2205 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 2206 + engines: {node: '>=12.0.0'} 2207 + peerDependencies: 2208 + picomatch: ^3 || ^4 2209 + peerDependenciesMeta: 2210 + picomatch: 2211 + optional: true 2089 2212 2090 2213 fill-range@7.1.1: 2091 2214 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} ··· 2649 2772 resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 2650 2773 engines: {node: '>=8.6'} 2651 2774 2775 + picomatch@4.0.3: 2776 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 2777 + engines: {node: '>=12'} 2778 + 2652 2779 pino-abstract-transport@1.2.0: 2653 2780 resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} 2654 2781 ··· 2802 2929 resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} 2803 2930 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 2804 2931 2932 + readdirp@5.0.0: 2933 + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} 2934 + engines: {node: '>= 20.19.0'} 2935 + 2805 2936 real-require@0.2.0: 2806 2937 resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} 2807 2938 engines: {node: '>= 12.13.0'} ··· 2834 2965 resolution: {integrity: sha512-RyXI+aNxwVyfF71a9cqz/jhXWbycnVh7GXnnJUniIBXKTOJQF3rmpNexStXt8TUcKyiXCwyfYzboZLMYUllPDA==} 2835 2966 engines: {node: '>=18.0'} 2836 2967 2968 + rolldown@1.0.0-beta.60: 2969 + resolution: {integrity: sha512-YYgpv7MiTp9LdLj1fzGzCtij8Yi2OKEc3HQtfbIxW4yuSgpQz9518I69U72T5ErPA/ATOXqlcisiLrWy+5V9YA==} 2970 + engines: {node: ^20.19.0 || >=22.12.0} 2971 + hasBin: true 2972 + 2837 2973 run-applescript@7.1.0: 2838 2974 resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} 2839 2975 engines: {node: '>=18'} ··· 2979 3115 2980 3116 thread-stream@2.7.0: 2981 3117 resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} 3118 + 3119 + tinyglobby@0.2.15: 3120 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 3121 + engines: {node: '>=12.0.0'} 2982 3122 2983 3123 tlds@1.261.0: 2984 3124 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 4061 4201 dependencies: 4062 4202 '@remix-run/route-pattern': 0.16.0 4063 4203 4064 - '@oomfware/forms@0.2.2(@oomfware/fetch-router@0.2.1)': 4204 + '@oomfware/forms@0.3.0(@oomfware/fetch-router@0.2.1)': 4065 4205 dependencies: 4066 4206 '@oomfware/fetch-router': 0.2.1 4067 4207 '@standard-schema/spec': 1.1.0 ··· 4122 4262 4123 4263 '@oxc-parser/binding-win32-x64-msvc@0.99.0': 4124 4264 optional: true 4265 + 4266 + '@oxc-project/types@0.108.0': {} 4125 4267 4126 4268 '@oxc-project/types@0.99.0': {} 4127 4269 ··· 4403 4545 4404 4546 '@remix-run/route-pattern@0.16.0': {} 4405 4547 4548 + '@rolldown/binding-android-arm64@1.0.0-beta.60': 4549 + optional: true 4550 + 4551 + '@rolldown/binding-darwin-arm64@1.0.0-beta.60': 4552 + optional: true 4553 + 4554 + '@rolldown/binding-darwin-x64@1.0.0-beta.60': 4555 + optional: true 4556 + 4557 + '@rolldown/binding-freebsd-x64@1.0.0-beta.60': 4558 + optional: true 4559 + 4560 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': 4561 + optional: true 4562 + 4563 + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': 4564 + optional: true 4565 + 4566 + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': 4567 + optional: true 4568 + 4569 + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': 4570 + optional: true 4571 + 4572 + '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': 4573 + optional: true 4574 + 4575 + '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': 4576 + optional: true 4577 + 4578 + '@rolldown/binding-wasm32-wasi@1.0.0-beta.60': 4579 + dependencies: 4580 + '@napi-rs/wasm-runtime': 1.1.1 4581 + optional: true 4582 + 4583 + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': 4584 + optional: true 4585 + 4586 + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': 4587 + optional: true 4588 + 4589 + '@rolldown/pluginutils@1.0.0-beta.60': {} 4590 + 4591 + '@rollup/plugin-multi-entry@7.1.0': 4592 + dependencies: 4593 + '@rollup/plugin-virtual': 3.0.2 4594 + tinyglobby: 0.2.15 4595 + 4596 + '@rollup/plugin-virtual@3.0.2': {} 4597 + 4406 4598 '@simplewebauthn/server@13.2.2': 4407 4599 dependencies: 4408 4600 '@hexagon/base64': 1.1.28 ··· 4818 5010 ansi-styles: 4.3.0 4819 5011 supports-color: 7.2.0 4820 5012 5013 + chokidar@5.0.0: 5014 + dependencies: 5015 + readdirp: 5.0.0 5016 + 4821 5017 cliui@6.0.0: 4822 5018 dependencies: 4823 5019 string-width: 4.2.3 ··· 5115 5311 5116 5312 fast-redact@3.5.0: {} 5117 5313 5314 + fdir@6.5.0(picomatch@4.0.3): 5315 + optionalDependencies: 5316 + picomatch: 4.0.3 5317 + 5118 5318 fill-range@7.1.1: 5119 5319 dependencies: 5120 5320 to-regex-range: 5.0.1 ··· 5649 5849 5650 5850 picomatch@2.3.1: {} 5651 5851 5852 + picomatch@4.0.3: {} 5853 + 5652 5854 pino-abstract-transport@1.2.0: 5653 5855 dependencies: 5654 5856 readable-stream: 4.7.0 ··· 5770 5972 process: 0.11.10 5771 5973 string_decoder: 1.3.0 5772 5974 5975 + readdirp@5.0.0: {} 5976 + 5773 5977 real-require@0.2.0: {} 5774 5978 5775 5979 redis-errors@1.2.0: {} ··· 5793 5997 fast-printf: 1.6.10 5794 5998 safe-stable-stringify: 2.5.0 5795 5999 semver-compare: 1.0.0 6000 + 6001 + rolldown@1.0.0-beta.60: 6002 + dependencies: 6003 + '@oxc-project/types': 0.108.0 6004 + '@rolldown/pluginutils': 1.0.0-beta.60 6005 + optionalDependencies: 6006 + '@rolldown/binding-android-arm64': 1.0.0-beta.60 6007 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.60 6008 + '@rolldown/binding-darwin-x64': 1.0.0-beta.60 6009 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.60 6010 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.60 6011 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.60 6012 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.60 6013 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.60 6014 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.60 6015 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.60 6016 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.60 6017 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.60 6018 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.60 5796 6019 5797 6020 run-applescript@7.1.0: {} 5798 6021 ··· 5997 6220 thread-stream@2.7.0: 5998 6221 dependencies: 5999 6222 real-require: 0.2.0 6223 + 6224 + tinyglobby@0.2.15: 6225 + dependencies: 6226 + fdir: 6.5.0(picomatch@4.0.3) 6227 + picomatch: 4.0.3 6000 6228 6001 6229 tlds@1.261.0: {} 6002 6230