@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.21
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.txt +81 -28
- package/dist/cjs/api-module.cjs +9 -0
- package/dist/cjs/api-module.d.ts +7 -4
- package/dist/cjs/api-server-base.cjs +607 -99
- package/dist/cjs/api-server-base.d.ts +80 -23
- package/dist/cjs/auth-api/auth-module.d.ts +23 -3
- package/dist/cjs/auth-api/auth-module.js +320 -124
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
- package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/mem-auth-store.js +14 -28
- package/dist/cjs/auth-api/module.d.ts +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/cjs/auth-api/sql-auth-store.js +43 -30
- package/dist/cjs/auth-api/storage.d.ts +6 -4
- package/dist/cjs/auth-api/storage.js +15 -5
- package/dist/cjs/auth-api/types.d.ts +7 -2
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-api/user-id.js +38 -0
- package/dist/cjs/auth-cookie-options.d.ts +11 -0
- package/dist/cjs/auth-cookie-options.js +66 -0
- package/dist/cjs/index.cjs +4 -14
- package/dist/cjs/index.d.ts +4 -9
- package/dist/cjs/oauth/memory.d.ts +6 -0
- package/dist/cjs/oauth/memory.js +44 -11
- package/dist/cjs/oauth/models.d.ts +7 -2
- package/dist/cjs/oauth/models.js +10 -21
- package/dist/cjs/oauth/sequelize.d.ts +10 -48
- package/dist/cjs/oauth/sequelize.js +44 -99
- package/dist/cjs/oauth/types.d.ts +1 -0
- package/dist/cjs/passkey/base.d.ts +2 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/config.js +26 -0
- package/dist/cjs/passkey/memory.d.ts +8 -0
- package/dist/cjs/passkey/memory.js +57 -16
- package/dist/cjs/passkey/models.d.ts +13 -4
- package/dist/cjs/passkey/models.js +41 -14
- package/dist/cjs/passkey/sequelize.d.ts +13 -25
- package/dist/cjs/passkey/sequelize.js +68 -153
- package/dist/cjs/passkey/service.d.ts +6 -2
- package/dist/cjs/passkey/service.js +205 -27
- package/dist/cjs/passkey/types.d.ts +18 -9
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/sequelize-utils.js +57 -0
- package/dist/cjs/token/base.d.ts +2 -1
- package/dist/cjs/token/base.js +3 -1
- package/dist/cjs/token/memory.d.ts +10 -0
- package/dist/cjs/token/memory.js +122 -32
- package/dist/cjs/token/sequelize.d.ts +4 -4
- package/dist/cjs/token/sequelize.js +67 -85
- package/dist/cjs/token/types.d.ts +8 -1
- package/dist/cjs/user/base.d.ts +1 -0
- package/dist/cjs/user/base.js +11 -4
- package/dist/cjs/user/memory.d.ts +2 -0
- package/dist/cjs/user/memory.js +9 -10
- package/dist/cjs/user/sequelize.d.ts +7 -2
- package/dist/cjs/user/sequelize.js +19 -32
- package/dist/esm/api-module.d.ts +7 -4
- package/dist/esm/api-module.js +9 -0
- package/dist/esm/api-server-base.d.ts +80 -23
- package/dist/esm/api-server-base.js +608 -100
- package/dist/esm/auth-api/auth-module.d.ts +23 -3
- package/dist/esm/auth-api/auth-module.js +321 -125
- package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/esm/auth-api/compat-auth-storage.js +13 -1
- package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/mem-auth-store.js +14 -28
- package/dist/esm/auth-api/module.d.ts +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/esm/auth-api/sql-auth-store.js +43 -30
- package/dist/esm/auth-api/storage.d.ts +6 -4
- package/dist/esm/auth-api/storage.js +13 -3
- package/dist/esm/auth-api/types.d.ts +7 -2
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +32 -0
- package/dist/esm/auth-cookie-options.d.ts +11 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/index.d.ts +4 -9
- package/dist/esm/index.js +2 -7
- package/dist/esm/oauth/memory.d.ts +6 -0
- package/dist/esm/oauth/memory.js +44 -11
- package/dist/esm/oauth/models.d.ts +7 -2
- package/dist/esm/oauth/models.js +6 -19
- package/dist/esm/oauth/sequelize.d.ts +10 -48
- package/dist/esm/oauth/sequelize.js +32 -87
- package/dist/esm/oauth/types.d.ts +1 -0
- package/dist/esm/passkey/base.d.ts +2 -0
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.d.ts +8 -0
- package/dist/esm/passkey/memory.js +57 -16
- package/dist/esm/passkey/models.d.ts +13 -4
- package/dist/esm/passkey/models.js +39 -12
- package/dist/esm/passkey/sequelize.d.ts +13 -25
- package/dist/esm/passkey/sequelize.js +69 -154
- package/dist/esm/passkey/service.d.ts +6 -2
- package/dist/esm/passkey/service.js +173 -28
- package/dist/esm/passkey/types.d.ts +18 -9
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +48 -0
- package/dist/esm/token/base.d.ts +2 -1
- package/dist/esm/token/base.js +3 -1
- package/dist/esm/token/memory.d.ts +10 -0
- package/dist/esm/token/memory.js +122 -32
- package/dist/esm/token/sequelize.d.ts +4 -4
- package/dist/esm/token/sequelize.js +67 -85
- package/dist/esm/token/types.d.ts +8 -1
- package/dist/esm/user/base.d.ts +1 -0
- package/dist/esm/user/base.js +11 -4
- package/dist/esm/user/memory.d.ts +2 -0
- package/dist/esm/user/memory.js +9 -10
- package/dist/esm/user/sequelize.d.ts +7 -2
- package/dist/esm/user/sequelize.js +19 -32
- package/docs/swagger/openapi.json +1876 -0
- package/package.json +84 -34
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
2
|
-
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
2
|
+
import { isoBase64URL, isoCBOR, decodeAttestationObject, parseAuthenticatorData } from '@simplewebauthn/server/helpers';
|
|
3
3
|
const ALLOWED_TRANSPORTS = [
|
|
4
4
|
'ble',
|
|
5
5
|
'cable',
|
|
@@ -18,6 +18,13 @@ function sanitizeTransports(input) {
|
|
|
18
18
|
.filter((value) => ALLOWED_TRANSPORTS.includes(value));
|
|
19
19
|
return filtered.length > 0 ? filtered : undefined;
|
|
20
20
|
}
|
|
21
|
+
function toOptionalString(value) {
|
|
22
|
+
if (typeof value !== 'string') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
27
|
+
}
|
|
21
28
|
function toBase64Url(buffer) {
|
|
22
29
|
return isoBase64URL.fromBuffer(new Uint8Array(buffer));
|
|
23
30
|
}
|
|
@@ -31,30 +38,112 @@ function toBuffer(value) {
|
|
|
31
38
|
const view = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
32
39
|
return Buffer.from(view);
|
|
33
40
|
}
|
|
41
|
+
function toBufferOrNull(value) {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (Buffer.isBuffer(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
49
|
+
return toBuffer(value);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'string') {
|
|
52
|
+
try {
|
|
53
|
+
return fromBase64Url(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
async function spkiToCosePublicKey(spki) {
|
|
62
|
+
try {
|
|
63
|
+
const subtle = (globalThis.crypto?.subtle ??
|
|
64
|
+
(await import('crypto')).webcrypto?.subtle);
|
|
65
|
+
if (!subtle) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const spkiView = new Uint8Array(spki);
|
|
69
|
+
const ecCurves = [
|
|
70
|
+
{ namedCurve: 'P-256', crv: 1, alg: -7, rawLength: 65 },
|
|
71
|
+
{ namedCurve: 'P-384', crv: 2, alg: -35, rawLength: 97 },
|
|
72
|
+
{ namedCurve: 'P-521', crv: 3, alg: -36, rawLength: 133 }
|
|
73
|
+
];
|
|
74
|
+
for (const curve of ecCurves) {
|
|
75
|
+
try {
|
|
76
|
+
const key = await subtle.importKey('spki', spkiView, { name: 'ECDSA', namedCurve: curve.namedCurve }, true, ['verify']);
|
|
77
|
+
const raw = Buffer.from(await subtle.exportKey('raw', key));
|
|
78
|
+
if (raw.length !== curve.rawLength || raw[0] !== 0x04) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const coordLength = (raw.length - 1) / 2;
|
|
82
|
+
const x = raw.slice(1, 1 + coordLength);
|
|
83
|
+
const y = raw.slice(1 + coordLength);
|
|
84
|
+
const coseMap = new Map([
|
|
85
|
+
[1, 2], // kty: EC2
|
|
86
|
+
[3, curve.alg],
|
|
87
|
+
[-1, curve.crv],
|
|
88
|
+
[-2, x],
|
|
89
|
+
[-3, y]
|
|
90
|
+
]);
|
|
91
|
+
return Buffer.from(isoCBOR.encode(coseMap));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// try next algorithm
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const edKey = await subtle.importKey('spki', spkiView, { name: 'Ed25519' }, true, ['verify']);
|
|
99
|
+
const raw = Buffer.from(await subtle.exportKey('raw', edKey));
|
|
100
|
+
if (raw.length !== 32) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const coseMap = new Map([
|
|
104
|
+
[1, 1], // kty: OKP
|
|
105
|
+
[3, -8], // alg: EdDSA
|
|
106
|
+
[-1, 6], // crv: Ed25519
|
|
107
|
+
[-2, raw]
|
|
108
|
+
]);
|
|
109
|
+
return Buffer.from(isoCBOR.encode(coseMap));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
34
119
|
export class PasskeyService {
|
|
35
120
|
constructor(config, adapter, logger = console) {
|
|
36
121
|
this.config = config;
|
|
37
122
|
this.adapter = adapter;
|
|
38
123
|
this.logger = logger;
|
|
39
124
|
}
|
|
125
|
+
async listUserCredentials(userId) {
|
|
126
|
+
return this.adapter.listUserCredentials(userId);
|
|
127
|
+
}
|
|
128
|
+
async deleteCredential(credentialId) {
|
|
129
|
+
return this.adapter.deleteCredential(credentialId);
|
|
130
|
+
}
|
|
40
131
|
async createChallenge(params) {
|
|
41
132
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
42
|
-
const metadata = {
|
|
43
|
-
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
44
|
-
fingerprint: typeof params.fingerprint === 'string' ? params.fingerprint : undefined,
|
|
45
|
-
label: typeof params.label === 'string' ? params.label : undefined,
|
|
46
|
-
userAgent: typeof params.userAgent === 'string' ? params.userAgent : undefined
|
|
47
|
-
};
|
|
48
133
|
if (params.action === 'register') {
|
|
49
|
-
return this.createRegistrationChallenge(params
|
|
134
|
+
return this.createRegistrationChallenge(params);
|
|
50
135
|
}
|
|
51
136
|
if (params.action === 'authenticate') {
|
|
52
|
-
return this.createAuthenticationChallenge(params
|
|
137
|
+
return this.createAuthenticationChallenge(params);
|
|
53
138
|
}
|
|
54
139
|
throw new Error(`Unsupported passkey action: ${String(params.action)}`);
|
|
55
140
|
}
|
|
56
141
|
async verifyResponse(params) {
|
|
57
142
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
143
|
+
const existing = this.adapter.getChallenge ? await this.adapter.getChallenge(params.expectedChallenge) : null;
|
|
144
|
+
if (existing && existing.expiresAt.getTime() <= Date.now()) {
|
|
145
|
+
return { verified: false };
|
|
146
|
+
}
|
|
58
147
|
const record = await this.adapter.consumeChallenge(params.expectedChallenge);
|
|
59
148
|
if (!record) {
|
|
60
149
|
return { verified: false };
|
|
@@ -75,7 +164,7 @@ export class PasskeyService {
|
|
|
75
164
|
}
|
|
76
165
|
return { verified: false };
|
|
77
166
|
}
|
|
78
|
-
async createRegistrationChallenge(params
|
|
167
|
+
async createRegistrationChallenge(params) {
|
|
79
168
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
80
169
|
const existing = await this.adapter.listUserCredentials(user.id);
|
|
81
170
|
const excludeCredentials = existing.map((credential) => {
|
|
@@ -98,16 +187,16 @@ export class PasskeyService {
|
|
|
98
187
|
action: 'register',
|
|
99
188
|
userId: user.id,
|
|
100
189
|
login: user.login,
|
|
101
|
-
expiresAt
|
|
102
|
-
metadata
|
|
190
|
+
expiresAt
|
|
103
191
|
});
|
|
104
192
|
return {
|
|
105
193
|
challenge: options.challenge,
|
|
106
194
|
expiresAt: expiresAt.toISOString(),
|
|
107
|
-
userId: user.id
|
|
195
|
+
userId: user.id,
|
|
196
|
+
publicKey: options
|
|
108
197
|
};
|
|
109
198
|
}
|
|
110
|
-
async createAuthenticationChallenge(params
|
|
199
|
+
async createAuthenticationChallenge(params) {
|
|
111
200
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
112
201
|
const credentials = await this.adapter.listUserCredentials(user.id);
|
|
113
202
|
const allowCredentials = credentials.map((credential) => {
|
|
@@ -127,13 +216,13 @@ export class PasskeyService {
|
|
|
127
216
|
action: 'authenticate',
|
|
128
217
|
userId: user.id,
|
|
129
218
|
login: user.login,
|
|
130
|
-
expiresAt
|
|
131
|
-
metadata
|
|
219
|
+
expiresAt
|
|
132
220
|
});
|
|
133
221
|
return {
|
|
134
222
|
challenge: options.challenge,
|
|
135
223
|
expiresAt: expiresAt.toISOString(),
|
|
136
|
-
userId: user.id
|
|
224
|
+
userId: user.id,
|
|
225
|
+
publicKey: options
|
|
137
226
|
};
|
|
138
227
|
}
|
|
139
228
|
async verifyRegistration(params, record) {
|
|
@@ -149,20 +238,71 @@ export class PasskeyService {
|
|
|
149
238
|
expectedChallenge: record.challenge,
|
|
150
239
|
expectedOrigin: this.config.origins,
|
|
151
240
|
expectedRPID: this.config.rpId,
|
|
152
|
-
requireUserVerification:
|
|
241
|
+
requireUserVerification: this.requireUserVerification()
|
|
153
242
|
});
|
|
154
243
|
if (!result.verified || !result.registrationInfo) {
|
|
244
|
+
if (!result.verified) {
|
|
245
|
+
const err = result.error ?? result;
|
|
246
|
+
this.logger.error?.('Passkey registration verification failed', err);
|
|
247
|
+
}
|
|
155
248
|
return { verified: false };
|
|
156
249
|
}
|
|
157
250
|
const registrationInfo = result.registrationInfo;
|
|
251
|
+
const attestationResponse = params.response.response;
|
|
252
|
+
const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
|
|
253
|
+
const credentialIdFallback = toBufferOrNull(params.response.id);
|
|
254
|
+
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0 ? credentialIdPrimary : credentialIdFallback;
|
|
255
|
+
const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
|
|
256
|
+
let publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
|
|
257
|
+
if ((!publicKeyPrimary || publicKeyPrimary.length === 0) && attestationResponse?.attestationObject) {
|
|
258
|
+
try {
|
|
259
|
+
const attestationObject = String(attestationResponse.attestationObject);
|
|
260
|
+
const attObj = decodeAttestationObject(isoBase64URL.toBuffer(attestationObject));
|
|
261
|
+
const parsedAuth = parseAuthenticatorData(attObj.get('authData'));
|
|
262
|
+
publicKeyFallback = toBufferOrNull(parsedAuth.credentialPublicKey) ?? publicKeyFallback;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.logger.warn?.('Passkey registration: failed to parse attestationObject', error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
|
|
269
|
+
if (this.config.debug && this.logger?.warn) {
|
|
270
|
+
const pkPrimaryHex = publicKeyPrimary ? publicKeyPrimary.slice(0, 4).toString('hex') : 'null';
|
|
271
|
+
const pkFallbackHex = publicKeyFallback ? publicKeyFallback.slice(0, 4).toString('hex') : 'null';
|
|
272
|
+
this.logger.warn(`Passkey registration: pkPrimary len=${publicKeyPrimary?.length ?? 0} head=${pkPrimaryHex}, pkFallback len=${publicKeyFallback?.length ?? 0} head=${pkFallbackHex}`);
|
|
273
|
+
}
|
|
274
|
+
if (!credentialId || credentialId.length === 0) {
|
|
275
|
+
return { verified: false, message: 'Missing credential id in registration response' };
|
|
276
|
+
}
|
|
277
|
+
if (!publicKey || publicKey.length === 0) {
|
|
278
|
+
return { verified: false, message: 'Missing public key in registration response' };
|
|
279
|
+
}
|
|
280
|
+
let storedPublicKey = Buffer.isBuffer(publicKey) ? publicKey : toBuffer(publicKey);
|
|
281
|
+
// If fallback is an SPKI key (DER, starts with 0x30) convert to COSE so verification succeeds.
|
|
282
|
+
if (storedPublicKey[0] === 0x30) {
|
|
283
|
+
const converted = await spkiToCosePublicKey(storedPublicKey);
|
|
284
|
+
if (converted) {
|
|
285
|
+
storedPublicKey = converted;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
this.logger.warn?.('Passkey registration: failed to convert SPKI public key to COSE');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
158
291
|
await this.adapter.saveCredential({
|
|
159
292
|
userId: user.id,
|
|
160
|
-
credentialId
|
|
161
|
-
publicKey:
|
|
162
|
-
counter: registrationInfo.counter,
|
|
293
|
+
credentialId,
|
|
294
|
+
publicKey: storedPublicKey,
|
|
295
|
+
counter: registrationInfo.counter ?? 0,
|
|
163
296
|
transports: sanitizeTransports(params.response.transports),
|
|
164
297
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
165
|
-
deviceType: registrationInfo.credentialDeviceType
|
|
298
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
299
|
+
label: toOptionalString(params.label),
|
|
300
|
+
createdDomain: toOptionalString(params.domain),
|
|
301
|
+
createdUserAgent: toOptionalString(params.userAgent),
|
|
302
|
+
createdBrowser: toOptionalString(params.browser),
|
|
303
|
+
createdOs: toOptionalString(params.os),
|
|
304
|
+
createdDevice: toOptionalString(params.device),
|
|
305
|
+
createdIp: toOptionalString(params.ip)
|
|
166
306
|
});
|
|
167
307
|
return { verified: true, userId: user.id, login: user.login };
|
|
168
308
|
}
|
|
@@ -179,22 +319,24 @@ export class PasskeyService {
|
|
|
179
319
|
}
|
|
180
320
|
const user = await this.requireUser({ userId: credential.userId, login: record.login });
|
|
181
321
|
const storedAuthData = {
|
|
182
|
-
|
|
322
|
+
id: toBase64Url(credential.credentialId),
|
|
323
|
+
publicKey: new Uint8Array(toBuffer(credential.publicKey)),
|
|
183
324
|
counter: credential.counter,
|
|
184
|
-
credentialBackedUp: credential.backedUp,
|
|
185
|
-
credentialDeviceType: credential.deviceType,
|
|
186
|
-
credentialPublicKey: credential.publicKey,
|
|
187
325
|
transports: credential.transports ?? undefined
|
|
326
|
+
// simplewebauthn accepts either Uint8Array or Buffer; ensure Buffer
|
|
327
|
+
// see https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/authentication/verifyAuthenticationResponse.ts
|
|
188
328
|
};
|
|
189
329
|
const result = await verifyAuthenticationResponse({
|
|
190
330
|
response,
|
|
191
331
|
expectedChallenge: record.challenge,
|
|
192
332
|
expectedOrigin: this.config.origins,
|
|
193
333
|
expectedRPID: this.config.rpId,
|
|
194
|
-
|
|
195
|
-
requireUserVerification:
|
|
334
|
+
credential: storedAuthData,
|
|
335
|
+
requireUserVerification: this.requireUserVerification()
|
|
196
336
|
});
|
|
197
337
|
if (!result.verified) {
|
|
338
|
+
const err = result.error ?? result;
|
|
339
|
+
this.logger.error?.('Passkey authentication verification failed', err);
|
|
198
340
|
return { verified: false };
|
|
199
341
|
}
|
|
200
342
|
await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
|
|
@@ -214,4 +356,7 @@ export class PasskeyService {
|
|
|
214
356
|
createExpiry() {
|
|
215
357
|
return new Date(Date.now() + this.config.timeoutMs);
|
|
216
358
|
}
|
|
359
|
+
requireUserVerification() {
|
|
360
|
+
return this.config.userVerification !== 'discouraged';
|
|
361
|
+
}
|
|
217
362
|
}
|
|
@@ -8,12 +8,11 @@ export interface PasskeyServiceConfig {
|
|
|
8
8
|
origins: string[];
|
|
9
9
|
timeoutMs: number;
|
|
10
10
|
userVerification?: 'preferred' | 'required' | 'discouraged';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
userAgent?: string;
|
|
11
|
+
/**
|
|
12
|
+
* When enabled, PasskeyService emits additional diagnostic logs during registration/authentication.
|
|
13
|
+
* Defaults to false.
|
|
14
|
+
*/
|
|
15
|
+
debug?: boolean;
|
|
17
16
|
}
|
|
18
17
|
export interface PasskeyChallengeRecord {
|
|
19
18
|
challenge: string;
|
|
@@ -21,7 +20,6 @@ export interface PasskeyChallengeRecord {
|
|
|
21
20
|
userId?: AuthIdentifier;
|
|
22
21
|
login?: string;
|
|
23
22
|
expiresAt: Date;
|
|
24
|
-
metadata: PasskeyChallengeMetadata;
|
|
25
23
|
}
|
|
26
24
|
export interface PasskeyUserDescriptor {
|
|
27
25
|
id: AuthIdentifier;
|
|
@@ -36,6 +34,15 @@ export interface StoredPasskeyCredential {
|
|
|
36
34
|
transports?: AuthenticatorTransportFuture[];
|
|
37
35
|
backedUp: boolean;
|
|
38
36
|
deviceType: CredentialDeviceType;
|
|
37
|
+
label?: string;
|
|
38
|
+
createdDomain?: string;
|
|
39
|
+
createdUserAgent?: string;
|
|
40
|
+
createdBrowser?: string;
|
|
41
|
+
createdOs?: string;
|
|
42
|
+
createdDevice?: string;
|
|
43
|
+
createdIp?: string;
|
|
44
|
+
createdAt?: Date;
|
|
45
|
+
updatedAt?: Date;
|
|
39
46
|
}
|
|
40
47
|
export interface PasskeyStorageAdapter {
|
|
41
48
|
resolveUser(params: {
|
|
@@ -43,17 +50,18 @@ export interface PasskeyStorageAdapter {
|
|
|
43
50
|
login?: string;
|
|
44
51
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
45
52
|
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
53
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
46
54
|
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
47
55
|
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
48
56
|
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
49
57
|
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
58
|
+
getChallenge?(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
50
59
|
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
51
60
|
cleanupChallenges?(now: Date): Promise<void>;
|
|
52
61
|
}
|
|
53
|
-
export interface PasskeyChallengeParams
|
|
62
|
+
export interface PasskeyChallengeParams {
|
|
54
63
|
action: 'register' | 'authenticate';
|
|
55
64
|
login?: string;
|
|
56
|
-
userAgent?: string;
|
|
57
65
|
userId?: AuthIdentifier;
|
|
58
66
|
}
|
|
59
67
|
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
@@ -66,6 +74,7 @@ export interface PasskeyVerificationParams extends Partial<Omit<Token, 'userId'>
|
|
|
66
74
|
login?: string;
|
|
67
75
|
response: Record<string, unknown>;
|
|
68
76
|
userId?: AuthIdentifier;
|
|
77
|
+
userAgent?: string;
|
|
69
78
|
}
|
|
70
79
|
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
71
80
|
login?: string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DataTypes, type InitOptions, type Model, type Sequelize } from 'sequelize';
|
|
2
|
+
export declare const DIALECTS_SUPPORTING_UNSIGNED: Set<string>;
|
|
3
|
+
export declare function integerIdType(sequelize: Sequelize): DataTypes.IntegerDataTypeConstructor;
|
|
4
|
+
export declare function tableOptions<ModelType extends Model>(sequelize: Sequelize, tableName: string, tablePrefix?: string, extra?: Partial<InitOptions<ModelType>>): InitOptions<ModelType>;
|
|
5
|
+
export declare function normalizeTablePrefix(prefix?: string): string | undefined;
|
|
6
|
+
export declare function applyTablePrefix(prefix: string | undefined, tableName: string): string;
|
|
7
|
+
export declare function encodeStringArray(values: string[] | undefined): string;
|
|
8
|
+
export declare function decodeStringArray(raw: string | null | undefined): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
export const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
3
|
+
export function integerIdType(sequelize) {
|
|
4
|
+
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
5
|
+
}
|
|
6
|
+
export function tableOptions(sequelize, tableName, tablePrefix, extra) {
|
|
7
|
+
const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
|
|
8
|
+
if (extra) {
|
|
9
|
+
Object.assign(opts, extra);
|
|
10
|
+
}
|
|
11
|
+
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
12
|
+
opts.charset = 'utf8mb4';
|
|
13
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
14
|
+
}
|
|
15
|
+
return opts;
|
|
16
|
+
}
|
|
17
|
+
export function normalizeTablePrefix(prefix) {
|
|
18
|
+
if (!prefix) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const trimmed = prefix.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function applyTablePrefix(prefix, tableName) {
|
|
25
|
+
const normalized = normalizeTablePrefix(prefix);
|
|
26
|
+
return normalized ? `${normalized}${tableName}` : tableName;
|
|
27
|
+
}
|
|
28
|
+
export function encodeStringArray(values) {
|
|
29
|
+
return JSON.stringify(values ?? []);
|
|
30
|
+
}
|
|
31
|
+
export function decodeStringArray(raw) {
|
|
32
|
+
if (!raw) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (Array.isArray(parsed)) {
|
|
38
|
+
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore malformed values
|
|
43
|
+
}
|
|
44
|
+
return raw
|
|
45
|
+
.split(/\s+/)
|
|
46
|
+
.map((entry) => entry.trim())
|
|
47
|
+
.filter((entry) => entry.length > 0);
|
|
48
|
+
}
|
package/dist/esm/token/base.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface JwtSignResult {
|
|
|
5
5
|
token?: string;
|
|
6
6
|
error?: string;
|
|
7
7
|
}
|
|
8
|
+
export type JwtSignPayload = string | Buffer | Record<string, unknown>;
|
|
8
9
|
export interface JwtVerifyResult<T> {
|
|
9
10
|
success: boolean;
|
|
10
11
|
data?: T;
|
|
@@ -32,7 +33,7 @@ export declare abstract class TokenStore {
|
|
|
32
33
|
}): Promise<Token[]>;
|
|
33
34
|
abstract close(): Promise<void>;
|
|
34
35
|
normalizeToken(token: Partial<Token>): Token;
|
|
35
|
-
jwtSign(payload:
|
|
36
|
+
jwtSign(payload: JwtSignPayload, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
36
37
|
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
37
38
|
jwtDecode<T>(token: string, options?: DecodeOptions): JwtDecodeResult<T>;
|
|
38
39
|
}
|
package/dist/esm/token/base.js
CHANGED
|
@@ -43,10 +43,10 @@ function normalizeTokenInternal(input) {
|
|
|
43
43
|
const status = input.status === 'revoked' ? 'revoked' : expires.getTime() < Date.now() ? 'expired' : 'active';
|
|
44
44
|
const sessionCookie = typeof input.sessionCookie === 'boolean' ? input.sessionCookie : false;
|
|
45
45
|
return {
|
|
46
|
-
...input,
|
|
47
46
|
accessToken: input.accessToken,
|
|
48
47
|
refreshToken: input.refreshToken,
|
|
49
48
|
userId,
|
|
49
|
+
clientId: typeof input.clientId === 'string' && input.clientId.length > 0 ? input.clientId : undefined,
|
|
50
50
|
domain: typeof input.domain === 'string' ? input.domain : '',
|
|
51
51
|
fingerprint: typeof input.fingerprint === 'string' ? input.fingerprint : '',
|
|
52
52
|
label: typeof input.label === 'string' ? input.label : '',
|
|
@@ -70,6 +70,8 @@ export class TokenStore {
|
|
|
70
70
|
normalizeToken(token) {
|
|
71
71
|
return normalizeTokenInternal(token);
|
|
72
72
|
}
|
|
73
|
+
// JWT helpers live on TokenStore so every adapter automatically exposes
|
|
74
|
+
// signing/verification without additional wiring.
|
|
73
75
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
74
76
|
const opts = { ...(options ?? {}), expiresIn: expiresInSeconds };
|
|
75
77
|
try {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { TokenStore } from './base.js';
|
|
2
2
|
import type { Token } from './types.js';
|
|
3
|
+
export interface MemoryTokenStoreOptions {
|
|
4
|
+
maxTokens?: number;
|
|
5
|
+
}
|
|
3
6
|
export declare class MemoryTokenStore extends TokenStore {
|
|
4
7
|
private readonly tokens;
|
|
8
|
+
private readonly tokensByUser;
|
|
9
|
+
private readonly maxTokens?;
|
|
10
|
+
constructor(options?: MemoryTokenStoreOptions);
|
|
11
|
+
private indexToken;
|
|
12
|
+
private unindexToken;
|
|
13
|
+
private removeByRefreshToken;
|
|
5
14
|
save(record: Token): Promise<void>;
|
|
6
15
|
get(query: Partial<Token>, opts?: {
|
|
7
16
|
includeExpired?: boolean;
|
|
@@ -16,4 +25,5 @@ export declare class MemoryTokenStore extends TokenStore {
|
|
|
16
25
|
includeExpired?: boolean;
|
|
17
26
|
}): Promise<Token[]>;
|
|
18
27
|
close(): Promise<void>;
|
|
28
|
+
private enforceCapacity;
|
|
19
29
|
}
|