@mrgnw/anahtar 0.0.25 → 0.0.27
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { guessDeviceName } from '../device.js';
|
|
3
3
|
import { resolveMessages, detectLocaleClient, type AuthMessages } from '../i18n/index.js';
|
|
4
4
|
import PasskeyPrompt from './PasskeyPrompt.svelte';
|
|
5
|
-
import { onMount } from 'svelte';
|
|
5
|
+
import { onMount, type Snippet } from 'svelte';
|
|
6
6
|
import { slide } from 'svelte/transition';
|
|
7
7
|
|
|
8
8
|
interface PasskeyInfo {
|
|
@@ -23,6 +23,8 @@ interface Props {
|
|
|
23
23
|
onPasskeysChange?: () => void | Promise<void>;
|
|
24
24
|
getPasskeys?: () => Promise<PasskeyInfo[]>;
|
|
25
25
|
onStepChange?: (step: 'email' | 'otp' | 'authenticated') => void;
|
|
26
|
+
/** Extra inline icons rendered before the sign-out button when authenticated. */
|
|
27
|
+
actions?: Snippet;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
let {
|
|
@@ -36,6 +38,7 @@ let {
|
|
|
36
38
|
onPasskeysChange,
|
|
37
39
|
getPasskeys,
|
|
38
40
|
onStepChange,
|
|
41
|
+
actions,
|
|
39
42
|
}: Props = $props();
|
|
40
43
|
|
|
41
44
|
let expanded = $state(false);
|
|
@@ -360,6 +363,10 @@ async function removePasskey(id: string) {
|
|
|
360
363
|
</button>
|
|
361
364
|
<span class="anahtar-pill-sep">·</span>
|
|
362
365
|
{/if}
|
|
366
|
+
{#if actions}
|
|
367
|
+
{@render actions()}
|
|
368
|
+
<span class="anahtar-pill-sep">·</span>
|
|
369
|
+
{/if}
|
|
363
370
|
<button class="anahtar-pill-icon anahtar-pill-signout" onclick={handleSignOut} title="Sign out" disabled={loading}>
|
|
364
371
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
365
372
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AuthMessages } from '../i18n/index.js';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
2
3
|
interface PasskeyInfo {
|
|
3
4
|
id: string;
|
|
4
5
|
credentialId?: string;
|
|
@@ -18,6 +19,8 @@ interface Props {
|
|
|
18
19
|
onPasskeysChange?: () => void | Promise<void>;
|
|
19
20
|
getPasskeys?: () => Promise<PasskeyInfo[]>;
|
|
20
21
|
onStepChange?: (step: 'email' | 'otp' | 'authenticated') => void;
|
|
22
|
+
/** Extra inline icons rendered before the sign-out button when authenticated. */
|
|
23
|
+
actions?: Snippet;
|
|
21
24
|
}
|
|
22
25
|
declare const AuthPill: import("svelte").Component<Props, {}, "">;
|
|
23
26
|
type AuthPill = ReturnType<typeof AuthPill>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgnw/anahtar",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Opinionated, reusable auth for SvelteKit. Email+OTP + passkeys.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,10 +32,6 @@
|
|
|
32
32
|
"types": "./dist/db/d1.d.ts",
|
|
33
33
|
"default": "./dist/db/d1.js"
|
|
34
34
|
},
|
|
35
|
-
"./tinybase": {
|
|
36
|
-
"types": "./dist/db/tinybase.d.ts",
|
|
37
|
-
"default": "./dist/db/tinybase.js"
|
|
38
|
-
},
|
|
39
35
|
"./components": {
|
|
40
36
|
"types": "./dist/components/index.d.ts",
|
|
41
37
|
"svelte": "./dist/components/index.js",
|
|
@@ -49,8 +45,7 @@
|
|
|
49
45
|
"peerDependencies": {
|
|
50
46
|
"@simplewebauthn/browser": "^13.0.0",
|
|
51
47
|
"@sveltejs/kit": "^2.0.0",
|
|
52
|
-
"svelte": "^5.0.0"
|
|
53
|
-
"tinybase": "^8.0.0"
|
|
48
|
+
"svelte": "^5.0.0"
|
|
54
49
|
},
|
|
55
50
|
"peerDependenciesMeta": {
|
|
56
51
|
"svelte": {
|
|
@@ -58,9 +53,6 @@
|
|
|
58
53
|
},
|
|
59
54
|
"@simplewebauthn/browser": {
|
|
60
55
|
"optional": true
|
|
61
|
-
},
|
|
62
|
-
"tinybase": {
|
|
63
|
-
"optional": true
|
|
64
56
|
}
|
|
65
57
|
},
|
|
66
58
|
"dependencies": {
|
|
@@ -78,7 +70,6 @@
|
|
|
78
70
|
"@testing-library/user-event": "^14.6.1",
|
|
79
71
|
"@types/better-sqlite3": "^7.6.13",
|
|
80
72
|
"better-sqlite3": "^12.6.2",
|
|
81
|
-
"tinybase": "^8.0.1",
|
|
82
73
|
"happy-dom": "^20.6.1",
|
|
83
74
|
"svelte": "^5.38.1",
|
|
84
75
|
"svelte-check": "^4.3.1",
|
package/dist/db/tinybase.d.ts
DELETED
package/dist/db/tinybase.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { createIndexes } from 'tinybase/indexes';
|
|
2
|
-
function encodeBytes(bytes) {
|
|
3
|
-
return btoa(String.fromCharCode(...bytes));
|
|
4
|
-
}
|
|
5
|
-
function decodeBytes(str) {
|
|
6
|
-
return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
7
|
-
}
|
|
8
|
-
export function tinybaseAdapter(store, options = {}) {
|
|
9
|
-
const p = options.tablePrefix ?? 'auth_';
|
|
10
|
-
const t = {
|
|
11
|
-
users: `${p}users`,
|
|
12
|
-
sessions: `${p}sessions`,
|
|
13
|
-
otpCodes: `${p}otp_codes`,
|
|
14
|
-
passkeys: `${p}passkeys`,
|
|
15
|
-
challenges: `${p}challenges`
|
|
16
|
-
};
|
|
17
|
-
const indexes = createIndexes(store);
|
|
18
|
-
return {
|
|
19
|
-
init() {
|
|
20
|
-
indexes.setIndexDefinition('email_to_user', t.users, 'email');
|
|
21
|
-
indexes.setIndexDefinition('email_to_otp', t.otpCodes, 'email');
|
|
22
|
-
indexes.setIndexDefinition('credential_to_passkey', t.passkeys, 'credential_id');
|
|
23
|
-
indexes.setIndexDefinition('user_to_passkey', t.passkeys, 'user_id');
|
|
24
|
-
},
|
|
25
|
-
getUserByEmail(email) {
|
|
26
|
-
const ids = indexes.getSliceRowIds('email_to_user', email);
|
|
27
|
-
if (!ids.length)
|
|
28
|
-
return null;
|
|
29
|
-
const row = store.getRow(t.users, ids[0]);
|
|
30
|
-
return {
|
|
31
|
-
id: ids[0],
|
|
32
|
-
email: row.email,
|
|
33
|
-
skipPasskeyPrompt: row.skip_passkey_prompt === 1,
|
|
34
|
-
createdAt: row.created_at
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
createUser(email) {
|
|
38
|
-
const id = crypto.randomUUID();
|
|
39
|
-
const createdAt = Math.floor(Date.now() / 1000);
|
|
40
|
-
store.setRow(t.users, id, {
|
|
41
|
-
email,
|
|
42
|
-
skip_passkey_prompt: 0,
|
|
43
|
-
created_at: createdAt
|
|
44
|
-
});
|
|
45
|
-
return { id, email, skipPasskeyPrompt: false, createdAt };
|
|
46
|
-
},
|
|
47
|
-
setSkipPasskeyPrompt(userId, skip) {
|
|
48
|
-
store.setCell(t.users, userId, 'skip_passkey_prompt', skip ? 1 : 0);
|
|
49
|
-
},
|
|
50
|
-
createSession(tokenHash, userId, expiresAt) {
|
|
51
|
-
store.setRow(t.sessions, tokenHash, {
|
|
52
|
-
user_id: userId,
|
|
53
|
-
expires_at: expiresAt,
|
|
54
|
-
created_at: Math.floor(Date.now() / 1000)
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
getSession(tokenHash) {
|
|
58
|
-
const session = store.getRow(t.sessions, tokenHash);
|
|
59
|
-
if (!session.user_id)
|
|
60
|
-
return null;
|
|
61
|
-
const userId = session.user_id;
|
|
62
|
-
const user = store.getRow(t.users, userId);
|
|
63
|
-
if (!user.email)
|
|
64
|
-
return null;
|
|
65
|
-
return {
|
|
66
|
-
id: tokenHash,
|
|
67
|
-
userId,
|
|
68
|
-
expiresAt: session.expires_at,
|
|
69
|
-
email: user.email
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
deleteSession(tokenHash) {
|
|
73
|
-
store.delRow(t.sessions, tokenHash);
|
|
74
|
-
},
|
|
75
|
-
storeOTP(email, id, code, expiresAt) {
|
|
76
|
-
store.setRow(t.otpCodes, id, {
|
|
77
|
-
email,
|
|
78
|
-
code,
|
|
79
|
-
attempts: 0,
|
|
80
|
-
expires_at: expiresAt,
|
|
81
|
-
created_at: Date.now()
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
getLatestOTP(email) {
|
|
85
|
-
const ids = indexes.getSliceRowIds('email_to_otp', email);
|
|
86
|
-
if (!ids.length)
|
|
87
|
-
return null;
|
|
88
|
-
let latestId = ids[0];
|
|
89
|
-
let latestCreatedAt = store.getCell(t.otpCodes, latestId, 'created_at');
|
|
90
|
-
for (let i = 1; i < ids.length; i++) {
|
|
91
|
-
const createdAt = store.getCell(t.otpCodes, ids[i], 'created_at');
|
|
92
|
-
if (createdAt > latestCreatedAt) {
|
|
93
|
-
latestCreatedAt = createdAt;
|
|
94
|
-
latestId = ids[i];
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const row = store.getRow(t.otpCodes, latestId);
|
|
98
|
-
return {
|
|
99
|
-
id: latestId,
|
|
100
|
-
email: row.email,
|
|
101
|
-
code: row.code,
|
|
102
|
-
attempts: row.attempts,
|
|
103
|
-
expiresAt: row.expires_at
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
updateOTPAttempts(id, attempts) {
|
|
107
|
-
store.setCell(t.otpCodes, id, 'attempts', attempts);
|
|
108
|
-
},
|
|
109
|
-
deleteOTP(id) {
|
|
110
|
-
store.delRow(t.otpCodes, id);
|
|
111
|
-
},
|
|
112
|
-
deleteOTPsForEmail(email) {
|
|
113
|
-
const ids = indexes.getSliceRowIds('email_to_otp', email);
|
|
114
|
-
for (const id of ids) {
|
|
115
|
-
store.delRow(t.otpCodes, id);
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
storeChallenge(challenge, userId, expiresAt) {
|
|
119
|
-
// Clean up expired challenges
|
|
120
|
-
const now = Date.now();
|
|
121
|
-
store.forEachRow(t.challenges, (rowId) => {
|
|
122
|
-
const exp = store.getCell(t.challenges, rowId, 'expires_at');
|
|
123
|
-
if (exp < now)
|
|
124
|
-
store.delRow(t.challenges, rowId);
|
|
125
|
-
});
|
|
126
|
-
store.setRow(t.challenges, challenge, {
|
|
127
|
-
user_id: userId,
|
|
128
|
-
expires_at: expiresAt,
|
|
129
|
-
created_at: Math.floor(Date.now() / 1000)
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
consumeChallenge(challenge) {
|
|
133
|
-
const row = store.getRow(t.challenges, challenge);
|
|
134
|
-
if (!row.user_id)
|
|
135
|
-
return null;
|
|
136
|
-
store.delRow(t.challenges, challenge);
|
|
137
|
-
if (row.expires_at < Date.now())
|
|
138
|
-
return null;
|
|
139
|
-
return { userId: row.user_id };
|
|
140
|
-
},
|
|
141
|
-
getPasskeyByCredentialId(credentialId) {
|
|
142
|
-
const ids = indexes.getSliceRowIds('credential_to_passkey', credentialId);
|
|
143
|
-
if (!ids.length)
|
|
144
|
-
return null;
|
|
145
|
-
const id = ids[0];
|
|
146
|
-
const row = store.getRow(t.passkeys, id);
|
|
147
|
-
const userId = row.user_id;
|
|
148
|
-
const user = store.getRow(t.users, userId);
|
|
149
|
-
if (!user.email)
|
|
150
|
-
return null;
|
|
151
|
-
return {
|
|
152
|
-
id,
|
|
153
|
-
userId,
|
|
154
|
-
credentialId: row.credential_id,
|
|
155
|
-
publicKey: decodeBytes(row.public_key),
|
|
156
|
-
counter: row.counter,
|
|
157
|
-
transports: row.transports ?? null,
|
|
158
|
-
name: row.name ?? null,
|
|
159
|
-
createdAt: row.created_at,
|
|
160
|
-
email: user.email
|
|
161
|
-
};
|
|
162
|
-
},
|
|
163
|
-
getUserPasskeys(userId) {
|
|
164
|
-
const ids = indexes.getSliceRowIds('user_to_passkey', userId);
|
|
165
|
-
return ids.map((id) => {
|
|
166
|
-
const row = store.getRow(t.passkeys, id);
|
|
167
|
-
return {
|
|
168
|
-
id,
|
|
169
|
-
credentialId: row.credential_id,
|
|
170
|
-
publicKey: decodeBytes(row.public_key),
|
|
171
|
-
counter: row.counter,
|
|
172
|
-
transports: row.transports ?? null,
|
|
173
|
-
name: row.name ?? null,
|
|
174
|
-
createdAt: row.created_at
|
|
175
|
-
};
|
|
176
|
-
});
|
|
177
|
-
},
|
|
178
|
-
storePasskey(passkey) {
|
|
179
|
-
store.setRow(t.passkeys, passkey.id, {
|
|
180
|
-
user_id: passkey.userId,
|
|
181
|
-
credential_id: passkey.credentialId,
|
|
182
|
-
public_key: encodeBytes(passkey.publicKey),
|
|
183
|
-
counter: passkey.counter,
|
|
184
|
-
...(passkey.transports !== null && { transports: passkey.transports }),
|
|
185
|
-
...(passkey.name !== null && { name: passkey.name }),
|
|
186
|
-
created_at: Math.floor(Date.now() / 1000)
|
|
187
|
-
});
|
|
188
|
-
},
|
|
189
|
-
updatePasskeyCounter(id, counter) {
|
|
190
|
-
store.setCell(t.passkeys, id, 'counter', counter);
|
|
191
|
-
},
|
|
192
|
-
deletePasskey(id, userId) {
|
|
193
|
-
const row = store.getRow(t.passkeys, id);
|
|
194
|
-
if (!row.user_id || row.user_id !== userId)
|
|
195
|
-
return false;
|
|
196
|
-
store.delRow(t.passkeys, id);
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
}
|