Project renamed to Paskia.

This commit is contained in:
2025-12-05 13:17:52 +00:00
parent 8609f2fe69
commit 208115ebc3
66 changed files with 232 additions and 139 deletions
+3 -4
View File
@@ -3,7 +3,6 @@ dist/
.*
!.gitignore
*.lock
passkey-auth.sqlite
/passkey/frontend-build
/test_*.py
passkey/_version.py
paskia.sqlite
/paskia/frontend-build
/paskia/_version.py
+2 -2
View File
@@ -1,6 +1,6 @@
# PassKey Auth API Documentation
# Paskia API Documentation
This document lists the HTTP and WebSocket endpoints exposed by the PassKey Auth
This document lists the HTTP and WebSocket endpoints exposed by the Paskia
service and how they behave depending on whether a dedicated authentication host
(`--auth-host` / environment `PASSKEY_AUTH_HOST`) is configured.
+12 -13
View File
@@ -1,4 +1,4 @@
# PasskeyAuth
# Paskia
A minimal FastAPI WebAuthn server with WebSocket support for passkey registration. This project demonstrates WebAuthn registration flow with Resident Keys (discoverable credentials) using modern Python tooling.
@@ -31,24 +31,23 @@ uv pip install -e .[dev]
### Run (new CLI)
`passkey-auth` now provides subcommands:
`paskia` now provides subcommands:
```text
passkey-auth serve [host:port] [--options]
passkey-auth dev [--options]
paskia serve [host:port] [--options]
```
Examples (fish shell shown):
```fish
# Production style (no reload)
passkey-auth serve
passkey-auth serve 0.0.0.0:8080 --rp-id example.com --origin https://example.com
paskia serve
paskia serve 0.0.0.0:8080 --rp-id example.com --origin https://example.com
# Development (auto-reload)
passkey-auth dev # localhost:4401
passkey-auth dev :5500 # localhost on port 5500
passkey-auth dev 127.0.0.1 # host only, default port 4401
# Development (auto-reload via scripts/dev.py)
python scripts/dev.py # localhost:4401
python scripts/dev.py :5500 # localhost on port 5500
python scripts/dev.py 127.0.0.1 # host only, default port 4401
```
Available options (both subcommands):
@@ -61,7 +60,7 @@ Available options (both subcommands):
### Legacy Invocation
If you previously used `python -m passkey.fastapi --dev --host ...`, switch to the new form above. The old flags `--host`, `--port`, and `--dev` are replaced by the `[host:port]` positional and the `dev` subcommand.
If you previously used `python -m paskia.fastapi --dev --host ...`, switch to the new form above. The old flags `--host`, `--port`, and `--dev` are replaced by using `scripts/dev.py` for development mode.
## Usage (Web)
@@ -90,8 +89,8 @@ hatch run ruff format .
### Project Structure
```
passkeyauth/
├── passkeyauth/
paskia/
├── paskia/
│ ├── __init__.py
│ └── main.py # FastAPI server with WebSocket support
├── static/
+3 -3
View File
@@ -1,6 +1,6 @@
# PasskeyAuth E2E Tests
# Paskia E2E Tests
End-to-end tests for PasskeyAuth using [Playwright](https://playwright.dev/) with Chrome's **Virtual Authenticator**.
End-to-end tests for Paskia using [Playwright](https://playwright.dev/) with Chrome's **Virtual Authenticator**.
## Overview
@@ -33,7 +33,7 @@ npm test
```
This will:
1. Start a fresh PasskeyAuth server with a test database
1. Start a fresh Paskia server with a test database
2. Run all E2E tests against it
3. Clean up the server when done
+2 -2
View File
@@ -1,8 +1,8 @@
{
"name": "passkey-auth-e2e",
"name": "paskia-e2e",
"version": "1.0.0",
"private": true,
"description": "E2E tests for PasskeyAuth using Playwright with Virtual Authenticator",
"description": "E2E tests for Paskia using Playwright with Virtual Authenticator",
"type": "module",
"scripts": {
"test": "bunx playwright test",
+2 -2
View File
@@ -1,7 +1,7 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Playwright configuration for PasskeyAuth E2E tests.
* Playwright configuration for Paskia E2E tests.
* Uses Chrome's Virtual Authenticator for automated passkey testing.
*
* Run with: bun run test
@@ -23,7 +23,7 @@ export default defineConfig({
globalTeardown: './tests/global-teardown.ts',
use: {
// Base URL for the passkey-auth server
// Base URL for the Paskia server
baseURL: process.env.BASE_URL || 'http://localhost:4401',
// Collect trace on failure for debugging
+1 -1
View File
@@ -45,7 +45,7 @@ export default async function globalSetup() {
// Start the server using Node's spawn
const serverProcess = spawn('uv', [
'run', 'passkey-auth', 'serve', ':4401',
'run', 'paskia', 'serve', ':4401',
'--rp-id', 'localhost',
'--origin', 'http://localhost:4401'
], {
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from './fixtures/passkey-helpers'
/**
* E2E tests for PasskeyAuth using Chrome's Virtual Authenticator.
* E2E tests for Paskia using Chrome's Virtual Authenticator.
*
* These tests exercise the complete WebAuthn flow:
* 1. Registration via WebSocket using bootstrap reset token
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PassKey Auth - Dev Mode</title>
<title>Paskia - Dev Mode</title>
<style>
:root {
color-scheme: light dark; /* Automatic themes by browser */
@@ -33,7 +33,7 @@
<body>
<div class="container">
<header>
<h1>🔐 PassKey Auth</h1>
<h1>🔐 Paskia</h1>
<p class="subtitle">Development server demonstration page.</p>
</header>
+2 -2
View File
@@ -27,7 +27,7 @@ export default defineConfig(({ command }) => ({
closeBundle() {
if (command !== 'build') return
const outDir = resolve(__dirname, '../passkey/frontend-build')
const outDir = resolve(__dirname, '../paskia/frontend-build')
const moves = [
{ from: 'auth.html', to: 'auth/index.html' },
{ from: 'admin.html', to: 'admin/index.html' },
@@ -69,7 +69,7 @@ export default defineConfig(({ command }) => ({
}
},
build: {
outDir: '../passkey/frontend-build',
outDir: '../paskia/frontend-build',
emptyOutDir: true,
rollupOptions: {
input: {
+3
View File
@@ -0,0 +1,3 @@
from paskia.sansio import Passkey
__all__ = ["Passkey"]
@@ -14,7 +14,7 @@ from importlib.resources import files
__ALL__ = ["AAGUID", "filter"]
# Path to the AAGUID JSON file
AAGUID_FILE = files("passkey") / "aaguid" / "combined_aaguid.json"
AAGUID_FILE = files("paskia") / "aaguid" / "combined_aaguid.json"
AAGUID: dict[str, dict] = json.loads(AAGUID_FILE.read_text(encoding="utf-8"))
@@ -11,11 +11,11 @@ independent of any web framework:
from datetime import datetime, timezone
from uuid import UUID
from .config import SESSION_LIFETIME
from .db import ResetToken, Session
from .globals import db, passkey
from .util import hostutil
from .util.tokens import create_token, reset_key, session_key
from paskia.config import SESSION_LIFETIME
from paskia.db import ResetToken, Session
from paskia.globals import db, passkey
from paskia.util import hostutil
from paskia.util.tokens import create_token, reset_key, session_key
EXPIRES = SESSION_LIFETIME
+3 -3
View File
@@ -12,9 +12,9 @@ from datetime import datetime, timezone
import uuid7
from . import authsession, globals
from .db import Org, Permission, Role, User
from .util import hostutil, passphrase, tokens
from paskia import authsession, globals
from paskia.db import Org, Permission, Role, User
from paskia.util import hostutil, passphrase, tokens
def _init_logger() -> logging.Logger:
+4 -4
View File
@@ -26,9 +26,8 @@ from sqlalchemy.dialects.sqlite import BLOB
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from ..config import SESSION_LIFETIME
from ..globals import db
from . import (
from paskia.config import SESSION_LIFETIME
from paskia.db import (
Credential,
DatabaseInterface,
Org,
@@ -39,8 +38,9 @@ from . import (
SessionContext,
User,
)
from paskia.globals import db
DB_PATH = "sqlite+aiosqlite:///passkey-auth.sqlite"
DB_PATH = "sqlite+aiosqlite:///paskia.sqlite"
def _normalize_dt(value: datetime | None) -> datetime | None:
+3
View File
@@ -0,0 +1,3 @@
from paskia.fastapi.mainapp import app
__all__ = ["app"]
@@ -126,7 +126,7 @@ def main():
logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
parser = argparse.ArgumentParser(
prog="passkey-auth", description="Passkey authentication server"
prog="paskia", description="Paskia authentication server"
)
sub = parser.add_subparsers(dest="command", required=True)
@@ -167,41 +167,41 @@ def main():
host = port = uds = all_ifaces = None # type: ignore
# Export configuration via environment for lifespan initialization in each process
os.environ.setdefault("PASSKEY_RP_ID", args.rp_id)
os.environ.setdefault("PASKIA_RP_ID", args.rp_id)
if args.rp_name:
os.environ["PASSKEY_RP_NAME"] = args.rp_name
os.environ["PASKIA_RP_NAME"] = args.rp_name
if args.origin:
os.environ["PASSKEY_ORIGIN"] = args.origin
os.environ["PASKIA_ORIGIN"] = args.origin
if getattr(args, "auth_host", None):
os.environ["PASSKEY_AUTH_HOST"] = args.auth_host
os.environ["PASKIA_AUTH_HOST"] = args.auth_host
else:
# Preserve pre-set env variable if CLI option omitted
args.auth_host = os.environ.get("PASSKEY_AUTH_HOST")
args.auth_host = os.environ.get("PASKIA_AUTH_HOST")
if args.auth_host:
validate_auth_host(args.auth_host, args.rp_id)
from passkey.util import hostutil as _hostutil # local import
from paskia.util import hostutil as _hostutil # local import
_hostutil.reload_config()
# One-time initialization + bootstrap before starting any server processes.
# Lifespan in worker processes will call globals.init with bootstrap disabled.
from passkey import globals as _globals # local import
from paskia import globals as _globals # local import
asyncio.run(
_globals.init(
rp_id=args.rp_id,
rp_name=args.rp_name,
origin=args.origin,
default_admin=os.getenv("PASSKEY_DEFAULT_ADMIN") or None,
default_org=os.getenv("PASSKEY_DEFAULT_ORG") or None,
default_admin=os.getenv("PASKIA_DEFAULT_ADMIN") or None,
default_org=os.getenv("PASKIA_DEFAULT_ORG") or None,
bootstrap=True,
)
)
# Handle recover-admin command (no server start)
if args.command == "reset":
from passkey.fastapi import reset as reset_cmd # local import
from paskia.fastapi import reset as reset_cmd # local import
exit_code = reset_cmd.run(getattr(args, "query", None))
raise SystemExit(exit_code)
@@ -211,15 +211,15 @@ def main():
"log_level": "info",
}
# Dev mode: enable reload when PASSKEY_DEVMODE is set
devmode = os.environ.get("PASSKEY_DEVMODE") == "1"
# Dev mode: enable reload when PASKIA_DEVMODE is set
devmode = os.environ.get("PASKIA_DEVMODE") == "1"
if devmode:
# Security: dev mode must run on localhost:4402 to prevent
# accidental public exposure of the Vite dev server
if host != "localhost" or port != 4402:
raise SystemExit(f"Dev mode requires localhost:4402, got {host}:{port}")
run_kwargs["reload"] = True
run_kwargs["reload_dirs"] = ["passkey"]
run_kwargs["reload_dirs"] = ["paskia"]
if uds:
run_kwargs["uds"] = uds
@@ -233,12 +233,12 @@ def main():
if devmode:
run_kwargs["host"] = "::"
run_kwargs["port"] = port
uvicorn.run("passkey.fastapi:app", **run_kwargs)
uvicorn.run("paskia.fastapi:app", **run_kwargs)
else:
# Production: run separate servers for IPv4 and IPv6
from uvicorn import Config, Server # noqa: E402 local import
from passkey.fastapi import (
from paskia.fastapi import (
app as fastapi_app, # noqa: E402 local import
)
@@ -261,7 +261,7 @@ def main():
asyncio.run(serve_both())
else:
uvicorn.run("passkey.fastapi:app", **run_kwargs)
uvicorn.run("paskia.fastapi:app", **run_kwargs)
if __name__ == "__main__":
@@ -5,9 +5,11 @@ from uuid import UUID, uuid4
from fastapi import Body, FastAPI, HTTPException, Request, Response
from fastapi.responses import JSONResponse
from ..authsession import reset_expires
from ..globals import db
from ..util import (
from paskia.authsession import reset_expires
from paskia.fastapi import authz
from paskia.fastapi.session import AUTH_COOKIE
from paskia.globals import db
from paskia.util import (
frontend,
hostutil,
passphrase,
@@ -16,9 +18,7 @@ from ..util import (
tokens,
useragent,
)
from ..util.tokens import encode_session_key, session_key
from . import authz
from .session import AUTH_COOKIE
from paskia.util.tokens import encode_session_key, session_key
app = FastAPI()
@@ -13,21 +13,19 @@ from fastapi import (
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBearer
from passkey.util import frontend
from ..authsession import (
from paskia.authsession import (
EXPIRES,
get_reset,
get_session,
refresh_session_token,
session_expiry,
)
from ..globals import db
from ..globals import passkey as global_passkey
from ..util import hostutil, htmlutil, passphrase, userinfo
from ..util.tokens import session_key
from . import authz, session, user
from .session import AUTH_COOKIE
from paskia.fastapi import authz, session, user
from paskia.fastapi.session import AUTH_COOKIE
from paskia.globals import db
from paskia.globals import passkey as global_passkey
from paskia.util import frontend, hostutil, htmlutil, passphrase, userinfo
from paskia.util.tokens import session_key
bearer_auth = HTTPBearer(auto_error=True)
@@ -3,7 +3,7 @@
from fastapi import Request, Response
from fastapi.responses import RedirectResponse
from passkey.util import hostutil, passphrase
from paskia.util import hostutil, passphrase
def is_ui_path(path: str) -> bool:
@@ -2,7 +2,7 @@ import logging
from fastapi import HTTPException
from ..util import permutil, sessionutil
from paskia.util import permutil, sessionutil
logger = logging.getLogger(__name__)
@@ -6,10 +6,9 @@ from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from passkey.util import frontend, hostutil, passphrase
from . import admin, api, auth_host, ws
from .session import AUTH_COOKIE
from paskia.fastapi import admin, api, auth_host, ws
from paskia.fastapi.session import AUTH_COOKIE
from paskia.util import frontend, hostutil, passphrase
@asynccontextmanager
@@ -19,15 +18,15 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path
We populate configuration from environment variables (set by the CLI entrypoint)
so that uvicorn reload / multiprocess workers inherit the settings.
"""
from .. import globals
from paskia import globals
rp_id = os.getenv("PASSKEY_RP_ID", "localhost")
rp_name = os.getenv("PASSKEY_RP_NAME") or None
origin = os.getenv("PASSKEY_ORIGIN") or None
rp_id = os.getenv("PASKIA_RP_ID", "localhost")
rp_name = os.getenv("PASKIA_RP_NAME") or None
origin = os.getenv("PASKIA_ORIGIN") or None
default_admin = (
os.getenv("PASSKEY_DEFAULT_ADMIN") or None
os.getenv("PASKIA_DEFAULT_ADMIN") or None
) # still passed for context
default_org = os.getenv("PASSKEY_DEFAULT_ORG") or None
default_org = os.getenv("PASKIA_DEFAULT_ORG") or None
try:
# CLI (__main__) performs bootstrap once; here we skip to avoid duplicate work
await globals.init(
@@ -56,7 +55,7 @@ app.mount("/auth/api/admin/", admin.app)
app.mount("/auth/api/", api.app)
app.mount("/auth/ws/", ws.app)
# In dev mode (PASSKEY_DEVMODE=1), Vite serves assets directly; skip static files mount
# In dev mode (PASKIA_DEVMODE=1), Vite serves assets directly; skip static files mount
if not frontend.is_dev_mode():
app.mount(
"/auth/assets/",
@@ -1,7 +1,7 @@
"""CLI support for creating user credential reset links.
Usage (via main CLI):
passkey-auth reset [query]
paskia reset [query]
If query is omitted, the master admin (first Administration role user in
an organization granting auth:admin) is targeted. Otherwise query is
@@ -15,10 +15,10 @@ from __future__ import annotations
import asyncio
from uuid import UUID
from passkey import authsession as _authsession
from passkey import globals as _g
from passkey.util import hostutil, passphrase
from passkey.util import tokens as _tokens
from paskia import authsession as _authsession
from paskia import globals as _g
from paskia.util import hostutil, passphrase
from paskia.util import tokens as _tokens
async def _resolve_targets(query: str | None):
@@ -10,7 +10,7 @@ Generic session management functions have been moved to authsession.py
from fastapi import Cookie, Request, Response, WebSocket
from ..authsession import EXPIRES
from paskia.authsession import EXPIRES
AUTH_COOKIE_NAME = "__Host-auth"
AUTH_COOKIE = Cookie(None, alias=AUTH_COOKIE_NAME)
@@ -10,16 +10,16 @@ from fastapi import (
)
from fastapi.responses import JSONResponse
from ..authsession import (
from paskia.authsession import (
delete_credential,
expires,
get_session,
)
from ..globals import db
from ..util import hostutil, passphrase, tokens
from ..util.tokens import decode_session_key, session_key
from . import authz, session
from .session import AUTH_COOKIE
from paskia.fastapi import authz, session
from paskia.fastapi.session import AUTH_COOKIE
from paskia.globals import db
from paskia.util import hostutil, passphrase, tokens
from paskia.util.tokens import decode_session_key, session_key
app = FastAPI()
@@ -5,12 +5,12 @@ from uuid import UUID
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from ..authsession import create_session, get_reset, get_session
from ..globals import db, passkey
from ..util import passphrase
from ..util.tokens import create_token, session_key
from . import authz
from .session import AUTH_COOKIE, infodict
from paskia.authsession import create_session, get_reset, get_session
from paskia.fastapi import authz
from paskia.fastapi.session import AUTH_COOKIE, infodict
from paskia.globals import db, passkey
from paskia.util import passphrase
from paskia.util.tokens import create_token, session_key
# WebSocket error handling decorator
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin</title>
<script type="module" crossorigin src="/auth/assets/admin-D8zxJOk4.js"></script>
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-CU0-cyzg.js">
<link rel="modulepreload" crossorigin href="/auth/assets/AccessDenied-guOGfNm-.js">
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
<link rel="stylesheet" crossorigin href="/auth/assets/AccessDenied-TAST_piX.css">
<link rel="stylesheet" crossorigin href="/auth/assets/admin-DIOoLLHy.css">
</head>
<body>
<div id="admin-app"></div>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{_ as z,G as E,r as g,c as y,o as L,d,e as h,i as f,f as n,t as _,n as N,R as O,C as Y,Y as w,V as T,p as D}from"./_plugin-vue_export-helper-R4vr2A9I.js";const q={class:"app-shell"},G={key:0,class:"global-status",style:{display:"block"}},J={class:"view-root"},W={key:0,class:"surface surface--tight"},j={class:"view-header center"},H={key:0,class:"user-line"},K={class:"view-lede"},Q={class:"section-block"},X={class:"section-body center"},Z={class:"button-row center"},ee=["disabled"],te=["disabled"],ae=["disabled"],se=["disabled"],ne={__name:"RestrictedAuth",props:{mode:{type:String,default:"login",validator:o=>["login","reauth","forbidden"].includes(o)}},emits:["authenticated","forbidden","logout","back","home","auth-error"],setup(o,{expose:V,emit:$}){const v=o,m=$,i=E({show:!1,message:"",type:"info"}),b=g(!0),t=g(!1),S=g(null),r=g(null),l=g("initial");let p=null;const u=y(()=>!!r.value?.authenticated),k=y(()=>b.value?!1:v.mode==="reauth"?!0:l.value!=="forbidden"),U=y(()=>v.mode==="reauth"?"🔐 Additional Authentication":l.value==="forbidden"?"🚫 Forbidden":`🔐 ${S.value?.rp_name||location.origin}`),B=y(()=>v.mode==="reauth"?"Please verify your identity to continue with this action.":l.value==="forbidden"?"You lack the required permissions.":"Please sign in with your passkey."),F=y(()=>r.value?.user?.user_name||"User");function c(e,a="info",s=3e3){i.show=!0,i.message=e,i.type=a,p&&clearTimeout(p),s>0&&(p=setTimeout(()=>{i.show=!1},s))}async function I(){try{const e=await Y();if(S.value=e,e?.rp_name){const a=v.mode==="reauth"?"Verify Identity":u.value?"Forbidden":"Sign In";document.title=`${e.rp_name} · ${a}`}}catch(e){console.warn("Unable to load settings",e)}}async function M(){try{r.value=await w("/auth/api/user-info",{method:"POST"}),u.value&&v.mode!=="reauth"?(l.value="forbidden",m("forbidden",r.value)):l.value="login"}catch(e){console.error("Failed to load user info",e),e.status!==401&&e.status!==403&&c(T(e),"error",4e3),r.value=null,l.value="login"}}async function A(){if(!k.value||t.value)return;t.value=!0,c("Starting authentication…","info");let e;try{e=await D.authenticate()}catch(a){t.value=!1;const s=a?.message||"Passkey authentication cancelled",P=s==="Passkey authentication cancelled";c(s,P?"info":"error",4e3),m("auth-error",{message:s,cancelled:P});return}try{await x(e)}catch(a){t.value=!1;const s=a?.message||"Failed to establish session";c(s,"error",4e3),m("auth-error",{message:s,cancelled:!1});return}t.value=!1,m("authenticated",e)}async function C(){if(!t.value){t.value=!0;try{await w("/auth/api/logout",{method:"POST"}),r.value=null,l.value="login",c("Logged out. You can sign in with a different account.","info",3e3)}catch(e){c(T(e),"error",4e3)}finally{t.value=!1}m("logout")}}function R(){const e=window.open("/auth/","passkey_auth_profile");e&&e.focus()}async function x(e){if(!e?.session_token)throw console.error("setSessionCookie called with missing session_token:",e),new Error("Authentication response missing session_token");return await w("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})}return L(async()=>{await I(),await M(),b.value=!1}),V({showMessage:c,isAuthenticated:u,userInfo:r}),(e,a)=>(h(),d("div",q,[i.show?(h(),d("div",G,[n("div",{class:N(["status",i.type])},_(i.message),3)])):f("",!0),n("main",J,[b.value?f("",!0):(h(),d("div",W,[n("header",j,[n("h1",null,_(U.value),1),u.value?(h(),d("p",H,"👤 "+_(F.value),1)):f("",!0),n("p",K,_(B.value),1)]),n("section",Q,[n("div",X,[n("div",Z,[O(e.$slots,"actions",{loading:t.value,canAuthenticate:k.value,isAuthenticated:u.value,authenticate:A,logout:C,mode:o.mode},()=>[n("button",{class:"btn-secondary",disabled:t.value,onClick:a[0]||(a[0]=s=>e.$emit("back"))},"Back",8,ee),k.value?(h(),d("button",{key:0,class:"btn-primary",disabled:t.value,onClick:A},_(t.value?o.mode==="reauth"?"Verifying…":"Signing in…":o.mode==="reauth"?"Verify":"Login"),9,te)):f("",!0),u.value&&o.mode!=="reauth"?(h(),d("button",{key:1,class:"btn-danger",disabled:t.value,onClick:C},"Logout",8,ae)):f("",!0),u.value&&o.mode!=="reauth"?(h(),d("button",{key:2,class:"btn-primary",disabled:t.value,onClick:R},"Profile",8,se)):f("",!0)])])])])]))])]))}},ie=z(ne,[["__scopeId","data-v-d00079a6"]]);export{ie as R};
@@ -0,0 +1 @@
.button-row.center[data-v-d00079a6]{display:flex;justify-content:center;gap:.75rem}.user-line[data-v-d00079a6]{margin:.5rem 0 0;font-weight:500;color:var(--color-text)}main.view-root[data-v-d00079a6]{min-height:100vh;align-items:center;justify-content:center;padding:2rem 1rem}.surface.surface--tight[data-v-d00079a6]{max-width:520px;margin:0 auto;width:100%;display:flex;flex-direction:column;gap:1.75rem}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.view-lede[data-v-0cc830bd]{margin:0;color:var(--color-text-muted);font-size:1rem}.section-header[data-v-0cc830bd]{display:flex;flex-direction:column;gap:.4rem}.section-description[data-v-0cc830bd]{margin:0;color:var(--color-text-muted)}.empty-state[data-v-0cc830bd]{margin:0;color:var(--color-text-muted);text-align:center;padding:1rem 0}.logout-button[data-v-0cc830bd]{align-self:flex-start}.logout-row[data-v-0cc830bd]{gap:1rem}.logout-row.single[data-v-0cc830bd]{justify-content:flex-start}.logout-note[data-v-0cc830bd]{margin:.75rem 0 0;color:var(--color-text-muted);font-size:.875rem}@media(max-width:720px){.logout-button[data-v-0cc830bd]{width:100%}}.host-view[data-v-88828278]{padding:3rem 1.5rem 4rem}.host-actions[data-v-88828278]{display:flex;flex-direction:column;gap:.75rem}.host-actions .button-row[data-v-88828278]{gap:.75rem;flex-wrap:wrap}.host-actions .button-row button[data-v-88828278]{flex:0 0 auto}.note[data-v-88828278],.empty-state[data-v-88828278]{margin:0;color:var(--color-text-muted)}@media(max-width:600px){.host-actions .button-row[data-v-88828278]{flex-direction:column}.host-actions .button-row button[data-v-88828278]{width:100%}}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{c as o,W as d,o as i,h as s,e as m,u as l,v as h}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{R as p}from"./RestrictedAuth-BIGLs28V.js";import{g as n}from"./helpers-CU0-cyzg.js";const f={__name:"RestrictedForward",setup(w){const a=o(()=>d()),r=o(()=>{const t=document.documentElement.getAttribute("data-mode");return t==="reauth"?"reauth":t==="forbidden"?"forbidden":"login"});function c(){location.reload()}function u(){const e=a.value||"/auth/";window.location.pathname!==e&&history.replaceState(null,"",e),window.location.href=e}return i(()=>{window.addEventListener("keydown",e=>{e.key==="Escape"&&n()})}),(e,t)=>(m(),s(p,{mode:r.value,onAuthenticated:c,onBack:l(n),onHome:u},null,8,["mode","onBack"]))}};h(f).mount("#app");
@@ -0,0 +1 @@
function f(r){if(!r)return"Never";const s=new Date(r),u=s-new Date,e=u>0,a=Math.abs(u),n=Math.round(a/(1e3*60)),o=Math.round(a/(1e3*60*60)),t=Math.round(a/(1e3*60*60*24));return a<1e3*60?"Now":n<=60?e?`In ${n} minute${n===1?"":"s"}`:n===1?"a minute ago":`${n} minutes ago`:o<=24?e?`In ${o} hour${o===1?"":"s"}`:o===1?"an hour ago":`${o} hours ago`:t<=14?e?`In ${t} day${t===1?"":"s"}`:t===1?"a day ago":`${t} days ago`:s.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})}const c=()=>history.back()||window.close();export{f,c as g};
@@ -0,0 +1 @@
.center[data-v-4f202f9a]{text-align:center}.button-row.center[data-v-4f202f9a]{display:flex;justify-content:center}.section-body[data-v-4f202f9a]{gap:1.25rem}.name-edit span[data-v-4f202f9a]{color:var(--color-text-muted);font-size:.9rem}
@@ -0,0 +1 @@
import{_ as M,G as F,r as i,c as v,W as b,o as U,d as c,e as u,i as V,f as t,t as g,n as $,z,A as E,S as I,C as N,q as R,X as D,V as K,p as O,v as j}from"./_plugin-vue_export-helper-R4vr2A9I.js";const q={class:"app-shell"},G={key:0,class:"global-status",style:{display:"block"}},H={class:"view-root"},J={class:"surface surface--tight",style:{"max-width":"560px",margin:"0 auto",width:"100%"}},L={class:"view-header",style:{"text-align":"center"}},W={class:"view-lede"},X={key:0,class:"section-block"},Y={key:1,class:"section-block"},Q={class:"section-body center"},Z={key:2,class:"section-block"},ee={class:"section-body"},se={class:"name-edit"},te=["disabled"],ae=["disabled"],ne={__name:"ResetApp",setup(ie){const o=F({show:!1,message:"",type:"info"}),d=i(!0),n=i(!1),r=i(""),x=i(null),p=i(null),f=i(""),m=i("");let h=null;const P=v(()=>p.value?.session_type||"your enrollment"),S=v(()=>d.value?"Preparing your secure enrollment…":y.value?`Finish up ${P.value}. You may edit the name below if needed, and it will be saved to your passkey.`:"This reset link is no longer valid.");v(()=>b());const y=v(()=>!!(r.value&&p.value));function l(e,s="info",a=3e3){o.show=!0,o.message=e,o.type=s,h&&clearTimeout(h),a>0&&(h=setTimeout(()=>{o.show=!1},a))}async function T(){try{const e=await N();x.value=e,e?.rp_name&&(document.title=`${e.rp_name} · Passkey Setup`)}catch(e){console.warn("Unable to load settings",e)}}async function C(){if(r.value)try{p.value=await R(`/auth/api/user-info?reset=${encodeURIComponent(r.value)}`,{method:"POST"}),f.value=p.value?.user?.user_name||""}catch(e){console.error("Failed to load user info",e);const s=e instanceof D?e.data?.detail||"Reset link is invalid or expired.":K(e);m.value=s,l(s,"error",0)}}async function _(){if(!y.value||n.value)return;n.value=!0,l("Starting passkey registration…","info");let e;try{const s=f.value.trim()||null;e=await O.register(r.value,s)}catch(s){n.value=!1;const a=s?.message||"Passkey registration cancelled",k=a==="Passkey registration cancelled";l(k?a:`Registration failed: ${a}`,k?"info":"error",4e3);return}try{await A(e)}catch(s){n.value=!1;const a=s?.message||"Failed to establish session";l(a,"error",4e3);return}l("Passkey registered successfully!","success",800),setTimeout(()=>{n.value=!1,w()},800)}async function A(e){if(!e?.session_token)throw new Error("Registration response missing session_token");return await R("/auth/api/set-session",{method:"POST",headers:{Authorization:`Bearer ${e.session_token}`}})}function w(){const e=b.value||"/auth/";window.location.pathname!==e&&history.replaceState(null,"",e),window.location.reload()}function B(){const e=window.location.pathname.split("/").filter(Boolean);if(!e.length)return"";const s=e[e.length-1],a=e.slice(0,-1);return a.length>1||a.length===1&&a[0]!=="auth"||!s.includes(".")?"":s}return U(async()=>{if(r.value=B(),await T(),!r.value){const e="Reset link is missing or malformed.";m.value=e,l(e,"error",0),d.value=!1;return}await C(),d.value=!1}),(e,s)=>(u(),c("div",q,[o.show?(u(),c("div",G,[t("div",{class:$(["status",o.type])},g(o.message),3)])):V("",!0),t("main",H,[t("div",J,[t("header",L,[s[1]||(s[1]=t("h1",null,"🔑 Registration",-1)),t("p",W,g(S.value),1)]),d.value?(u(),c("section",X,[...s[2]||(s[2]=[t("div",{class:"section-body center"},[t("p",null,"Loading reset details…")],-1)])])):y.value?(u(),c("section",Z,[t("div",ee,[t("label",se,[s[3]||(s[3]=t("span",null,"👤 Name",-1)),z(t("input",{type:"text","onUpdate:modelValue":s[0]||(s[0]=a=>f.value=a),disabled:n.value,maxlength:"64",onKeyup:I(_,["enter"])},null,40,te),[[E,f.value]])]),t("button",{class:"btn-primary",disabled:n.value,onClick:_},g(n.value?"Registering…":"Register Passkey"),9,ae)])])):(u(),c("section",Y,[t("div",Q,[t("p",null,g(m.value),1),t("div",{class:"button-row center",style:{"justify-content":"center"}},[t("button",{class:"btn-secondary",onClick:w},"Return to sign-in")])])]))])])]))}},oe=M(ne,[["__scopeId","data-v-4f202f9a"]]);j(oe).mount("#app");
@@ -0,0 +1 @@
import{c as r,o as c,h as i,e as d,v as u}from"./_plugin-vue_export-helper-R4vr2A9I.js";import{R as p}from"./RestrictedAuth-BIGLs28V.js";const h={__name:"RestrictedApi",setup(m){const a=r(()=>{const n=new URLSearchParams(window.location.hash.slice(1)).get("mode");return n==="reauth"?"reauth":n==="forbidden"?"forbidden":"login"});function t(e){window.parent&&window.parent!==window&&window.parent.postMessage(e,"*")}function s(e){t({type:"auth-success",authenticated:!0,sessionToken:e.session_token})}function o(){t({type:"auth-back"})}return c(()=>{t({type:"auth-ready"}),window.addEventListener("keydown",e=>{e.key==="Escape"&&o()})}),(e,n)=>(d(),i(p,{mode:a.value,onAuthenticated:s,onBack:o},null,8,["mode"]))}};u(h).mount("#app");
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Profile</title>
<script type="module" crossorigin src="/auth/assets/auth-a0yJ_sei.js"></script>
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-CU0-cyzg.js">
<link rel="modulepreload" crossorigin href="/auth/assets/AccessDenied-guOGfNm-.js">
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
<link rel="stylesheet" crossorigin href="/auth/assets/AccessDenied-TAST_piX.css">
<link rel="stylesheet" crossorigin href="/auth/assets/auth-CBojJKUK.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
@@ -0,0 +1,9 @@
<html style="background: transparent">
<script type="module" crossorigin src="/auth/assets/restricted-DVCvYFGN.js"></script>
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-BIGLs28V.js">
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CMHKrNJh.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<div id="app"></div>
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Restricted</title>
<script type="module" crossorigin src="/auth/assets/forward-BHNzlQhM.js"></script>
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-BIGLs28V.js">
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-CU0-cyzg.js">
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CMHKrNJh.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Complete Passkey Setup</title>
<script type="module" crossorigin src="/auth/assets/reset-YnZxhnI5.js"></script>
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-R4vr2A9I.js">
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-Bx2cFCEC.css">
<link rel="stylesheet" crossorigin href="/auth/assets/reset-DXzuKgh6.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+2 -2
View File
@@ -1,7 +1,7 @@
from typing import Generic, TypeVar
from .db import DatabaseInterface
from .sansio import Passkey
from paskia.db import DatabaseInterface
from paskia.sansio import Passkey
T = TypeVar("T")
+1 -1
View File
@@ -37,7 +37,7 @@ from webauthn.helpers.structs import (
UserVerificationRequirement,
)
from .db import Credential
from paskia.db import Credential
class Passkey:
@@ -14,7 +14,7 @@ DEV_SERVER = "http://localhost:4403"
def _resolve_static_dir() -> Path:
# Try packaged path via importlib.resources (works for wheel/installed).
try: # pragma: no cover - trivial path resolution
pkg_dir = resources.files("passkey") / "frontend-build"
pkg_dir = resources.files("paskia") / "frontend-build"
fs_path = Path(str(pkg_dir))
if fs_path.is_dir():
return fs_path
@@ -34,7 +34,7 @@ def file(*parts: str) -> Path:
def is_dev_mode() -> bool:
"""Check if we're running in dev mode (Vite frontend server)."""
return os.environ.get("PASSKEY_DEVMODE") == "1"
return os.environ.get("PASKIA_DEVMODE") == "1"
async def read(filepath: str) -> tuple[bytes, int, dict[str, str]]:
@@ -4,9 +4,9 @@ import os
from functools import lru_cache
from urllib.parse import urlparse, urlsplit
from ..globals import passkey as global_passkey
from paskia.globals import passkey as global_passkey
_AUTH_HOST_ENV = "PASSKEY_AUTH_HOST"
_AUTH_HOST_ENV = "PASKIA_AUTH_HOST"
def _default_origin_scheme() -> str:
@@ -1,6 +1,6 @@
import secrets
from .wordlist import words
from paskia.util.wordlist import words
N_WORDS = 5
@@ -3,9 +3,9 @@
from collections.abc import Sequence
from fnmatch import fnmatchcase
from ..globals import db
from .hostutil import normalize_host
from .tokens import session_key
from paskia.globals import db
from paskia.util.hostutil import normalize_host
from paskia.util.tokens import session_key
__all__ = ["has_any", "has_all", "session_context"]
@@ -2,8 +2,8 @@
from datetime import datetime, timezone
from ..db import SessionContext
from .timeutil import parse_duration
from paskia.db import SessionContext
from paskia.util.timeutil import parse_duration
def check_session_age(ctx: SessionContext, max_age: str | None) -> bool:
@@ -2,7 +2,7 @@ import base64
import hashlib
import secrets
from .passphrase import is_well_formed
from paskia.util.passphrase import is_well_formed
def create_token() -> str:
@@ -2,12 +2,10 @@
from datetime import timezone
from passkey.util import useragent
from .. import aaguid
from ..authsession import session_key
from ..globals import db
from . import hostutil, permutil, tokens
from paskia import aaguid
from paskia.authsession import session_key
from paskia.globals import db
from paskia.util import hostutil, permutil, tokens, useragent
def _format_datetime(dt):
-3
View File
@@ -1,3 +0,0 @@
from .sansio import Passkey
__all__ = ["Passkey"]
-3
View File
@@ -1,3 +0,0 @@
from .mainapp import app
__all__ = ["app"]
+5 -5
View File
@@ -3,7 +3,7 @@ requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "passkey"
name = "paskia"
dynamic = ["version"]
description = "Passkey Authentication for Web Services"
authors = [
@@ -26,7 +26,7 @@ requires-python = ">=3.10"
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "passkey/_version.py"
version-file = "paskia/_version.py"
[project.optional-dependencies]
dev = [
@@ -40,11 +40,11 @@ line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
ignore = ["E501"] # Line too long
isort.known-first-party = ["passkey"]
isort.known-first-party = ["paskia"]
[project.scripts]
passkey-auth = "passkey.fastapi.__main__:main"
paskia = "paskia.fastapi.__main__:main"
[tool.hatch.build]
artifacts = ["passkey/frontend-build"]
artifacts = ["paskia/frontend-build"]
targets.sdist.hooks.custom.path = "scripts/build-frontend.py"