@passkeykit/server 3.0.0 → 3.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/dist/esm/express-routes.js +56 -30
- package/dist/esm/stores.js +34 -26
- package/dist/express-routes.d.ts +1 -1
- package/dist/express-routes.js +56 -30
- package/dist/stores.js +33 -25
- package/package.json +5 -4
|
@@ -3,13 +3,50 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @ai_context This is a convenience wrapper. Apps that don't use Express
|
|
5
5
|
* can use PasskeyServer directly. These routes implement the standard
|
|
6
|
-
* challenge-response pattern with proper error handling.
|
|
6
|
+
* challenge-response pattern with proper error handling and Zod validation.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* const routes = createExpressRoutes(passkeyServer, { getUserInfo });
|
|
10
10
|
* app.use('/api/auth/passkey', routes);
|
|
11
11
|
*/
|
|
12
12
|
import { Router } from 'express';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Zod Schemas — strict input validation for every route
|
|
16
|
+
// ============================================================
|
|
17
|
+
const registerOptionsSchema = z.object({
|
|
18
|
+
userId: z.string().min(1),
|
|
19
|
+
authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(),
|
|
20
|
+
residentKey: z.enum(['required', 'preferred', 'discouraged']).optional(),
|
|
21
|
+
userVerification: z.enum(['required', 'preferred', 'discouraged']).optional(),
|
|
22
|
+
}).strict();
|
|
23
|
+
const registerVerifySchema = z.object({
|
|
24
|
+
userId: z.string().min(1),
|
|
25
|
+
response: z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
rawId: z.string(),
|
|
28
|
+
type: z.literal('public-key'),
|
|
29
|
+
response: z.record(z.string(), z.unknown()),
|
|
30
|
+
clientExtensionResults: z.record(z.string(), z.unknown()),
|
|
31
|
+
authenticatorAttachment: z.string().optional(),
|
|
32
|
+
}).passthrough(),
|
|
33
|
+
credentialName: z.string().optional(),
|
|
34
|
+
challengeToken: z.string().optional(),
|
|
35
|
+
}).strict();
|
|
36
|
+
const authenticateOptionsSchema = z.object({
|
|
37
|
+
userId: z.string().min(1).optional(),
|
|
38
|
+
userVerification: z.enum(['required', 'preferred', 'discouraged']).optional(),
|
|
39
|
+
}).strict();
|
|
40
|
+
const authenticateVerifySchema = z.object({
|
|
41
|
+
sessionKey: z.string().min(1),
|
|
42
|
+
response: z.object({
|
|
43
|
+
id: z.string(),
|
|
44
|
+
rawId: z.string(),
|
|
45
|
+
type: z.literal('public-key'),
|
|
46
|
+
response: z.record(z.string(), z.unknown()),
|
|
47
|
+
clientExtensionResults: z.record(z.string(), z.unknown()),
|
|
48
|
+
}).passthrough(),
|
|
49
|
+
}).strict();
|
|
13
50
|
/**
|
|
14
51
|
* Create Express router with passkey registration and authentication routes.
|
|
15
52
|
*
|
|
@@ -21,18 +58,14 @@ import { Router } from 'express';
|
|
|
21
58
|
*/
|
|
22
59
|
export function createExpressRoutes(server, config) {
|
|
23
60
|
const router = Router();
|
|
24
|
-
/**
|
|
25
|
-
* POST /register/options
|
|
26
|
-
* Body: { userId: string, authenticatorAttachment?: 'platform' | 'cross-platform' }
|
|
27
|
-
* Response includes `challengeToken` in stateless mode.
|
|
28
|
-
*/
|
|
29
61
|
router.post('/register/options', async (req, res) => {
|
|
30
62
|
try {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
33
|
-
res.status(400).json({ error: '
|
|
63
|
+
const parsed = registerOptionsSchema.safeParse(req.body);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
34
66
|
return;
|
|
35
67
|
}
|
|
68
|
+
const { userId, authenticatorAttachment, residentKey, userVerification } = parsed.data;
|
|
36
69
|
const user = await config.getUserInfo(userId);
|
|
37
70
|
if (!user) {
|
|
38
71
|
res.status(404).json({ error: 'User not found' });
|
|
@@ -50,18 +83,14 @@ export function createExpressRoutes(server, config) {
|
|
|
50
83
|
res.status(500).json({ error: 'Failed to generate registration options' });
|
|
51
84
|
}
|
|
52
85
|
});
|
|
53
|
-
/**
|
|
54
|
-
* POST /register/verify
|
|
55
|
-
* Body: { userId: string, response: RegistrationResponseJSON, credentialName?: string, challengeToken?: string }
|
|
56
|
-
* `challengeToken` is required in stateless mode.
|
|
57
|
-
*/
|
|
58
86
|
router.post('/register/verify', async (req, res) => {
|
|
59
87
|
try {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
res.status(400).json({ error: '
|
|
88
|
+
const parsed = registerVerifySchema.safeParse(req.body);
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
63
91
|
return;
|
|
64
92
|
}
|
|
93
|
+
const { userId, response, credentialName, challengeToken } = parsed.data;
|
|
65
94
|
const result = await server.verifyRegistration(userId, response, credentialName, challengeToken);
|
|
66
95
|
if (config.onRegistrationSuccess) {
|
|
67
96
|
await config.onRegistrationSuccess(userId, result.credential.credentialId);
|
|
@@ -78,14 +107,14 @@ export function createExpressRoutes(server, config) {
|
|
|
78
107
|
res.status(400).json({ error: message });
|
|
79
108
|
}
|
|
80
109
|
});
|
|
81
|
-
/**
|
|
82
|
-
* POST /authenticate/options
|
|
83
|
-
* Body: { userId?: string }
|
|
84
|
-
* Response includes `sessionKey` (which IS the challengeToken in stateless mode).
|
|
85
|
-
*/
|
|
86
110
|
router.post('/authenticate/options', async (req, res) => {
|
|
87
111
|
try {
|
|
88
|
-
const
|
|
112
|
+
const parsed = authenticateOptionsSchema.safeParse(req.body);
|
|
113
|
+
if (!parsed.success) {
|
|
114
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { userId, userVerification } = parsed.data;
|
|
89
118
|
const { options, sessionKey, challengeToken } = await server.generateAuthenticationOptions(userId, { userVerification });
|
|
90
119
|
res.json({ options, sessionKey, challengeToken });
|
|
91
120
|
}
|
|
@@ -94,17 +123,14 @@ export function createExpressRoutes(server, config) {
|
|
|
94
123
|
res.status(500).json({ error: 'Failed to generate authentication options' });
|
|
95
124
|
}
|
|
96
125
|
});
|
|
97
|
-
/**
|
|
98
|
-
* POST /authenticate/verify
|
|
99
|
-
* Body: { sessionKey: string, response: AuthenticationResponseJSON }
|
|
100
|
-
*/
|
|
101
126
|
router.post('/authenticate/verify', async (req, res) => {
|
|
102
127
|
try {
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
105
|
-
res.status(400).json({ error: '
|
|
128
|
+
const parsed = authenticateVerifySchema.safeParse(req.body);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
106
131
|
return;
|
|
107
132
|
}
|
|
133
|
+
const { sessionKey, response } = parsed.data;
|
|
108
134
|
const result = await server.verifyAuthentication(sessionKey, response);
|
|
109
135
|
let extra = {};
|
|
110
136
|
if (config.onAuthenticationSuccess) {
|
package/dist/esm/stores.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* For production with multiple server instances, implement the ChallengeStore
|
|
5
5
|
* and CredentialStore interfaces with a shared backend (Redis, database, etc).
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
8
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
8
9
|
import { dirname } from 'path';
|
|
9
10
|
// ============================================================
|
|
10
11
|
// In-Memory Stores (good for development and single-process)
|
|
@@ -63,35 +64,39 @@ export class FileChallengeStore {
|
|
|
63
64
|
if (!existsSync(dir))
|
|
64
65
|
mkdirSync(dir, { recursive: true });
|
|
65
66
|
}
|
|
66
|
-
load() {
|
|
67
|
+
async load() {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
const raw = await readFile(this.filePath, 'utf-8');
|
|
70
|
+
return JSON.parse(raw);
|
|
69
71
|
}
|
|
70
|
-
catch {
|
|
71
|
-
|
|
72
|
+
catch (err) {
|
|
73
|
+
// File not yet created — valid initial state
|
|
74
|
+
if (err?.code === 'ENOENT')
|
|
75
|
+
return {};
|
|
76
|
+
// Anything else (permission denied, corrupted JSON) must surface
|
|
77
|
+
throw err;
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
|
-
persist(data) {
|
|
75
|
-
// Clean expired
|
|
80
|
+
async persist(data) {
|
|
76
81
|
const now = Date.now();
|
|
77
82
|
for (const [key, val] of Object.entries(data)) {
|
|
78
83
|
if (now > val.expiresAt)
|
|
79
84
|
delete data[key];
|
|
80
85
|
}
|
|
81
|
-
|
|
86
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2));
|
|
82
87
|
}
|
|
83
88
|
async save(key, challenge) {
|
|
84
|
-
const data = this.load();
|
|
89
|
+
const data = await this.load();
|
|
85
90
|
data[key] = challenge;
|
|
86
|
-
this.persist(data);
|
|
91
|
+
await this.persist(data);
|
|
87
92
|
}
|
|
88
93
|
async consume(key) {
|
|
89
|
-
const data = this.load();
|
|
94
|
+
const data = await this.load();
|
|
90
95
|
const challenge = data[key];
|
|
91
96
|
if (!challenge)
|
|
92
97
|
return null;
|
|
93
98
|
delete data[key];
|
|
94
|
-
this.persist(data);
|
|
99
|
+
await this.persist(data);
|
|
95
100
|
if (Date.now() > challenge.expiresAt)
|
|
96
101
|
return null;
|
|
97
102
|
return challenge;
|
|
@@ -108,38 +113,41 @@ export class FileCredentialStore {
|
|
|
108
113
|
if (!existsSync(dir))
|
|
109
114
|
mkdirSync(dir, { recursive: true });
|
|
110
115
|
}
|
|
111
|
-
load() {
|
|
116
|
+
async load() {
|
|
112
117
|
try {
|
|
113
|
-
|
|
118
|
+
const raw = await readFile(this.filePath, 'utf-8');
|
|
119
|
+
return JSON.parse(raw);
|
|
114
120
|
}
|
|
115
|
-
catch {
|
|
116
|
-
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (err?.code === 'ENOENT')
|
|
123
|
+
return [];
|
|
124
|
+
throw err;
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
|
-
persist(data) {
|
|
120
|
-
|
|
127
|
+
async persist(data) {
|
|
128
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2));
|
|
121
129
|
}
|
|
122
130
|
async save(credential) {
|
|
123
|
-
const data = this.load();
|
|
131
|
+
const data = await this.load();
|
|
124
132
|
data.push(credential);
|
|
125
|
-
this.persist(data);
|
|
133
|
+
await this.persist(data);
|
|
126
134
|
}
|
|
127
135
|
async getByUserId(userId) {
|
|
128
|
-
return this.load().filter(c => c.userId === userId);
|
|
136
|
+
return (await this.load()).filter(c => c.userId === userId);
|
|
129
137
|
}
|
|
130
138
|
async getByCredentialId(credentialId) {
|
|
131
|
-
return this.load().find(c => c.credentialId === credentialId) ?? null;
|
|
139
|
+
return (await this.load()).find(c => c.credentialId === credentialId) ?? null;
|
|
132
140
|
}
|
|
133
141
|
async updateCounter(credentialId, newCounter) {
|
|
134
|
-
const data = this.load();
|
|
142
|
+
const data = await this.load();
|
|
135
143
|
const cred = data.find(c => c.credentialId === credentialId);
|
|
136
144
|
if (cred) {
|
|
137
145
|
cred.counter = newCounter;
|
|
138
|
-
this.persist(data);
|
|
146
|
+
await this.persist(data);
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
async delete(credentialId) {
|
|
142
|
-
const data = this.load().filter(c => c.credentialId !== credentialId);
|
|
143
|
-
this.persist(data);
|
|
150
|
+
const data = (await this.load()).filter(c => c.credentialId !== credentialId);
|
|
151
|
+
await this.persist(data);
|
|
144
152
|
}
|
|
145
153
|
}
|
package/dist/express-routes.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @ai_context This is a convenience wrapper. Apps that don't use Express
|
|
5
5
|
* can use PasskeyServer directly. These routes implement the standard
|
|
6
|
-
* challenge-response pattern with proper error handling.
|
|
6
|
+
* challenge-response pattern with proper error handling and Zod validation.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* const routes = createExpressRoutes(passkeyServer, { getUserInfo });
|
package/dist/express-routes.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @ai_context This is a convenience wrapper. Apps that don't use Express
|
|
6
6
|
* can use PasskeyServer directly. These routes implement the standard
|
|
7
|
-
* challenge-response pattern with proper error handling.
|
|
7
|
+
* challenge-response pattern with proper error handling and Zod validation.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* const routes = createExpressRoutes(passkeyServer, { getUserInfo });
|
|
@@ -13,6 +13,43 @@
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.createExpressRoutes = createExpressRoutes;
|
|
15
15
|
const express_1 = require("express");
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
// ============================================================
|
|
18
|
+
// Zod Schemas — strict input validation for every route
|
|
19
|
+
// ============================================================
|
|
20
|
+
const registerOptionsSchema = zod_1.z.object({
|
|
21
|
+
userId: zod_1.z.string().min(1),
|
|
22
|
+
authenticatorAttachment: zod_1.z.enum(['platform', 'cross-platform']).optional(),
|
|
23
|
+
residentKey: zod_1.z.enum(['required', 'preferred', 'discouraged']).optional(),
|
|
24
|
+
userVerification: zod_1.z.enum(['required', 'preferred', 'discouraged']).optional(),
|
|
25
|
+
}).strict();
|
|
26
|
+
const registerVerifySchema = zod_1.z.object({
|
|
27
|
+
userId: zod_1.z.string().min(1),
|
|
28
|
+
response: zod_1.z.object({
|
|
29
|
+
id: zod_1.z.string(),
|
|
30
|
+
rawId: zod_1.z.string(),
|
|
31
|
+
type: zod_1.z.literal('public-key'),
|
|
32
|
+
response: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
33
|
+
clientExtensionResults: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
34
|
+
authenticatorAttachment: zod_1.z.string().optional(),
|
|
35
|
+
}).passthrough(),
|
|
36
|
+
credentialName: zod_1.z.string().optional(),
|
|
37
|
+
challengeToken: zod_1.z.string().optional(),
|
|
38
|
+
}).strict();
|
|
39
|
+
const authenticateOptionsSchema = zod_1.z.object({
|
|
40
|
+
userId: zod_1.z.string().min(1).optional(),
|
|
41
|
+
userVerification: zod_1.z.enum(['required', 'preferred', 'discouraged']).optional(),
|
|
42
|
+
}).strict();
|
|
43
|
+
const authenticateVerifySchema = zod_1.z.object({
|
|
44
|
+
sessionKey: zod_1.z.string().min(1),
|
|
45
|
+
response: zod_1.z.object({
|
|
46
|
+
id: zod_1.z.string(),
|
|
47
|
+
rawId: zod_1.z.string(),
|
|
48
|
+
type: zod_1.z.literal('public-key'),
|
|
49
|
+
response: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
50
|
+
clientExtensionResults: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
51
|
+
}).passthrough(),
|
|
52
|
+
}).strict();
|
|
16
53
|
/**
|
|
17
54
|
* Create Express router with passkey registration and authentication routes.
|
|
18
55
|
*
|
|
@@ -24,18 +61,14 @@ const express_1 = require("express");
|
|
|
24
61
|
*/
|
|
25
62
|
function createExpressRoutes(server, config) {
|
|
26
63
|
const router = (0, express_1.Router)();
|
|
27
|
-
/**
|
|
28
|
-
* POST /register/options
|
|
29
|
-
* Body: { userId: string, authenticatorAttachment?: 'platform' | 'cross-platform' }
|
|
30
|
-
* Response includes `challengeToken` in stateless mode.
|
|
31
|
-
*/
|
|
32
64
|
router.post('/register/options', async (req, res) => {
|
|
33
65
|
try {
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
res.status(400).json({ error: '
|
|
66
|
+
const parsed = registerOptionsSchema.safeParse(req.body);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
37
69
|
return;
|
|
38
70
|
}
|
|
71
|
+
const { userId, authenticatorAttachment, residentKey, userVerification } = parsed.data;
|
|
39
72
|
const user = await config.getUserInfo(userId);
|
|
40
73
|
if (!user) {
|
|
41
74
|
res.status(404).json({ error: 'User not found' });
|
|
@@ -53,18 +86,14 @@ function createExpressRoutes(server, config) {
|
|
|
53
86
|
res.status(500).json({ error: 'Failed to generate registration options' });
|
|
54
87
|
}
|
|
55
88
|
});
|
|
56
|
-
/**
|
|
57
|
-
* POST /register/verify
|
|
58
|
-
* Body: { userId: string, response: RegistrationResponseJSON, credentialName?: string, challengeToken?: string }
|
|
59
|
-
* `challengeToken` is required in stateless mode.
|
|
60
|
-
*/
|
|
61
89
|
router.post('/register/verify', async (req, res) => {
|
|
62
90
|
try {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
res.status(400).json({ error: '
|
|
91
|
+
const parsed = registerVerifySchema.safeParse(req.body);
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
66
94
|
return;
|
|
67
95
|
}
|
|
96
|
+
const { userId, response, credentialName, challengeToken } = parsed.data;
|
|
68
97
|
const result = await server.verifyRegistration(userId, response, credentialName, challengeToken);
|
|
69
98
|
if (config.onRegistrationSuccess) {
|
|
70
99
|
await config.onRegistrationSuccess(userId, result.credential.credentialId);
|
|
@@ -81,14 +110,14 @@ function createExpressRoutes(server, config) {
|
|
|
81
110
|
res.status(400).json({ error: message });
|
|
82
111
|
}
|
|
83
112
|
});
|
|
84
|
-
/**
|
|
85
|
-
* POST /authenticate/options
|
|
86
|
-
* Body: { userId?: string }
|
|
87
|
-
* Response includes `sessionKey` (which IS the challengeToken in stateless mode).
|
|
88
|
-
*/
|
|
89
113
|
router.post('/authenticate/options', async (req, res) => {
|
|
90
114
|
try {
|
|
91
|
-
const
|
|
115
|
+
const parsed = authenticateOptionsSchema.safeParse(req.body);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { userId, userVerification } = parsed.data;
|
|
92
121
|
const { options, sessionKey, challengeToken } = await server.generateAuthenticationOptions(userId, { userVerification });
|
|
93
122
|
res.json({ options, sessionKey, challengeToken });
|
|
94
123
|
}
|
|
@@ -97,17 +126,14 @@ function createExpressRoutes(server, config) {
|
|
|
97
126
|
res.status(500).json({ error: 'Failed to generate authentication options' });
|
|
98
127
|
}
|
|
99
128
|
});
|
|
100
|
-
/**
|
|
101
|
-
* POST /authenticate/verify
|
|
102
|
-
* Body: { sessionKey: string, response: AuthenticationResponseJSON }
|
|
103
|
-
*/
|
|
104
129
|
router.post('/authenticate/verify', async (req, res) => {
|
|
105
130
|
try {
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
108
|
-
res.status(400).json({ error: '
|
|
131
|
+
const parsed = authenticateVerifySchema.safeParse(req.body);
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten().fieldErrors });
|
|
109
134
|
return;
|
|
110
135
|
}
|
|
136
|
+
const { sessionKey, response } = parsed.data;
|
|
111
137
|
const result = await server.verifyAuthentication(sessionKey, response);
|
|
112
138
|
let extra = {};
|
|
113
139
|
if (config.onAuthenticationSuccess) {
|
package/dist/stores.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.FileCredentialStore = exports.FileChallengeStore = exports.MemoryCredentialStore = exports.MemoryChallengeStore = void 0;
|
|
10
|
+
const promises_1 = require("fs/promises");
|
|
10
11
|
const fs_1 = require("fs");
|
|
11
12
|
const path_1 = require("path");
|
|
12
13
|
// ============================================================
|
|
@@ -68,35 +69,39 @@ class FileChallengeStore {
|
|
|
68
69
|
if (!(0, fs_1.existsSync)(dir))
|
|
69
70
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
70
71
|
}
|
|
71
|
-
load() {
|
|
72
|
+
async load() {
|
|
72
73
|
try {
|
|
73
|
-
|
|
74
|
+
const raw = await (0, promises_1.readFile)(this.filePath, 'utf-8');
|
|
75
|
+
return JSON.parse(raw);
|
|
74
76
|
}
|
|
75
|
-
catch {
|
|
76
|
-
|
|
77
|
+
catch (err) {
|
|
78
|
+
// File not yet created — valid initial state
|
|
79
|
+
if (err?.code === 'ENOENT')
|
|
80
|
+
return {};
|
|
81
|
+
// Anything else (permission denied, corrupted JSON) must surface
|
|
82
|
+
throw err;
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
|
-
persist(data) {
|
|
80
|
-
// Clean expired
|
|
85
|
+
async persist(data) {
|
|
81
86
|
const now = Date.now();
|
|
82
87
|
for (const [key, val] of Object.entries(data)) {
|
|
83
88
|
if (now > val.expiresAt)
|
|
84
89
|
delete data[key];
|
|
85
90
|
}
|
|
86
|
-
(0,
|
|
91
|
+
await (0, promises_1.writeFile)(this.filePath, JSON.stringify(data, null, 2));
|
|
87
92
|
}
|
|
88
93
|
async save(key, challenge) {
|
|
89
|
-
const data = this.load();
|
|
94
|
+
const data = await this.load();
|
|
90
95
|
data[key] = challenge;
|
|
91
|
-
this.persist(data);
|
|
96
|
+
await this.persist(data);
|
|
92
97
|
}
|
|
93
98
|
async consume(key) {
|
|
94
|
-
const data = this.load();
|
|
99
|
+
const data = await this.load();
|
|
95
100
|
const challenge = data[key];
|
|
96
101
|
if (!challenge)
|
|
97
102
|
return null;
|
|
98
103
|
delete data[key];
|
|
99
|
-
this.persist(data);
|
|
104
|
+
await this.persist(data);
|
|
100
105
|
if (Date.now() > challenge.expiresAt)
|
|
101
106
|
return null;
|
|
102
107
|
return challenge;
|
|
@@ -114,39 +119,42 @@ class FileCredentialStore {
|
|
|
114
119
|
if (!(0, fs_1.existsSync)(dir))
|
|
115
120
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
116
121
|
}
|
|
117
|
-
load() {
|
|
122
|
+
async load() {
|
|
118
123
|
try {
|
|
119
|
-
|
|
124
|
+
const raw = await (0, promises_1.readFile)(this.filePath, 'utf-8');
|
|
125
|
+
return JSON.parse(raw);
|
|
120
126
|
}
|
|
121
|
-
catch {
|
|
122
|
-
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err?.code === 'ENOENT')
|
|
129
|
+
return [];
|
|
130
|
+
throw err;
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
|
-
persist(data) {
|
|
126
|
-
(0,
|
|
133
|
+
async persist(data) {
|
|
134
|
+
await (0, promises_1.writeFile)(this.filePath, JSON.stringify(data, null, 2));
|
|
127
135
|
}
|
|
128
136
|
async save(credential) {
|
|
129
|
-
const data = this.load();
|
|
137
|
+
const data = await this.load();
|
|
130
138
|
data.push(credential);
|
|
131
|
-
this.persist(data);
|
|
139
|
+
await this.persist(data);
|
|
132
140
|
}
|
|
133
141
|
async getByUserId(userId) {
|
|
134
|
-
return this.load().filter(c => c.userId === userId);
|
|
142
|
+
return (await this.load()).filter(c => c.userId === userId);
|
|
135
143
|
}
|
|
136
144
|
async getByCredentialId(credentialId) {
|
|
137
|
-
return this.load().find(c => c.credentialId === credentialId) ?? null;
|
|
145
|
+
return (await this.load()).find(c => c.credentialId === credentialId) ?? null;
|
|
138
146
|
}
|
|
139
147
|
async updateCounter(credentialId, newCounter) {
|
|
140
|
-
const data = this.load();
|
|
148
|
+
const data = await this.load();
|
|
141
149
|
const cred = data.find(c => c.credentialId === credentialId);
|
|
142
150
|
if (cred) {
|
|
143
151
|
cred.counter = newCounter;
|
|
144
|
-
this.persist(data);
|
|
152
|
+
await this.persist(data);
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
async delete(credentialId) {
|
|
148
|
-
const data = this.load().filter(c => c.credentialId !== credentialId);
|
|
149
|
-
this.persist(data);
|
|
156
|
+
const data = (await this.load()).filter(c => c.credentialId !== credentialId);
|
|
157
|
+
await this.persist(data);
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
exports.FileCredentialStore = FileCredentialStore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passkeykit/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Server-side WebAuthn passkey verification — stateless or stateful, pure JS, works on serverless",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -54,12 +54,13 @@
|
|
|
54
54
|
],
|
|
55
55
|
"license": "MIT",
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@noble/hashes": "^1.7.0"
|
|
57
|
+
"@noble/hashes": "^1.7.0",
|
|
58
|
+
"zod": "^4.3.6"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
61
|
"@simplewebauthn/server": "^13.0.0",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
62
|
+
"argon2": "^0.41.0",
|
|
63
|
+
"express": "^4.0.0 || ^5.0.0"
|
|
63
64
|
},
|
|
64
65
|
"peerDependenciesMeta": {
|
|
65
66
|
"@simplewebauthn/server": {
|