@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.20
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 +81 -32
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.PasskeyService = void 0;
|
|
4
37
|
const server_1 = require("@simplewebauthn/server");
|
|
@@ -21,6 +54,13 @@ function sanitizeTransports(input) {
|
|
|
21
54
|
.filter((value) => ALLOWED_TRANSPORTS.includes(value));
|
|
22
55
|
return filtered.length > 0 ? filtered : undefined;
|
|
23
56
|
}
|
|
57
|
+
function toOptionalString(value) {
|
|
58
|
+
if (typeof value !== 'string') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
63
|
+
}
|
|
24
64
|
function toBase64Url(buffer) {
|
|
25
65
|
return helpers_1.isoBase64URL.fromBuffer(new Uint8Array(buffer));
|
|
26
66
|
}
|
|
@@ -34,30 +74,112 @@ function toBuffer(value) {
|
|
|
34
74
|
const view = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
35
75
|
return Buffer.from(view);
|
|
36
76
|
}
|
|
77
|
+
function toBufferOrNull(value) {
|
|
78
|
+
if (!value) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (Buffer.isBuffer(value)) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
85
|
+
return toBuffer(value);
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'string') {
|
|
88
|
+
try {
|
|
89
|
+
return fromBase64Url(value);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
async function spkiToCosePublicKey(spki) {
|
|
98
|
+
try {
|
|
99
|
+
const subtle = (globalThis.crypto?.subtle ??
|
|
100
|
+
(await Promise.resolve().then(() => __importStar(require('crypto')))).webcrypto?.subtle);
|
|
101
|
+
if (!subtle) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const spkiView = new Uint8Array(spki);
|
|
105
|
+
const ecCurves = [
|
|
106
|
+
{ namedCurve: 'P-256', crv: 1, alg: -7, rawLength: 65 },
|
|
107
|
+
{ namedCurve: 'P-384', crv: 2, alg: -35, rawLength: 97 },
|
|
108
|
+
{ namedCurve: 'P-521', crv: 3, alg: -36, rawLength: 133 }
|
|
109
|
+
];
|
|
110
|
+
for (const curve of ecCurves) {
|
|
111
|
+
try {
|
|
112
|
+
const key = await subtle.importKey('spki', spkiView, { name: 'ECDSA', namedCurve: curve.namedCurve }, true, ['verify']);
|
|
113
|
+
const raw = Buffer.from(await subtle.exportKey('raw', key));
|
|
114
|
+
if (raw.length !== curve.rawLength || raw[0] !== 0x04) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const coordLength = (raw.length - 1) / 2;
|
|
118
|
+
const x = raw.slice(1, 1 + coordLength);
|
|
119
|
+
const y = raw.slice(1 + coordLength);
|
|
120
|
+
const coseMap = new Map([
|
|
121
|
+
[1, 2], // kty: EC2
|
|
122
|
+
[3, curve.alg],
|
|
123
|
+
[-1, curve.crv],
|
|
124
|
+
[-2, x],
|
|
125
|
+
[-3, y]
|
|
126
|
+
]);
|
|
127
|
+
return Buffer.from(helpers_1.isoCBOR.encode(coseMap));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// try next algorithm
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const edKey = await subtle.importKey('spki', spkiView, { name: 'Ed25519' }, true, ['verify']);
|
|
135
|
+
const raw = Buffer.from(await subtle.exportKey('raw', edKey));
|
|
136
|
+
if (raw.length !== 32) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const coseMap = new Map([
|
|
140
|
+
[1, 1], // kty: OKP
|
|
141
|
+
[3, -8], // alg: EdDSA
|
|
142
|
+
[-1, 6], // crv: Ed25519
|
|
143
|
+
[-2, raw]
|
|
144
|
+
]);
|
|
145
|
+
return Buffer.from(helpers_1.isoCBOR.encode(coseMap));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
37
155
|
class PasskeyService {
|
|
38
156
|
constructor(config, adapter, logger = console) {
|
|
39
157
|
this.config = config;
|
|
40
158
|
this.adapter = adapter;
|
|
41
159
|
this.logger = logger;
|
|
42
160
|
}
|
|
161
|
+
async listUserCredentials(userId) {
|
|
162
|
+
return this.adapter.listUserCredentials(userId);
|
|
163
|
+
}
|
|
164
|
+
async deleteCredential(credentialId) {
|
|
165
|
+
return this.adapter.deleteCredential(credentialId);
|
|
166
|
+
}
|
|
43
167
|
async createChallenge(params) {
|
|
44
168
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
45
|
-
const metadata = {
|
|
46
|
-
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
47
|
-
fingerprint: typeof params.fingerprint === 'string' ? params.fingerprint : undefined,
|
|
48
|
-
label: typeof params.label === 'string' ? params.label : undefined,
|
|
49
|
-
userAgent: typeof params.userAgent === 'string' ? params.userAgent : undefined
|
|
50
|
-
};
|
|
51
169
|
if (params.action === 'register') {
|
|
52
|
-
return this.createRegistrationChallenge(params
|
|
170
|
+
return this.createRegistrationChallenge(params);
|
|
53
171
|
}
|
|
54
172
|
if (params.action === 'authenticate') {
|
|
55
|
-
return this.createAuthenticationChallenge(params
|
|
173
|
+
return this.createAuthenticationChallenge(params);
|
|
56
174
|
}
|
|
57
175
|
throw new Error(`Unsupported passkey action: ${String(params.action)}`);
|
|
58
176
|
}
|
|
59
177
|
async verifyResponse(params) {
|
|
60
178
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
179
|
+
const existing = this.adapter.getChallenge ? await this.adapter.getChallenge(params.expectedChallenge) : null;
|
|
180
|
+
if (existing && existing.expiresAt.getTime() <= Date.now()) {
|
|
181
|
+
return { verified: false };
|
|
182
|
+
}
|
|
61
183
|
const record = await this.adapter.consumeChallenge(params.expectedChallenge);
|
|
62
184
|
if (!record) {
|
|
63
185
|
return { verified: false };
|
|
@@ -78,7 +200,7 @@ class PasskeyService {
|
|
|
78
200
|
}
|
|
79
201
|
return { verified: false };
|
|
80
202
|
}
|
|
81
|
-
async createRegistrationChallenge(params
|
|
203
|
+
async createRegistrationChallenge(params) {
|
|
82
204
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
83
205
|
const existing = await this.adapter.listUserCredentials(user.id);
|
|
84
206
|
const excludeCredentials = existing.map((credential) => {
|
|
@@ -101,16 +223,16 @@ class PasskeyService {
|
|
|
101
223
|
action: 'register',
|
|
102
224
|
userId: user.id,
|
|
103
225
|
login: user.login,
|
|
104
|
-
expiresAt
|
|
105
|
-
metadata
|
|
226
|
+
expiresAt
|
|
106
227
|
});
|
|
107
228
|
return {
|
|
108
229
|
challenge: options.challenge,
|
|
109
230
|
expiresAt: expiresAt.toISOString(),
|
|
110
|
-
userId: user.id
|
|
231
|
+
userId: user.id,
|
|
232
|
+
publicKey: options
|
|
111
233
|
};
|
|
112
234
|
}
|
|
113
|
-
async createAuthenticationChallenge(params
|
|
235
|
+
async createAuthenticationChallenge(params) {
|
|
114
236
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
115
237
|
const credentials = await this.adapter.listUserCredentials(user.id);
|
|
116
238
|
const allowCredentials = credentials.map((credential) => {
|
|
@@ -130,13 +252,13 @@ class PasskeyService {
|
|
|
130
252
|
action: 'authenticate',
|
|
131
253
|
userId: user.id,
|
|
132
254
|
login: user.login,
|
|
133
|
-
expiresAt
|
|
134
|
-
metadata
|
|
255
|
+
expiresAt
|
|
135
256
|
});
|
|
136
257
|
return {
|
|
137
258
|
challenge: options.challenge,
|
|
138
259
|
expiresAt: expiresAt.toISOString(),
|
|
139
|
-
userId: user.id
|
|
260
|
+
userId: user.id,
|
|
261
|
+
publicKey: options
|
|
140
262
|
};
|
|
141
263
|
}
|
|
142
264
|
async verifyRegistration(params, record) {
|
|
@@ -152,20 +274,71 @@ class PasskeyService {
|
|
|
152
274
|
expectedChallenge: record.challenge,
|
|
153
275
|
expectedOrigin: this.config.origins,
|
|
154
276
|
expectedRPID: this.config.rpId,
|
|
155
|
-
requireUserVerification:
|
|
277
|
+
requireUserVerification: this.requireUserVerification()
|
|
156
278
|
});
|
|
157
279
|
if (!result.verified || !result.registrationInfo) {
|
|
280
|
+
if (!result.verified) {
|
|
281
|
+
const err = result.error ?? result;
|
|
282
|
+
this.logger.error?.('Passkey registration verification failed', err);
|
|
283
|
+
}
|
|
158
284
|
return { verified: false };
|
|
159
285
|
}
|
|
160
286
|
const registrationInfo = result.registrationInfo;
|
|
287
|
+
const attestationResponse = params.response.response;
|
|
288
|
+
const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
|
|
289
|
+
const credentialIdFallback = toBufferOrNull(params.response.id);
|
|
290
|
+
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0 ? credentialIdPrimary : credentialIdFallback;
|
|
291
|
+
const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
|
|
292
|
+
let publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
|
|
293
|
+
if ((!publicKeyPrimary || publicKeyPrimary.length === 0) && attestationResponse?.attestationObject) {
|
|
294
|
+
try {
|
|
295
|
+
const attestationObject = String(attestationResponse.attestationObject);
|
|
296
|
+
const attObj = (0, helpers_1.decodeAttestationObject)(helpers_1.isoBase64URL.toBuffer(attestationObject));
|
|
297
|
+
const parsedAuth = (0, helpers_1.parseAuthenticatorData)(attObj.get('authData'));
|
|
298
|
+
publicKeyFallback = toBufferOrNull(parsedAuth.credentialPublicKey) ?? publicKeyFallback;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
this.logger.warn?.('Passkey registration: failed to parse attestationObject', error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
|
|
305
|
+
if (this.config.debug && this.logger?.warn) {
|
|
306
|
+
const pkPrimaryHex = publicKeyPrimary ? publicKeyPrimary.slice(0, 4).toString('hex') : 'null';
|
|
307
|
+
const pkFallbackHex = publicKeyFallback ? publicKeyFallback.slice(0, 4).toString('hex') : 'null';
|
|
308
|
+
this.logger.warn(`Passkey registration: pkPrimary len=${publicKeyPrimary?.length ?? 0} head=${pkPrimaryHex}, pkFallback len=${publicKeyFallback?.length ?? 0} head=${pkFallbackHex}`);
|
|
309
|
+
}
|
|
310
|
+
if (!credentialId || credentialId.length === 0) {
|
|
311
|
+
return { verified: false, message: 'Missing credential id in registration response' };
|
|
312
|
+
}
|
|
313
|
+
if (!publicKey || publicKey.length === 0) {
|
|
314
|
+
return { verified: false, message: 'Missing public key in registration response' };
|
|
315
|
+
}
|
|
316
|
+
let storedPublicKey = Buffer.isBuffer(publicKey) ? publicKey : toBuffer(publicKey);
|
|
317
|
+
// If fallback is an SPKI key (DER, starts with 0x30) convert to COSE so verification succeeds.
|
|
318
|
+
if (storedPublicKey[0] === 0x30) {
|
|
319
|
+
const converted = await spkiToCosePublicKey(storedPublicKey);
|
|
320
|
+
if (converted) {
|
|
321
|
+
storedPublicKey = converted;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
this.logger.warn?.('Passkey registration: failed to convert SPKI public key to COSE');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
161
327
|
await this.adapter.saveCredential({
|
|
162
328
|
userId: user.id,
|
|
163
|
-
credentialId
|
|
164
|
-
publicKey:
|
|
165
|
-
counter: registrationInfo.counter,
|
|
329
|
+
credentialId,
|
|
330
|
+
publicKey: storedPublicKey,
|
|
331
|
+
counter: registrationInfo.counter ?? 0,
|
|
166
332
|
transports: sanitizeTransports(params.response.transports),
|
|
167
333
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
168
|
-
deviceType: registrationInfo.credentialDeviceType
|
|
334
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
335
|
+
label: toOptionalString(params.label),
|
|
336
|
+
createdDomain: toOptionalString(params.domain),
|
|
337
|
+
createdUserAgent: toOptionalString(params.userAgent),
|
|
338
|
+
createdBrowser: toOptionalString(params.browser),
|
|
339
|
+
createdOs: toOptionalString(params.os),
|
|
340
|
+
createdDevice: toOptionalString(params.device),
|
|
341
|
+
createdIp: toOptionalString(params.ip)
|
|
169
342
|
});
|
|
170
343
|
return { verified: true, userId: user.id, login: user.login };
|
|
171
344
|
}
|
|
@@ -182,22 +355,24 @@ class PasskeyService {
|
|
|
182
355
|
}
|
|
183
356
|
const user = await this.requireUser({ userId: credential.userId, login: record.login });
|
|
184
357
|
const storedAuthData = {
|
|
185
|
-
|
|
358
|
+
id: toBase64Url(credential.credentialId),
|
|
359
|
+
publicKey: new Uint8Array(toBuffer(credential.publicKey)),
|
|
186
360
|
counter: credential.counter,
|
|
187
|
-
credentialBackedUp: credential.backedUp,
|
|
188
|
-
credentialDeviceType: credential.deviceType,
|
|
189
|
-
credentialPublicKey: credential.publicKey,
|
|
190
361
|
transports: credential.transports ?? undefined
|
|
362
|
+
// simplewebauthn accepts either Uint8Array or Buffer; ensure Buffer
|
|
363
|
+
// see https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/authentication/verifyAuthenticationResponse.ts
|
|
191
364
|
};
|
|
192
365
|
const result = await (0, server_1.verifyAuthenticationResponse)({
|
|
193
366
|
response,
|
|
194
367
|
expectedChallenge: record.challenge,
|
|
195
368
|
expectedOrigin: this.config.origins,
|
|
196
369
|
expectedRPID: this.config.rpId,
|
|
197
|
-
|
|
198
|
-
requireUserVerification:
|
|
370
|
+
credential: storedAuthData,
|
|
371
|
+
requireUserVerification: this.requireUserVerification()
|
|
199
372
|
});
|
|
200
373
|
if (!result.verified) {
|
|
374
|
+
const err = result.error ?? result;
|
|
375
|
+
this.logger.error?.('Passkey authentication verification failed', err);
|
|
201
376
|
return { verified: false };
|
|
202
377
|
}
|
|
203
378
|
await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
|
|
@@ -217,5 +392,8 @@ class PasskeyService {
|
|
|
217
392
|
createExpiry() {
|
|
218
393
|
return new Date(Date.now() + this.config.timeoutMs);
|
|
219
394
|
}
|
|
395
|
+
requireUserVerification() {
|
|
396
|
+
return this.config.userVerification !== 'discouraged';
|
|
397
|
+
}
|
|
220
398
|
}
|
|
221
399
|
exports.PasskeyService = PasskeyService;
|
|
@@ -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,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DIALECTS_SUPPORTING_UNSIGNED = void 0;
|
|
4
|
+
exports.integerIdType = integerIdType;
|
|
5
|
+
exports.tableOptions = tableOptions;
|
|
6
|
+
exports.normalizeTablePrefix = normalizeTablePrefix;
|
|
7
|
+
exports.applyTablePrefix = applyTablePrefix;
|
|
8
|
+
exports.encodeStringArray = encodeStringArray;
|
|
9
|
+
exports.decodeStringArray = decodeStringArray;
|
|
10
|
+
const sequelize_1 = require("sequelize");
|
|
11
|
+
exports.DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
12
|
+
function integerIdType(sequelize) {
|
|
13
|
+
return exports.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
|
|
14
|
+
}
|
|
15
|
+
function tableOptions(sequelize, tableName, tablePrefix, extra) {
|
|
16
|
+
const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
|
|
17
|
+
if (extra) {
|
|
18
|
+
Object.assign(opts, extra);
|
|
19
|
+
}
|
|
20
|
+
if (exports.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
21
|
+
opts.charset = 'utf8mb4';
|
|
22
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
23
|
+
}
|
|
24
|
+
return opts;
|
|
25
|
+
}
|
|
26
|
+
function normalizeTablePrefix(prefix) {
|
|
27
|
+
if (!prefix) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = prefix.trim();
|
|
31
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
32
|
+
}
|
|
33
|
+
function applyTablePrefix(prefix, tableName) {
|
|
34
|
+
const normalized = normalizeTablePrefix(prefix);
|
|
35
|
+
return normalized ? `${normalized}${tableName}` : tableName;
|
|
36
|
+
}
|
|
37
|
+
function encodeStringArray(values) {
|
|
38
|
+
return JSON.stringify(values ?? []);
|
|
39
|
+
}
|
|
40
|
+
function decodeStringArray(raw) {
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (Array.isArray(parsed)) {
|
|
47
|
+
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore malformed values
|
|
52
|
+
}
|
|
53
|
+
return raw
|
|
54
|
+
.split(/\s+/)
|
|
55
|
+
.map((entry) => entry.trim())
|
|
56
|
+
.filter((entry) => entry.length > 0);
|
|
57
|
+
}
|
package/dist/cjs/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/cjs/token/base.js
CHANGED
|
@@ -49,10 +49,10 @@ function normalizeTokenInternal(input) {
|
|
|
49
49
|
const status = input.status === 'revoked' ? 'revoked' : expires.getTime() < Date.now() ? 'expired' : 'active';
|
|
50
50
|
const sessionCookie = typeof input.sessionCookie === 'boolean' ? input.sessionCookie : false;
|
|
51
51
|
return {
|
|
52
|
-
...input,
|
|
53
52
|
accessToken: input.accessToken,
|
|
54
53
|
refreshToken: input.refreshToken,
|
|
55
54
|
userId,
|
|
55
|
+
clientId: typeof input.clientId === 'string' && input.clientId.length > 0 ? input.clientId : undefined,
|
|
56
56
|
domain: typeof input.domain === 'string' ? input.domain : '',
|
|
57
57
|
fingerprint: typeof input.fingerprint === 'string' ? input.fingerprint : '',
|
|
58
58
|
label: typeof input.label === 'string' ? input.label : '',
|
|
@@ -76,6 +76,8 @@ class TokenStore {
|
|
|
76
76
|
normalizeToken(token) {
|
|
77
77
|
return normalizeTokenInternal(token);
|
|
78
78
|
}
|
|
79
|
+
// JWT helpers live on TokenStore so every adapter automatically exposes
|
|
80
|
+
// signing/verification without additional wiring.
|
|
79
81
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
80
82
|
const opts = { ...(options ?? {}), expiresIn: expiresInSeconds };
|
|
81
83
|
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
|
}
|