paskia 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. paskia/_version.py +2 -2
  2. paskia/authsession.py +14 -27
  3. paskia/bootstrap.py +31 -103
  4. paskia/db/__init__.py +25 -51
  5. paskia/db/background.py +17 -37
  6. paskia/db/jsonl.py +168 -6
  7. paskia/db/migrations.py +34 -0
  8. paskia/db/operations.py +400 -723
  9. paskia/db/structs.py +214 -90
  10. paskia/fastapi/__main__.py +24 -28
  11. paskia/fastapi/admin.py +101 -160
  12. paskia/fastapi/api.py +47 -83
  13. paskia/fastapi/mainapp.py +13 -6
  14. paskia/fastapi/remote.py +16 -39
  15. paskia/fastapi/reset.py +27 -17
  16. paskia/fastapi/session.py +2 -2
  17. paskia/fastapi/user.py +21 -27
  18. paskia/fastapi/ws.py +27 -62
  19. paskia/fastapi/wschat.py +62 -0
  20. paskia/frontend-build/auth/admin/index.html +5 -5
  21. paskia/frontend-build/auth/assets/{AccessDenied-Bc249ASC.css → AccessDenied-DPkUS8LZ.css} +1 -1
  22. paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js +8 -0
  23. paskia/frontend-build/auth/assets/{RestrictedAuth-DgdJyscT.css → RestrictedAuth-CvR33_Z0.css} +1 -1
  24. paskia/frontend-build/auth/assets/RestrictedAuth-DsJXicIw.js +1 -0
  25. paskia/frontend-build/auth/assets/{_plugin-vue_export-helper-rKFEraYH.js → _plugin-vue_export-helper-nhjnO_bd.js} +1 -1
  26. paskia/frontend-build/auth/assets/admin-CPE1pLMm.js +1 -0
  27. paskia/frontend-build/auth/assets/{admin-BeNu48FR.css → admin-DzzjSg72.css} +1 -1
  28. paskia/frontend-build/auth/assets/{auth-BKX7shEe.css → auth-C7k64Wad.css} +1 -1
  29. paskia/frontend-build/auth/assets/auth-YIZvPlW_.js +1 -0
  30. paskia/frontend-build/auth/assets/{forward-Dzg-aE1C.js → forward-DmqVHZ7e.js} +1 -1
  31. paskia/frontend-build/auth/assets/reset-Chtv69AT.css +1 -0
  32. paskia/frontend-build/auth/assets/reset-s20PATTN.js +1 -0
  33. paskia/frontend-build/auth/assets/{restricted-C0IQufuH.js → restricted-D3AJx3_6.js} +1 -1
  34. paskia/frontend-build/auth/index.html +5 -5
  35. paskia/frontend-build/auth/restricted/index.html +4 -4
  36. paskia/frontend-build/int/forward/index.html +4 -4
  37. paskia/frontend-build/int/reset/index.html +3 -3
  38. paskia/globals.py +2 -2
  39. paskia/migrate/__init__.py +62 -55
  40. paskia/migrate/sql.py +72 -22
  41. paskia/remoteauth.py +1 -2
  42. paskia/sansio.py +6 -12
  43. {paskia-0.8.1.dist-info → paskia-0.9.0.dist-info}/METADATA +1 -1
  44. paskia-0.9.0.dist-info/RECORD +57 -0
  45. paskia/frontend-build/auth/assets/AccessDenied-aTdCvz9k.js +0 -8
  46. paskia/frontend-build/auth/assets/RestrictedAuth-BLMK7-nL.js +0 -1
  47. paskia/frontend-build/auth/assets/admin-tVs8oyLv.js +0 -1
  48. paskia/frontend-build/auth/assets/auth-Dk3q4pNS.js +0 -1
  49. paskia/frontend-build/auth/assets/reset-BWF4cWKR.css +0 -1
  50. paskia/frontend-build/auth/assets/reset-C_Td1_jn.js +0 -1
  51. paskia-0.8.1.dist-info/RECORD +0 -55
  52. {paskia-0.8.1.dist-info → paskia-0.9.0.dist-info}/WHEEL +0 -0
  53. {paskia-0.8.1.dist-info → paskia-0.9.0.dist-info}/entry_points.txt +0 -0
@@ -3,12 +3,12 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <script type="module" crossorigin src="/auth/assets/restricted-C0IQufuH.js"></script>
7
- <link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-rKFEraYH.js">
6
+ <script type="module" crossorigin src="/auth/assets/restricted-D3AJx3_6.js"></script>
7
+ <link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js">
8
8
  <link rel="modulepreload" crossorigin href="/auth/assets/pow-2N9bxgAo.js">
