passkey-magic 0.1.0
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.
- package/README.md +302 -0
- package/dist/adapters/memory.d.mts +10 -0
- package/dist/adapters/memory.mjs +142 -0
- package/dist/adapters/unstorage.d.mts +30 -0
- package/dist/adapters/unstorage.mjs +238 -0
- package/dist/better-auth/client.d.mts +9 -0
- package/dist/better-auth/client.mjs +7 -0
- package/dist/better-auth/index.d.mts +2 -0
- package/dist/better-auth/index.mjs +4044 -0
- package/dist/client/index.d.mts +167 -0
- package/dist/client/index.mjs +166 -0
- package/dist/crypto-KHRNe6EL.mjs +45 -0
- package/dist/index-BoHvgaqz.d.mts +2816 -0
- package/dist/index-Cqqpr_uS.d.mts +176 -0
- package/dist/nitro/index.d.mts +89 -0
- package/dist/nitro/index.mjs +117 -0
- package/dist/server/index.d.mts +3 -0
- package/dist/server/index.mjs +3 -0
- package/dist/server-BXkm8lU0.mjs +823 -0
- package/dist/types-BjM1f6uu.d.mts +249 -0
- package/package.json +74 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { AuthenticationResponseJSON, AuthenticatorTransportFuture, CredentialDeviceType, RegistrationResponseJSON } from "@simplewebauthn/server";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/** A registered user account. */
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
}
|
|
10
|
+
/** A WebAuthn credential (passkey) associated with a user. */
|
|
11
|
+
interface Credential {
|
|
12
|
+
id: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
publicKey: Uint8Array;
|
|
15
|
+
counter: number;
|
|
16
|
+
deviceType: CredentialDeviceType;
|
|
17
|
+
backedUp: boolean;
|
|
18
|
+
transports?: AuthenticatorTransportFuture[];
|
|
19
|
+
/** Human-readable label (e.g. "iPhone", "YubiKey"). */
|
|
20
|
+
label?: string;
|
|
21
|
+
createdAt: Date;
|
|
22
|
+
}
|
|
23
|
+
/** An authenticated session bound to a user. */
|
|
24
|
+
interface Session {
|
|
25
|
+
id: string;
|
|
26
|
+
token: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
expiresAt: Date;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
/** User-agent string from the request that created the session. */
|
|
31
|
+
userAgent?: string;
|
|
32
|
+
/** IP address from the request that created the session. */
|
|
33
|
+
ipAddress?: string;
|
|
34
|
+
}
|
|
35
|
+
/** A cross-device login session initiated by QR code scan. */
|
|
36
|
+
interface QRSession {
|
|
37
|
+
id: string;
|
|
38
|
+
state: 'pending' | 'scanned' | 'authenticated' | 'expired';
|
|
39
|
+
userId?: string;
|
|
40
|
+
sessionToken?: string;
|
|
41
|
+
expiresAt: Date;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
}
|
|
44
|
+
/** Discriminated union returned from all authentication flows. */
|
|
45
|
+
type AuthResult = {
|
|
46
|
+
method: 'passkey';
|
|
47
|
+
user: User;
|
|
48
|
+
session: Session;
|
|
49
|
+
} | {
|
|
50
|
+
method: 'magic-link';
|
|
51
|
+
user: User;
|
|
52
|
+
session: Session;
|
|
53
|
+
isNewUser: boolean;
|
|
54
|
+
} | {
|
|
55
|
+
method: 'qr';
|
|
56
|
+
user: User;
|
|
57
|
+
session: Session;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Persistence layer interface. Implement this to use any database.
|
|
61
|
+
*
|
|
62
|
+
* All methods must be safe to call concurrently. Implementations should
|
|
63
|
+
* handle their own serialization of `Date` and `Uint8Array` fields.
|
|
64
|
+
*/
|
|
65
|
+
interface StorageAdapter {
|
|
66
|
+
createUser(user: User): Promise<User>;
|
|
67
|
+
getUserById(id: string): Promise<User | null>;
|
|
68
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
69
|
+
updateUser(id: string, update: Partial<Pick<User, 'email'>>): Promise<User>;
|
|
70
|
+
deleteUser(id: string): Promise<void>;
|
|
71
|
+
createCredential(credential: Credential): Promise<Credential>;
|
|
72
|
+
getCredentialById(id: string): Promise<Credential | null>;
|
|
73
|
+
getCredentialsByUserId(userId: string): Promise<Credential[]>;
|
|
74
|
+
updateCredential(id: string, update: Partial<Pick<Credential, 'counter' | 'label'>>): Promise<void>;
|
|
75
|
+
deleteCredential(id: string): Promise<void>;
|
|
76
|
+
createSession(session: Session): Promise<Session>;
|
|
77
|
+
getSessionByToken(token: string): Promise<Session | null>;
|
|
78
|
+
getSessionsByUserId(userId: string): Promise<Session[]>;
|
|
79
|
+
deleteSession(id: string): Promise<void>;
|
|
80
|
+
deleteSessionsByUserId(userId: string): Promise<void>;
|
|
81
|
+
storeChallenge(key: string, challenge: string, ttlMs: number): Promise<void>;
|
|
82
|
+
getChallenge(key: string): Promise<string | null>;
|
|
83
|
+
deleteChallenge(key: string): Promise<void>;
|
|
84
|
+
storeMagicLink(token: string, email: string, ttlMs: number): Promise<void>;
|
|
85
|
+
getMagicLink(token: string): Promise<{
|
|
86
|
+
email: string;
|
|
87
|
+
} | null>;
|
|
88
|
+
deleteMagicLink(token: string): Promise<void>;
|
|
89
|
+
createQRSession(session: QRSession): Promise<QRSession>;
|
|
90
|
+
getQRSession(id: string): Promise<QRSession | null>;
|
|
91
|
+
updateQRSession(id: string, update: Partial<QRSession>): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Email delivery interface. Implement this to send magic link emails.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const resendAdapter: EmailAdapter = {
|
|
99
|
+
* async sendMagicLink(email, url, token) {
|
|
100
|
+
* await resend.emails.send({ to: email, subject: 'Login', html: `<a href="${url}">Login</a>` })
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
interface EmailAdapter {
|
|
106
|
+
sendMagicLink(email: string, url: string, token: string): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
/** Lifecycle hooks for intercepting auth flows. Return `false` from `before*` hooks to abort. */
|
|
109
|
+
interface AuthHooks {
|
|
110
|
+
beforeRegister?: (ctx: {
|
|
111
|
+
email?: string;
|
|
112
|
+
}) => Promise<void | false>;
|
|
113
|
+
afterRegister?: (ctx: {
|
|
114
|
+
user: User;
|
|
115
|
+
credential: Credential;
|
|
116
|
+
}) => Promise<void>;
|
|
117
|
+
beforeAuthenticate?: (ctx: {
|
|
118
|
+
credentialId?: string;
|
|
119
|
+
}) => Promise<void | false>;
|
|
120
|
+
afterAuthenticate?: (ctx: {
|
|
121
|
+
user: User;
|
|
122
|
+
session: Session;
|
|
123
|
+
}) => Promise<void>;
|
|
124
|
+
beforeMagicLink?: (ctx: {
|
|
125
|
+
email: string;
|
|
126
|
+
}) => Promise<void | false>;
|
|
127
|
+
afterMagicLink?: (ctx: {
|
|
128
|
+
user: User;
|
|
129
|
+
session: Session;
|
|
130
|
+
isNewUser: boolean;
|
|
131
|
+
}) => Promise<void>;
|
|
132
|
+
beforeQRComplete?: (ctx: {
|
|
133
|
+
sessionId: string;
|
|
134
|
+
userId: string;
|
|
135
|
+
}) => Promise<void | false>;
|
|
136
|
+
afterQRComplete?: (ctx: {
|
|
137
|
+
user: User;
|
|
138
|
+
session: Session;
|
|
139
|
+
}) => Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
/** Map of all auth events and their payload types. */
|
|
142
|
+
interface AuthEventMap {
|
|
143
|
+
'user:created': {
|
|
144
|
+
user: User;
|
|
145
|
+
};
|
|
146
|
+
'user:deleted': {
|
|
147
|
+
userId: string;
|
|
148
|
+
};
|
|
149
|
+
'session:created': {
|
|
150
|
+
session: Session;
|
|
151
|
+
user: User;
|
|
152
|
+
method: 'passkey' | 'magic-link' | 'qr';
|
|
153
|
+
};
|
|
154
|
+
'session:revoked': {
|
|
155
|
+
sessionId: string;
|
|
156
|
+
userId: string;
|
|
157
|
+
};
|
|
158
|
+
'credential:created': {
|
|
159
|
+
credential: Credential;
|
|
160
|
+
user: User;
|
|
161
|
+
};
|
|
162
|
+
'credential:updated': {
|
|
163
|
+
credentialId: string;
|
|
164
|
+
userId: string;
|
|
165
|
+
};
|
|
166
|
+
'credential:removed': {
|
|
167
|
+
credentialId: string;
|
|
168
|
+
userId: string;
|
|
169
|
+
};
|
|
170
|
+
'magic-link:sent': {
|
|
171
|
+
email: string;
|
|
172
|
+
};
|
|
173
|
+
'qr:scanned': {
|
|
174
|
+
sessionId: string;
|
|
175
|
+
};
|
|
176
|
+
'qr:completed': {
|
|
177
|
+
sessionId: string;
|
|
178
|
+
user: User;
|
|
179
|
+
};
|
|
180
|
+
'email:linked': {
|
|
181
|
+
userId: string;
|
|
182
|
+
email: string;
|
|
183
|
+
};
|
|
184
|
+
'email:unlinked': {
|
|
185
|
+
userId: string;
|
|
186
|
+
email: string;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Typed event handler for a specific auth event. */
|
|
190
|
+
type AuthEventHandler<K extends keyof AuthEventMap> = (event: AuthEventMap[K]) => void;
|
|
191
|
+
/** Configuration for `createAuth()`. */
|
|
192
|
+
interface AuthConfig<TEmail extends EmailAdapter | undefined = undefined> {
|
|
193
|
+
/** Relying party name shown during passkey prompts. */
|
|
194
|
+
rpName: string;
|
|
195
|
+
/** Relying party ID — typically the domain (e.g. "example.com"). */
|
|
196
|
+
rpID: string;
|
|
197
|
+
/** Expected origin(s) for WebAuthn verification. */
|
|
198
|
+
origin: string | string[];
|
|
199
|
+
/** Storage adapter for persistence. */
|
|
200
|
+
storage: StorageAdapter;
|
|
201
|
+
/** Email adapter — enables magic link auth when provided. */
|
|
202
|
+
email?: TEmail;
|
|
203
|
+
/** Base URL for magic link verification. Required if `email` is set. */
|
|
204
|
+
magicLinkURL?: TEmail extends EmailAdapter ? string : string | undefined;
|
|
205
|
+
/** Session TTL in ms. Default: 7 days. */
|
|
206
|
+
sessionTTL?: number;
|
|
207
|
+
/** WebAuthn challenge TTL in ms. Default: 60s. */
|
|
208
|
+
challengeTTL?: number;
|
|
209
|
+
/** Magic link token TTL in ms. Default: 15 minutes. */
|
|
210
|
+
magicLinkTTL?: number;
|
|
211
|
+
/** QR session TTL in ms. Default: 5 minutes. */
|
|
212
|
+
qrSessionTTL?: number;
|
|
213
|
+
/** Custom ID generator. Default: `crypto.randomUUID()`. */
|
|
214
|
+
generateId?: () => string;
|
|
215
|
+
/** Lifecycle hooks. */
|
|
216
|
+
hooks?: AuthHooks;
|
|
217
|
+
}
|
|
218
|
+
/** Methods only available when an `EmailAdapter` is configured. */
|
|
219
|
+
interface MagicLinkMethods {
|
|
220
|
+
/** Send a magic link email. Throws if email format is invalid. */
|
|
221
|
+
sendMagicLink(params: {
|
|
222
|
+
email: string;
|
|
223
|
+
}): Promise<{
|
|
224
|
+
sent: true;
|
|
225
|
+
}>;
|
|
226
|
+
/** Verify a magic link token and create a session. Single-use — token is consumed. */
|
|
227
|
+
verifyMagicLink(params: {
|
|
228
|
+
token: string;
|
|
229
|
+
}): Promise<AuthResult & {
|
|
230
|
+
method: 'magic-link';
|
|
231
|
+
}>;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Client configuration. Provide a `request` function that handles
|
|
235
|
+
* transport to your server (fetch, tRPC, WebSocket, etc.).
|
|
236
|
+
*/
|
|
237
|
+
interface ClientConfig {
|
|
238
|
+
request: <T = unknown>(endpoint: string, body?: unknown) => Promise<T>;
|
|
239
|
+
}
|
|
240
|
+
/** Status of a QR cross-device login session. */
|
|
241
|
+
interface QRSessionStatus {
|
|
242
|
+
state: QRSession['state'];
|
|
243
|
+
session?: {
|
|
244
|
+
token: string;
|
|
245
|
+
expiresAt: string;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
export { AuthResult as a, Credential as c, QRSessionStatus as d, RegistrationResponseJSON as f, User as h, AuthHooks as i, EmailAdapter as l, StorageAdapter as m, AuthEventHandler as n, AuthenticationResponseJSON as o, Session as p, AuthEventMap as r, ClientConfig as s, AuthConfig as t, MagicLinkMethods as u };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "passkey-magic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Passkey-first authentication with QR cross-device login and magic link fallback",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./server": {
|
|
8
|
+
"import": "./dist/server/index.mjs",
|
|
9
|
+
"types": "./dist/server/index.d.mts"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"import": "./dist/client/index.mjs",
|
|
13
|
+
"types": "./dist/client/index.d.mts"
|
|
14
|
+
},
|
|
15
|
+
"./adapters/memory": {
|
|
16
|
+
"import": "./dist/adapters/memory.mjs",
|
|
17
|
+
"types": "./dist/adapters/memory.d.mts"
|
|
18
|
+
},
|
|
19
|
+
"./adapters/unstorage": {
|
|
20
|
+
"import": "./dist/adapters/unstorage.mjs",
|
|
21
|
+
"types": "./dist/adapters/unstorage.d.mts"
|
|
22
|
+
},
|
|
23
|
+
"./nitro": {
|
|
24
|
+
"import": "./dist/nitro/index.mjs",
|
|
25
|
+
"types": "./dist/nitro/index.d.mts"
|
|
26
|
+
},
|
|
27
|
+
"./better-auth": {
|
|
28
|
+
"import": "./dist/better-auth/index.mjs",
|
|
29
|
+
"types": "./dist/better-auth/index.d.mts"
|
|
30
|
+
},
|
|
31
|
+
"./better-auth/client": {
|
|
32
|
+
"import": "./dist/better-auth/client.mjs",
|
|
33
|
+
"types": "./dist/better-auth/client.d.mts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"passkey",
|
|
46
|
+
"webauthn",
|
|
47
|
+
"auth",
|
|
48
|
+
"magic-link",
|
|
49
|
+
"qr",
|
|
50
|
+
"fido2"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
55
|
+
"@simplewebauthn/server": "^13.3.0",
|
|
56
|
+
"unstorage": "^1.17.4",
|
|
57
|
+
"uqr": "^0.1.2"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"better-auth": ">=1.0.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"better-auth": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "^25.5.0",
|
|
69
|
+
"better-auth": "^1.5.5",
|
|
70
|
+
"tsdown": "^0.21.4",
|
|
71
|
+
"typescript": "^5.9.3",
|
|
72
|
+
"vitest": "^4.1.0"
|
|
73
|
+
}
|
|
74
|
+
}
|