9
- <link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-BLMK7-nL.js">
9
+ <link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-DsJXicIw.js">
10
10
  <link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
11
- <link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-DgdJyscT.css">
11
+ <link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CvR33_Z0.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="app"></div>
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Access Restricted</title>
7
- <script type="module" crossorigin src="/auth/assets/forward-Dzg-aE1C.js"></script>
8
- <link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-rKFEraYH.js">
7
+ <script type="module" crossorigin src="/auth/assets/forward-DmqVHZ7e.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js">
9
9
  <link rel="modulepreload" crossorigin href="/auth/assets/pow-2N9bxgAo.js">
10
- <link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-BLMK7-nL.js">
10
+ <link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-DsJXicIw.js">
11
11
  <link rel="modulepreload" crossorigin href="/auth/assets/helpers-DzjFIx78.js">
12
12
  <link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
13
- <link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-DgdJyscT.css">
13
+ <link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CvR33_Z0.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="app"></div>
@@ -4,10 +4,10 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Complete Passkey Setup</title>
7
- <script type="module" crossorigin src="/auth/assets/reset-C_Td1_jn.js"></script>
8
- <link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-rKFEraYH.js">
7
+ <script type="module" crossorigin src="/auth/assets/reset-s20PATTN.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js">
9
9
  <link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
10
- <link rel="stylesheet" crossorigin href="/auth/assets/reset-BWF4cWKR.css">
10
+ <link rel="stylesheet" crossorigin href="/auth/assets/reset-Chtv69AT.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="app"></div>
paskia/globals.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from typing import Generic, TypeVar
2
2
 
3
+ from paskia import db, remoteauth
4
+ from paskia.bootstrap import bootstrap_if_needed
3
5
  from paskia.sansio import Passkey
4
6
 
5
7
  T = TypeVar("T")
@@ -42,7 +44,6 @@ async def init(
42
44
  Set PASKIA_DB environment variable to specify the JSONL database file path.
43
45
  Default: paskia.jsonl
44
46
  """
45
- from . import db, remoteauth
46
47
 
47
48
  # Initialize passkey instance with provided parameters
48
49
  passkey.instance = Passkey(
@@ -59,7 +60,6 @@ async def init(
59
60
 
60
61
  if bootstrap:
61
62
  # Bootstrap system if needed
62
- from .bootstrap import bootstrap_if_needed
63
63
 
64
64
  await bootstrap_if_needed()
65
65
 
@@ -11,13 +11,28 @@ Or via the CLI entry point (if installed):
11
11
  paskia-migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl
12
12
  """
13
13
 
14
+ import argparse
14
15
  import asyncio
16
+ import re
15
17
  from datetime import datetime, timezone
16
18
  from uuid import UUID
17
19
 
18
20
  import base64url
21
+ import uuid7
22
+ from sqlalchemy import select
19
23
 
20
24
  from paskia.authsession import EXPIRES
25
+ from paskia.db.jsonl import JsonlStore
26
+ from paskia.db.structs import (
27
+ DB,
28
+ Credential,
29
+ Org,
30
+ Permission,
31
+ ResetToken,
32
+ Role,
33
+ Session,
34
+ User,
35
+ )
21
36
 
22
37
  from .sql import (
23
38
  DB as SQLDB,
@@ -47,30 +62,14 @@ async def migrate_from_sql(
47
62
  sql_db_path: SQLAlchemy connection string for the source SQL database
48
63
  json_db_path: Path for the destination JSONL file
49
64
  """
50
- # Import here to avoid circular imports and to not require JSON db at import time
51
- import re
52
-
53
- import uuid7
54
- from sqlalchemy import select
55
-
56
- from paskia.db.operations import DB as JSONDB
57
- from paskia.db.structs import (
58
- _CredentialData,
59
- _OrgData,
60
- _PermissionData,
61
- _ResetTokenData,
62
- _RoleData,
63
- _SessionData,
64
- _UserData,
65
- )
66
-
67
65
  # Initialize source SQL database
68
66
  sql_db = SQLDB(sql_db_path)
69
67
  await sql_db.init_db()
70
68
 
71
69
  # Initialize destination JSON database (fresh, don't load existing)
72
- json_db = JSONDB(json_db_path)
73
- # Don't call json_db.load() - we want a fresh database, not to load existing
70
+ db = DB()
71
+ store = JsonlStore(db, json_db_path)
72
+ db._store = store
74
73
 
75
74
  print(f"Migrating from {sql_db_path} to {json_db_path}...")
76
75
 
@@ -90,11 +89,13 @@ async def migrate_from_sql(
90
89
  # Migrate permissions with UUID keys and scope field
91
90
  # Always create exactly one common auth:org:admin permission for all org admin needs
92
91
  org_admin_perm_uuid: UUID = uuid7.create()
93
- json_db._data.permissions[org_admin_perm_uuid] = _PermissionData(
92
+ org_admin_perm = Permission(
94
93
  scope="auth:org:admin",
95
94
  display_name="Org Admin",
96
95
  orgs={},
97
96
  )
97
+ org_admin_perm.uuid = org_admin_perm_uuid
98
+ db.permissions[org_admin_perm_uuid] = org_admin_perm
98
99
 
99
100
  # Mapping from old permission ID to new permission UUID
100
101
  perm_id_to_uuid: dict[str, UUID] = {}
@@ -113,11 +114,13 @@ async def migrate_from_sql(
113
114
 
114
115
  # Regular permission - create with UUID key
115
116
  perm_uuid: UUID = uuid7.create()
116
- json_db._data.permissions[perm_uuid] = _PermissionData(
117
+ new_perm = Permission(
117
118
  scope=perm.id, # Old ID becomes the scope
118
119
  display_name=perm.display_name,
119
120
  orgs={},
120
121
  )
122
+ new_perm.uuid = perm_uuid
123
+ db.permissions[perm_uuid] = new_perm
121
124
  perm_id_to_uuid[perm.id] = perm_uuid
122
125
  print(
123
126
  f" Migrated {len(permissions)} permissions (with {len(org_admin_uuids)} org-specific admins consolidated to auth:org:admin)"
@@ -127,16 +130,16 @@ async def migrate_from_sql(
127
130
  orgs = await sql_db.list_organizations()
128
131
  for org in orgs:
129
132
  org_key: UUID = org.uuid
130
- json_db._data.orgs[org_key] = _OrgData(
131
- display_name=org.display_name,
132
- )
133
+ new_org = Org(display_name=org.display_name)
134
+ new_org.uuid = org_key
135
+ db.orgs[org_key] = new_org
133
136
  # Update permissions to allow this org to grant them (by UUID)
134
137
  for old_perm_id in org.permissions:
135
138
  perm_uuid = perm_id_to_uuid.get(old_perm_id)
136
- if perm_uuid and perm_uuid in json_db._data.permissions:
137
- json_db._data.permissions[perm_uuid].orgs[org_key] = True
139
+ if perm_uuid and perm_uuid in db.permissions:
140
+ db.permissions[perm_uuid].orgs[org_key] = True
138
141
  # Ensure every org can grant auth:org:admin
139
- json_db._data.permissions[org_admin_perm_uuid].orgs[org_key] = True
142
+ db.permissions[org_admin_perm_uuid].orgs[org_key] = True
140
143
  print(f" Migrated {len(orgs)} organizations")
141
144
 
142
145
  # Migrate roles - convert old permission IDs to UUIDs
@@ -150,11 +153,13 @@ async def migrate_from_sql(
150
153
  perm_uuid = perm_id_to_uuid.get(old_perm_id)
151
154
  if perm_uuid:
152
155
  new_permissions[perm_uuid] = True
153
- json_db._data.roles[role_key] = _RoleData(
156
+ new_role = Role(
154
157
  org=role.org_uuid,
155
158
  display_name=role.display_name,
156
159
  permissions=new_permissions,
157
160
  )
161
+ new_role.uuid = role_key
162
+ db.roles[role_key] = new_role
158
163
  role_count += 1
159
164
  print(f" Migrated {role_count} roles")
160
165
 
@@ -163,15 +168,17 @@ async def migrate_from_sql(
163
168
  result = await session.execute(select(UserModel))
164
169
  user_models = result.scalars().all()
165
170
  for um in user_models:
166
- user = um.as_dataclass()
167
- user_key: UUID = user.uuid
168
- json_db._data.users[user_key] = _UserData(
169
- display_name=user.display_name,
170
- role=user.role_uuid,
171
- created_at=user.created_at or datetime.now(timezone.utc),
172
- last_seen=user.last_seen,
173
- visits=user.visits,
171
+ legacy_user = um.as_dataclass()
172
+ user_key: UUID = legacy_user.uuid
173
+ new_user = User(
174
+ display_name=legacy_user.display_name,
175
+ role=legacy_user.role_uuid,
176
+ created_at=legacy_user.created_at or datetime.now(timezone.utc),
177
+ last_seen=legacy_user.last_seen,
178
+ visits=legacy_user.visits,
174
179
  )
180
+ new_user.uuid = user_key
181
+ db.users[user_key] = new_user
175
182
  print(f" Migrated {len(user_models)} users")
176
183
 
177
184
  # Migrate credentials
@@ -179,18 +186,20 @@ async def migrate_from_sql(
179
186
  result = await session.execute(select(CredentialModel))
180
187
  cred_models = result.scalars().all()
181
188
  for cm in cred_models:
182
- cred = cm.as_dataclass()
183
- cred_key: UUID = cred.uuid
184
- json_db._data.credentials[cred_key] = _CredentialData(
185
- credential_id=cred.credential_id,
186
- user=cred.user_uuid,
187
- aaguid=cred.aaguid,
188
- public_key=cred.public_key,
189
- sign_count=cred.sign_count,
190
- created_at=cred.created_at,
191
- last_used=cred.last_used,
192
- last_verified=cred.last_verified,
189
+ legacy_cred = cm.as_dataclass()
190
+ cred_key: UUID = legacy_cred.uuid
191
+ new_cred = Credential(
192
+ credential_id=legacy_cred.credential_id,
193
+ user=legacy_cred.user_uuid,
194
+ aaguid=legacy_cred.aaguid,
195
+ public_key=legacy_cred.public_key,
196
+ sign_count=legacy_cred.sign_count,
197
+ created_at=legacy_cred.created_at,
198
+ last_used=legacy_cred.last_used,
199
+ last_verified=legacy_cred.last_verified,
193
200
  )
201
+ new_cred.uuid = cred_key
202
+ db.credentials[cred_key] = new_cred
194
203
  print(f" Migrated {len(cred_models)} credentials")
195
204
 
196
205
  # Migrate sessions
@@ -207,7 +216,7 @@ async def migrate_from_sql(
207
216
  else:
208
217
  # Already in new format or unknown - try to use as-is
209
218
  session_key = base64url.enc(old_key[:12])
210
- json_db._data.sessions[session_key] = _SessionData(
219
+ db.sessions[session_key] = Session(
211
220
  user=sess.user_uuid,
212
221
  credential=sess.credential_uuid,
213
222
  host=sess.host,
@@ -231,26 +240,24 @@ async def migrate_from_sql(
231
240
  else:
232
241
  # Already in new format or unknown - truncate to 9 bytes
233
242
  token_key = old_key[:9]
234
- json_db._data.reset_tokens[token_key] = _ResetTokenData(
243
+ db.reset_tokens[token_key] = ResetToken(
235
244
  user=token.user_uuid,
236
245
  expiry=token.expiry,
237
246
  token_type=token.token_type,
238
247
  )
239
248
  print(f" Migrated {len(token_models)} reset tokens")
240
249
 
241
- # Queue and flush all changes with actor "migrate"
242
- json_db._current_actor = "migrate"
243
- json_db._queue_change()
244
- from paskia.db.jsonl import flush_changes
250
+ # Queue and flush all changes using the transaction mechanism
251
+ with db.transaction("migrate"):
252
+ pass # All data already added to _data, transaction commits on exit
245
253
 
246
- await flush_changes(json_db.db_path, json_db._pending_changes)
254
+ await store.flush()
247
255
 
248
256
  print("Migration complete!")
249
257
 
250
258
 
251
259
  def main():
252
260
  """CLI entry point for migration."""
253
- import argparse
254
261
 
255
262
  parser = argparse.ArgumentParser(
256
263
  description="Migrate Paskia database from SQL to JSON"
paskia/migrate/sql.py CHANGED
@@ -26,30 +26,55 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
26
26
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
27
27
 
28
28
  from paskia.db import (
29
- Credential,
30
29
  Org,
31
- ResetToken,
32
30
  Role,
33
- User,
34
31
  )
35
32
 
36
33
 
37
- # Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
34
+ # Legacy User class for SQL schema (uses 'role_uuid' not 'role')
38
35
  @dataclass
39
- class SqlPermission:
40
- """Permission as stored in the old SQL schema with id field."""
36
+ class _LegacyUser:
37
+ """User as stored in the old SQL schema with role_uuid field."""
41
38
 
42
- id: str
39
+ uuid: UUID
43
40
  display_name: str
41
+ role_uuid: UUID
42
+ created_at: datetime | None = None
43
+ last_seen: datetime | None = None
44
+ visits: int = 0
44
45
 
45
46
 
46
- DB_PATH_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite"
47
+ # Legacy Credential class for SQL schema (uses 'user_uuid' not 'user')
48
+ @dataclass
49
+ class _LegacyCredential:
50
+ """Credential as stored in the old SQL schema with user_uuid field."""
51
+
52
+ uuid: UUID
53
+ credential_id: bytes
54
+ user_uuid: UUID
55
+ aaguid: UUID
56
+ public_key: bytes
57
+ sign_count: int
58
+ created_at: datetime
59
+ last_used: datetime | None = None
60
+ last_verified: datetime | None = None
47
61
 
48
62
 
49
- # Local Session class for SQL schema (uses 'renewed' not 'expiry')
63
+ # Legacy Role class for SQL schema (uses 'org_uuid' not 'org')
50
64
  @dataclass
51
- class _SqlSession:
52
- """Session as stored in the old SQL schema with renewed timestamp."""
65
+ class _LegacyRole:
66
+ """Role as stored in the old SQL schema with org_uuid field."""
67
+
68
+ uuid: UUID
69
+ org_uuid: UUID
70
+ display_name: str
71
+ permissions: list[str] | None = None
72
+
73
+
74
+ # Legacy Session class for SQL schema (uses 'key' as field, 'user_uuid', 'credential_uuid')
75
+ @dataclass
76
+ class _LegacySession:
77
+ """Session as stored in the old SQL schema."""
53
78
 
54
79
  key: bytes
55
80
  user_uuid: UUID
@@ -60,6 +85,29 @@ class _SqlSession:
60
85
  renewed: datetime
61
86
 
62
87
 
88
+ # Legacy ResetToken class for SQL schema (uses 'key' as field, 'user_uuid')
89
+ @dataclass
90
+ class _LegacyResetToken:
91
+ """ResetToken as stored in the old SQL schema."""
92
+
93
+ key: bytes
94
+ user_uuid: UUID
95
+ token_type: str
96
+ expiry: datetime
97
+
98
+
99
+ # Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
100
+ @dataclass
101
+ class SqlPermission:
102
+ """Permission as stored in the old SQL schema with id field."""
103
+
104
+ id: str
105
+ display_name: str
106
+
107
+
108
+ DB_PATH_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite"
109
+
110
+
63
111
  def _normalize_dt(value: datetime | None) -> datetime | None:
64
112
  if value is None:
65
113
  return None
@@ -80,7 +128,9 @@ class OrgModel(Base):
80
128
 
81
129
  def as_dataclass(self):
82
130
  # Base Org without permissions/roles (filled by data accessors)
83
- return Org(UUID(bytes=self.uuid), self.display_name)
131
+ org = Org(display_name=self.display_name)
132
+ org.uuid = UUID(bytes=self.uuid)
133
+ return org
84
134
 
85
135
  @staticmethod
86
136
  def from_dataclass(org: Org):
@@ -98,14 +148,14 @@ class RoleModel(Base):
98
148
 
99
149
  def as_dataclass(self):
100
150
  # Base Role without permissions (filled by data accessors)
101
- return Role(
151
+ return _LegacyRole(
102
152
  uuid=UUID(bytes=self.uuid),
103
153
  org_uuid=UUID(bytes=self.org_uuid),
104
154
  display_name=self.display_name,
105
155
  )
106
156
 
107
157
  @staticmethod
108
- def from_dataclass(role: Role):
158
+ def from_dataclass(role: _LegacyRole):
109
159
  return RoleModel(
110
160
  uuid=role.uuid.bytes,
111
161
  org_uuid=role.org_uuid.bytes,
@@ -129,8 +179,8 @@ class UserModel(Base):
129
179
  )
130
180
  visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
131
181
 
132
- def as_dataclass(self) -> User:
133
- return User(
182
+ def as_dataclass(self) -> "_LegacyUser":
183
+ return _LegacyUser(
134
184
  uuid=UUID(bytes=self.uuid),
135
185
  display_name=self.display_name,
136
186
  role_uuid=UUID(bytes=self.role_uuid),
@@ -140,7 +190,7 @@ class UserModel(Base):
140
190
  )
141
191
 
142
192
  @staticmethod
143
- def from_dataclass(user: User):
193
+ def from_dataclass(user: "_LegacyUser"):
144
194
  return UserModel(
145
195
  uuid=user.uuid.bytes,
146
196
  display_name=user.display_name,
@@ -175,7 +225,7 @@ class CredentialModel(Base):
175
225
  )
176
226
 
177
227
  def as_dataclass(self):
178
- return Credential(
228
+ return _LegacyCredential(
179
229
  uuid=UUID(bytes=self.uuid),
180
230
  credential_id=self.credential_id,
181
231
  user_uuid=UUID(bytes=self.user_uuid),
@@ -210,7 +260,7 @@ class SessionModel(Base):
210
260
  )
211
261
 
212
262
  def as_dataclass(self):
213
- return _SqlSession(
263
+ return _LegacySession(
214
264
  key=self.key,
215
265
  user_uuid=UUID(bytes=self.user_uuid),
216
266
  credential_uuid=UUID(bytes=self.credential_uuid),
@@ -221,7 +271,7 @@ class SessionModel(Base):
221
271
  )
222
272
 
223
273
  @staticmethod
224
- def from_dataclass(session: _SqlSession):
274
+ def from_dataclass(session: _LegacySession):
225
275
  return SessionModel(
226
276
  key=session.key,
227
277
  user_uuid=session.user_uuid.bytes,
@@ -243,8 +293,8 @@ class ResetTokenModel(Base):
243
293
  token_type: Mapped[str] = mapped_column(String, nullable=False)
244
294
  expiry: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
245
295
 
246
- def as_dataclass(self) -> ResetToken:
247
- return ResetToken(
296
+ def as_dataclass(self) -> _LegacyResetToken:
297
+ return _LegacyResetToken(
248
298
  key=self.key,
249
299
  user_uuid=UUID(bytes=self.user_uuid),
250
300
  token_type=self.token_type,
paskia/remoteauth.py CHANGED
@@ -24,7 +24,7 @@ from datetime import datetime, timedelta, timezone
24
24
  from typing import Callable
25
25
  from uuid import UUID
26
26
 
27
- from paskia.util import passphrase
27
+ from paskia.util import passphrase, pow
28
28
 
29
29
  # Remote auth requests expire after this duration
30
30
  REMOTE_AUTH_LIFETIME = timedelta(minutes=5)
@@ -319,7 +319,6 @@ class RemoteAuthManager:
319
319
  Returns:
320
320
  PoW work units (pow.NORMAL or pow.HARD)
321
321
  """
322
- from paskia.util import pow
323
322
 
324
323
  count = self.get_connection_count()
325
324
  return pow.HARD if count >= 10 else pow.NORMAL
paskia/sansio.py CHANGED
@@ -8,11 +8,9 @@ This module provides a unified interface for WebAuthn operations including:
8
8
  """
9
9
 
10
10
  import json
11
- from datetime import datetime, timezone
12
11
  from urllib.parse import urlparse
13
12
  from uuid import UUID
14
13
 
15
- import uuid7
16
14
  from webauthn import (
17
15
  generate_authentication_options,
18
16
  generate_registration_options,
@@ -176,14 +174,12 @@ class Passkey:
176
174
  expected_origin=origin,
177
175
  expected_rp_id=self.rp_id,
178
176
  )
179
- return Credential(
180
- uuid=uuid7.create(),
177
+ return Credential.create(
181
178
  credential_id=credential.raw_id,
182
- user_uuid=user_uuid,
179
+ user=user_uuid,
183
180
  aaguid=UUID(registration.aaguid),
184
181
  public_key=registration.credential_public_key,
185
182
  sign_count=registration.sign_count,
186
- created_at=datetime.now(timezone.utc),
187
183
  )
188
184
 
189
185
  ### Authentication Methods ###
@@ -234,8 +230,11 @@ class Passkey:
234
230
  Args:
235
231
  credential: The authentication credential response from the client
236
232
  expected_challenge: The earlier generated challenge bytes
237
- stored_cred: The server stored credential record (modified by this function)
233
+ stored_cred: The server stored credential record (NOT modified)
238
234
  origin: The origin URL (required, must be pre-validated)
235
+
236
+ Returns:
237
+ VerifiedAuthentication with new_sign_count and user_verified status
239
238
  """
240
239
  # Verify the authentication response
241
240
  verification = verify_authentication_response(
@@ -246,11 +245,6 @@ class Passkey:
246
245
  credential_public_key=stored_cred.public_key,
247
246
  credential_current_sign_count=stored_cred.sign_count,
248
247
  )
249
- stored_cred.sign_count = verification.new_sign_count
250
- now = datetime.now(timezone.utc)
251
- stored_cred.last_used = now
252
- if verification.user_verified:
253
- stored_cred.last_verified = now
254
248
  return verification
255
249
 
256
250
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paskia
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Passkey Auth made easy: all sites and APIs can be guarded even without any changes on the protected site.
5
5
  Project-URL: Homepage, https://git.zi.fi/LeoVasanko/paskia
6
6
  Project-URL: Repository, https://github.com/LeoVasanko/paskia
@@ -0,0 +1,57 @@
1
+ paskia/__init__.py,sha256=6eopO87IOFA2zfOuqt8Jj8Tdtp93HBMOgUBtTzMRweM,57
2
+ paskia/_version.py,sha256=TvxBYkx8Rz_Q1S3JFp831BRT8Wo0Yxt6TJMtgZKenTo,704
3
+ paskia/authsession.py,sha256=WtHC0iurDfNbjCxbKzZXZL-hklYorLFLsXM2w9JaUu0,1741
4
+ paskia/bootstrap.py,sha256=FShAMvLmLxQ4HfE9yz1ZB6EO9fC-lQroesU4dC58VO8,3875
5
+ paskia/config.py,sha256=BdGzQ3Ja1enSTHmkDkBDGQk_JluT3VaK3Y7AqB5xMlk,723
6
+ paskia/globals.py,sha256=ip03kLoS_27cNIgXTVcXNoeQDjTAC_IILuXaHKShTws,1712
7
+ paskia/remoteauth.py,sha256=0lDH6mQg7YGxTdj4OHSJFJzAahtK8M8nrCK6k0iLk20,12450
8
+ paskia/sansio.py,sha256=LQRdV1kW_aGwDWC8fhyEvqWPwKZVx_8qzQv65et6utg,9727
9
+ paskia/aaguid/__init__.py,sha256=Moy67wiJSYulL_whgEEBBKabKEOvlR-NRLyXprDdBO0,1007
10
+ paskia/aaguid/combined_aaguid.json,sha256=CaZ96AiwdAjBnyVZnJ1eolAHxUQMB2H6mDgZkorYg_A,4124722
11
+ paskia/db/__init__.py,sha256=5ZUTqpwY939DvR-4ejodwXep8dPtml8k2Z_2ecPJoTM,3274
12
+ paskia/db/background.py,sha256=6vBLFh0HPIZQy9fSbSX5C3MmnkkxyOaJ3V4MKPz0aaQ,3569
13
+ paskia/db/jsonl.py,sha256=9fX31DmQL3YiTIcwwOaao6Z6grGZGrMyw-U-M5apXNQ,10240
14
+ paskia/db/migrations.py,sha256=fwdVbIf2ybXra8qO2th3G26ocpUCftA4lP4s6zRhjoY,969
15
+ paskia/db/operations.py,sha256=mva4hMsi8080AisjgXXuo5PGJ55zWPzmcl4v_3CLseM,29826
16
+ paskia/db/structs.py,sha256=y6X2W0t64qvzDnin6HRoiMFHV-KhovaPe4fPnV1tF_g,7838
17
+ paskia/fastapi/__init__.py,sha256=NFsTX1qytoyZKiur7RDTa2fxiOWHrop5CAAx8rqK9E0,58
18
+ paskia/fastapi/__main__.py,sha256=qxApOdjY51pQquDsmoMNkXWEBTr2edQo-p2AKB8NbWc,7701
19
+ paskia/fastapi/admin.py,sha256=51Gm3Mf_laHpXJNfR6d6-zPy0L__zQ7L95XJ-TDwS40,34960
20
+ paskia/fastapi/api.py,sha256=zUM7x3w05STphaVAAmd7e02-D8_ETtz-zR4vyiFZBiY,9377
21
+ paskia/fastapi/auth_host.py,sha256=Y5w9Mz6jyq0hj7SX8LfwebaesUOLGcWzGW9lsmw5WOo,3242
22
+ paskia/fastapi/authz.py,sha256=6s2TGkb3C7qWTXHOaMj613NiqMfztqM5QENuSb2IjO8,3529
23
+ paskia/fastapi/mainapp.py,sha256=SgPuXbgh82S0zlCfdlxmZajicwE4w7yh67PsSw0pdFI,4622
24
+ paskia/fastapi/remote.py,sha256=bbE9U90wo7cpfXxMlBFm7s-eR0ALq7ImiKKUXeynA1s,18835
25
+ paskia/fastapi/reset.py,sha256=k5UEVc3yWKwc3tHk7IuKAAdmSsbX3Zq9J6twXpaU9so,3609
26
+ paskia/fastapi/session.py,sha256=BRnlgR8pTY7o0f7qFnkdyepS2fKEAgqwT9Hj951sZJM,1479
27
+ paskia/fastapi/user.py,sha256=2mB4OU6BxeO9KQBvd1usgzSvTW5rsY37UdPp_OcGjr4,4620
28
+ paskia/fastapi/ws.py,sha256=p0rHGbvglWw7S2ti89iTy_FlEScxjYqL1oJxkoWcPDY,4479
29
+ paskia/fastapi/wschat.py,sha256=eUtVTGyxVIG2DOZIpjeYGgJRBpU5LnWfTT66vIsKaOA,1818
30
+ paskia/fastapi/wsutil.py,sha256=CJZOyy4e9jazgyvj6CQ_kXVlGtpHNwfjGOdmHL02Nec,2620
31
+ paskia/frontend-build/auth/index.html,sha256=PhNRnHQSaQCIyiSl8fZqidC-PIGMe_KWLJokw0N8628,936
32
+ paskia/frontend-build/auth/admin/index.html,sha256=jiCkfwVKDRT-nmz1OEwWzZBw8tNMXGnNjIfaIBXJ4fE,862
33
+ paskia/frontend-build/auth/assets/AccessDenied-DPkUS8LZ.css,sha256=s7BvCBUV8YZQA1riEuXFvBGcm8dRr6QNZ1VJMBWoKPA,7941
34
+ paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js,sha256=6ZgY9gSDDLtQUq0k4N5YWlpqpGyQW9r_X9ocWgSHuVM,51702
35
+ paskia/frontend-build/auth/assets/RestrictedAuth-CvR33_Z0.css,sha256=jOnmoy1hon9QwzoY9tHmhd-0zUye9v9-bv5zPku82GY,5397
36
+ paskia/frontend-build/auth/assets/RestrictedAuth-DsJXicIw.js,sha256=YctOv9_npORcB8kdudZs2iEf2yKhWJkLPTZTkvu-qB8,9761
37
+ paskia/frontend-build/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css,sha256=GpCu32ZoXHmL_8wVVa0Yja8dtKpaJSRyfitYAURx4Yc,12796
38
+ paskia/frontend-build/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js,sha256=6UQZRGDUU0m_cRZd_QKtt-4C1-3H2MtocoRR89is2AI,84746
39
+ paskia/frontend-build/auth/assets/admin-CPE1pLMm.js,sha256=EgB9vPz_8Zey0klCLnAaQgPANnLrAXWnar64oZFZiqM,41005
40
+ paskia/frontend-build/auth/assets/admin-DzzjSg72.css,sha256=X8K-ycZwZ3gXNdB8YFMxdGpRD8uS_Qeq0_awx9eJ-VY,7599
41
+ paskia/frontend-build/auth/assets/auth-C7k64Wad.css,sha256=x1CKaX0MmuunxZvew4ArCusqTYNrNjLv4JW2bnvZhDg,4333
42
+ paskia/frontend-build/auth/assets/auth-YIZvPlW_.js,sha256=pSJnxtX9ALwKNAu0rL7LAfr0zZIxy_Y--Qn5UllyMng,25209
43
+ paskia/frontend-build/auth/assets/forward-DmqVHZ7e.js,sha256=rfiUg6N9ez9WuZA-pWf0CViV7j4-UoWqH3riFDr81xE,782
44
+ paskia/frontend-build/auth/assets/helpers-DzjFIx78.js,sha256=w_IsCBn3QwidsuwQhVRycd8Fa53lvbgRGGojTBXVlUc,940
45
+ paskia/frontend-build/auth/assets/pow-2N9bxgAo.js,sha256=7AfzW5lcTefPI6YGXrYao1b56L7v5Bon9Y9N40yHsaE,9447
46
+ paskia/frontend-build/auth/assets/reset-Chtv69AT.css,sha256=1iFB1F8va7Ktdn7YxAMyNU8keFcaalmo25mCCM5QQys,238
47
+ paskia/frontend-build/auth/assets/reset-s20PATTN.js,sha256=Fo4u2Kz58spVVJ2BmV3Q_HPG8x5jysL5_0KDYQbVRac,3976
48
+ paskia/frontend-build/auth/assets/restricted-D3AJx3_6.js,sha256=H7L5F3nW6btinOZPgi8U_yLkxBO93L_rTeDMUWx7-Ow,1023
49
+ paskia/frontend-build/auth/restricted/index.html,sha256=6RPwe3edRPjM2-TLp87S9C170NM0gGYufboL_sp7q-k,785
50
+ paskia/frontend-build/int/forward/index.html,sha256=iA5lgiEgLxszNEdvTi7jMawKkhSTWydU8idRs0LLKlw,870
51
+ paskia/frontend-build/int/reset/index.html,sha256=E6ylRX8-OupR3eBUPSzFRH1_rXVft93esAY0zaoVFBQ,612
52
+ paskia/migrate/__init__.py,sha256=CuJxt4w7ZvnL4JJ6FxiBKXTSRvCx-rPxKe9CbMGagD4,9896
53
+ paskia/migrate/sql.py,sha256=MK6KfmvoD3U65IN6EV2au3mhdLdicFIrV83mpQq8Bp4,14096
54
+ paskia-0.9.0.dist-info/METADATA,sha256=fGtkP1RbaVeNZ2B-mcaX7hCgZn9Nc0QS1gfPg5av4l8,4261
55
+ paskia-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
56
+ paskia-0.9.0.dist-info/entry_points.txt,sha256=vvx6RYetgd61I2ODqQPHqrKHgCfuo08w_T35yDlHenE,93
57
+ paskia-0.9.0.dist-info/RECORD,,