@technomoron/api-server-base 2.0.0-beta.19 → 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/dist/cjs/api-server-base.cjs +48 -21
- package/dist/cjs/auth-api/auth-module.js +16 -30
- package/dist/cjs/auth-api/mem-auth-store.js +5 -4
- package/dist/cjs/auth-api/sql-auth-store.js +6 -4
- package/dist/cjs/auth-api/user-id.d.ts +1 -0
- package/dist/cjs/auth-api/user-id.js +7 -0
- package/dist/cjs/auth-cookie-options.js +10 -1
- package/dist/cjs/oauth/memory.d.ts +6 -0
- package/dist/cjs/oauth/memory.js +43 -4
- package/dist/cjs/oauth/models.d.ts +1 -0
- package/dist/cjs/oauth/models.js +7 -18
- package/dist/cjs/oauth/sequelize.d.ts +5 -52
- package/dist/cjs/oauth/sequelize.js +34 -93
- package/dist/cjs/oauth/types.d.ts +1 -0
- package/dist/cjs/passkey/base.d.ts +1 -0
- package/dist/cjs/passkey/memory.d.ts +7 -0
- package/dist/cjs/passkey/memory.js +47 -5
- package/dist/cjs/passkey/models.js +2 -5
- package/dist/cjs/passkey/sequelize.d.ts +5 -29
- package/dist/cjs/passkey/sequelize.js +48 -191
- package/dist/cjs/passkey/service.d.ts +1 -0
- package/dist/cjs/passkey/service.js +52 -15
- package/dist/cjs/passkey/types.d.ts +1 -0
- package/dist/cjs/sequelize-utils.d.ts +5 -0
- package/dist/cjs/sequelize-utils.js +40 -0
- package/dist/cjs/token/base.js +3 -1
- package/dist/cjs/token/memory.d.ts +6 -0
- package/dist/cjs/token/memory.js +32 -7
- package/dist/cjs/token/sequelize.d.ts +0 -3
- package/dist/cjs/token/sequelize.js +50 -81
- package/dist/cjs/token/types.d.ts +1 -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 +8 -2
- package/dist/cjs/user/sequelize.js +12 -22
- package/dist/esm/api-server-base.js +48 -21
- package/dist/esm/auth-api/auth-module.js +16 -30
- package/dist/esm/auth-api/mem-auth-store.js +5 -4
- package/dist/esm/auth-api/sql-auth-store.js +6 -4
- package/dist/esm/auth-api/user-id.d.ts +1 -0
- package/dist/esm/auth-api/user-id.js +6 -0
- package/dist/esm/auth-cookie-options.js +10 -1
- package/dist/esm/oauth/memory.d.ts +6 -0
- package/dist/esm/oauth/memory.js +44 -5
- package/dist/esm/oauth/models.d.ts +1 -0
- package/dist/esm/oauth/models.js +2 -15
- package/dist/esm/oauth/sequelize.d.ts +5 -52
- package/dist/esm/oauth/sequelize.js +21 -80
- package/dist/esm/oauth/types.d.ts +1 -0
- package/dist/esm/passkey/base.d.ts +1 -0
- package/dist/esm/passkey/memory.d.ts +7 -0
- package/dist/esm/passkey/memory.js +47 -5
- package/dist/esm/passkey/models.js +1 -4
- package/dist/esm/passkey/sequelize.d.ts +5 -29
- package/dist/esm/passkey/sequelize.js +47 -190
- package/dist/esm/passkey/service.d.ts +1 -0
- package/dist/esm/passkey/service.js +52 -15
- package/dist/esm/passkey/types.d.ts +1 -0
- package/dist/esm/sequelize-utils.d.ts +5 -0
- package/dist/esm/sequelize-utils.js +36 -0
- package/dist/esm/token/base.js +3 -1
- package/dist/esm/token/memory.d.ts +6 -0
- package/dist/esm/token/memory.js +32 -7
- package/dist/esm/token/sequelize.d.ts +0 -3
- package/dist/esm/token/sequelize.js +51 -82
- package/dist/esm/token/types.d.ts +1 -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 +8 -2
- package/dist/esm/user/sequelize.js +13 -23
- package/package.json +5 -5
|
@@ -1,149 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Op, Transaction } from 'sequelize';
|
|
2
2
|
import { normalizeNumericUserId } from '../auth-api/user-id.js';
|
|
3
|
-
import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
|
|
4
3
|
import { PasskeyStore } from './base.js';
|
|
5
|
-
|
|
6
|
-
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
7
|
-
}
|
|
4
|
+
import { initPasskeyChallengeModel, initPasskeyCredentialModel } from './models.js';
|
|
8
5
|
function encodeCredentialId(value) {
|
|
9
6
|
return Buffer.isBuffer(value) ? value.toString('base64') : value;
|
|
10
7
|
}
|
|
11
|
-
function normalizeUserId(identifier) {
|
|
12
|
-
return normalizeNumericUserId(identifier);
|
|
13
|
-
}
|
|
14
|
-
class PasskeyCredentialModel extends Model {
|
|
15
|
-
}
|
|
16
|
-
class PasskeyChallengeModel extends Model {
|
|
17
|
-
}
|
|
18
|
-
function initPasskeyCredentialModel(sequelize, options = {}) {
|
|
19
|
-
const idType = integerIdType(sequelize);
|
|
20
|
-
return PasskeyCredentialModel.init({
|
|
21
|
-
credentialId: {
|
|
22
|
-
field: 'credential_id',
|
|
23
|
-
type: DataTypes.STRING(768),
|
|
24
|
-
primaryKey: true,
|
|
25
|
-
allowNull: false,
|
|
26
|
-
get() {
|
|
27
|
-
const raw = this.getDataValue('credentialId');
|
|
28
|
-
if (!raw) {
|
|
29
|
-
return raw;
|
|
30
|
-
}
|
|
31
|
-
if (Buffer.isBuffer(raw)) {
|
|
32
|
-
return raw;
|
|
33
|
-
}
|
|
34
|
-
return Buffer.from(raw, 'base64');
|
|
35
|
-
},
|
|
36
|
-
set(value) {
|
|
37
|
-
const encoded = typeof value === 'string' ? value : value.toString('base64');
|
|
38
|
-
this.setDataValue('credentialId', encoded);
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
userId: {
|
|
42
|
-
field: 'user_id',
|
|
43
|
-
type: idType,
|
|
44
|
-
allowNull: false
|
|
45
|
-
},
|
|
46
|
-
publicKey: {
|
|
47
|
-
field: 'public_key',
|
|
48
|
-
type: DataTypes.BLOB,
|
|
49
|
-
allowNull: false
|
|
50
|
-
},
|
|
51
|
-
counter: {
|
|
52
|
-
type: DataTypes.INTEGER,
|
|
53
|
-
allowNull: false,
|
|
54
|
-
defaultValue: 0
|
|
55
|
-
},
|
|
56
|
-
transports: {
|
|
57
|
-
type: DataTypes.JSON,
|
|
58
|
-
allowNull: true
|
|
59
|
-
},
|
|
60
|
-
backedUp: {
|
|
61
|
-
field: 'backed_up',
|
|
62
|
-
type: DataTypes.BOOLEAN,
|
|
63
|
-
allowNull: false,
|
|
64
|
-
defaultValue: false
|
|
65
|
-
},
|
|
66
|
-
deviceType: {
|
|
67
|
-
field: 'device_type',
|
|
68
|
-
type: DataTypes.STRING(32),
|
|
69
|
-
allowNull: false,
|
|
70
|
-
defaultValue: 'multiDevice'
|
|
71
|
-
},
|
|
72
|
-
label: {
|
|
73
|
-
type: DataTypes.STRING(120),
|
|
74
|
-
allowNull: true
|
|
75
|
-
},
|
|
76
|
-
createdDomain: {
|
|
77
|
-
field: 'created_domain',
|
|
78
|
-
type: DataTypes.STRING(255),
|
|
79
|
-
allowNull: true
|
|
80
|
-
},
|
|
81
|
-
createdUserAgent: {
|
|
82
|
-
field: 'created_user_agent',
|
|
83
|
-
type: DataTypes.TEXT,
|
|
84
|
-
allowNull: true
|
|
85
|
-
},
|
|
86
|
-
createdBrowser: {
|
|
87
|
-
field: 'created_browser',
|
|
88
|
-
type: DataTypes.STRING(120),
|
|
89
|
-
allowNull: true
|
|
90
|
-
},
|
|
91
|
-
createdOs: {
|
|
92
|
-
field: 'created_os',
|
|
93
|
-
type: DataTypes.STRING(120),
|
|
94
|
-
allowNull: true
|
|
95
|
-
},
|
|
96
|
-
createdDevice: {
|
|
97
|
-
field: 'created_device',
|
|
98
|
-
type: DataTypes.STRING(120),
|
|
99
|
-
allowNull: true
|
|
100
|
-
},
|
|
101
|
-
createdIp: {
|
|
102
|
-
field: 'created_ip',
|
|
103
|
-
type: DataTypes.STRING(45),
|
|
104
|
-
allowNull: true
|
|
105
|
-
}
|
|
106
|
-
}, {
|
|
107
|
-
sequelize,
|
|
108
|
-
tableName: applyTablePrefix(options.tablePrefix, 'passkey_credentials'),
|
|
109
|
-
timestamps: true,
|
|
110
|
-
underscored: true
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
function initPasskeyChallengeModel(sequelize, options = {}) {
|
|
114
|
-
const idType = integerIdType(sequelize);
|
|
115
|
-
return PasskeyChallengeModel.init({
|
|
116
|
-
challenge: {
|
|
117
|
-
type: DataTypes.STRING(255),
|
|
118
|
-
primaryKey: true,
|
|
119
|
-
allowNull: false
|
|
120
|
-
},
|
|
121
|
-
action: {
|
|
122
|
-
type: DataTypes.STRING(16),
|
|
123
|
-
allowNull: false
|
|
124
|
-
},
|
|
125
|
-
userId: {
|
|
126
|
-
field: 'user_id',
|
|
127
|
-
type: idType,
|
|
128
|
-
allowNull: true
|
|
129
|
-
},
|
|
130
|
-
login: {
|
|
131
|
-
type: DataTypes.STRING(128),
|
|
132
|
-
allowNull: true
|
|
133
|
-
},
|
|
134
|
-
expiresAt: {
|
|
135
|
-
field: 'expires_at',
|
|
136
|
-
type: DataTypes.DATE,
|
|
137
|
-
allowNull: false
|
|
138
|
-
}
|
|
139
|
-
}, {
|
|
140
|
-
sequelize,
|
|
141
|
-
tableName: applyTablePrefix(options.tablePrefix, 'passkey_challenges'),
|
|
142
|
-
timestamps: true,
|
|
143
|
-
underscored: true,
|
|
144
|
-
indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
8
|
export class SequelizePasskeyStore extends PasskeyStore {
|
|
148
9
|
constructor(options) {
|
|
149
10
|
super();
|
|
@@ -166,25 +27,8 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
166
27
|
return this.resolveUserFn(params);
|
|
167
28
|
}
|
|
168
29
|
async listUserCredentials(userId) {
|
|
169
|
-
const models = await this.credentials.findAll({ where: { userId:
|
|
170
|
-
return models.map((model) => (
|
|
171
|
-
userId: model.userId,
|
|
172
|
-
credentialId: model.credentialId,
|
|
173
|
-
publicKey: model.publicKey,
|
|
174
|
-
counter: model.counter,
|
|
175
|
-
transports: (model.transports ?? undefined),
|
|
176
|
-
backedUp: model.backedUp,
|
|
177
|
-
deviceType: model.deviceType,
|
|
178
|
-
label: model.label ?? undefined,
|
|
179
|
-
createdDomain: model.createdDomain ?? undefined,
|
|
180
|
-
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
181
|
-
createdBrowser: model.createdBrowser ?? undefined,
|
|
182
|
-
createdOs: model.createdOs ?? undefined,
|
|
183
|
-
createdDevice: model.createdDevice ?? undefined,
|
|
184
|
-
createdIp: model.createdIp ?? undefined,
|
|
185
|
-
createdAt: model.createdAt ?? undefined,
|
|
186
|
-
updatedAt: model.updatedAt ?? undefined
|
|
187
|
-
}));
|
|
30
|
+
const models = await this.credentials.findAll({ where: { userId: normalizeNumericUserId(userId) } });
|
|
31
|
+
return models.map((model) => this.toStoredCredential(model));
|
|
188
32
|
}
|
|
189
33
|
async deleteCredential(credentialId) {
|
|
190
34
|
const encoded = Buffer.isBuffer(credentialId) ? credentialId.toString('base64') : credentialId;
|
|
@@ -193,32 +37,12 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
193
37
|
}
|
|
194
38
|
async findCredentialById(credentialId) {
|
|
195
39
|
const model = await this.credentials.findByPk(encodeCredentialId(credentialId));
|
|
196
|
-
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
userId: model.userId,
|
|
201
|
-
credentialId: model.credentialId,
|
|
202
|
-
publicKey: model.publicKey,
|
|
203
|
-
counter: model.counter,
|
|
204
|
-
transports: (model.transports ?? undefined),
|
|
205
|
-
backedUp: model.backedUp,
|
|
206
|
-
deviceType: model.deviceType,
|
|
207
|
-
label: model.label ?? undefined,
|
|
208
|
-
createdDomain: model.createdDomain ?? undefined,
|
|
209
|
-
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
210
|
-
createdBrowser: model.createdBrowser ?? undefined,
|
|
211
|
-
createdOs: model.createdOs ?? undefined,
|
|
212
|
-
createdDevice: model.createdDevice ?? undefined,
|
|
213
|
-
createdIp: model.createdIp ?? undefined,
|
|
214
|
-
createdAt: model.createdAt ?? undefined,
|
|
215
|
-
updatedAt: model.updatedAt ?? undefined
|
|
216
|
-
};
|
|
40
|
+
return model ? this.toStoredCredential(model) : null;
|
|
217
41
|
}
|
|
218
42
|
async saveCredential(record) {
|
|
219
43
|
await this.credentials.upsert({
|
|
220
44
|
credentialId: record.credentialId,
|
|
221
|
-
userId:
|
|
45
|
+
userId: normalizeNumericUserId(record.userId),
|
|
222
46
|
publicKey: record.publicKey,
|
|
223
47
|
counter: record.counter,
|
|
224
48
|
transports: record.transports ?? null,
|
|
@@ -240,26 +64,59 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
240
64
|
await this.challenges.upsert({
|
|
241
65
|
challenge: record.challenge,
|
|
242
66
|
action: record.action,
|
|
243
|
-
userId: record.userId !== undefined ?
|
|
67
|
+
userId: record.userId !== undefined ? normalizeNumericUserId(record.userId) : null,
|
|
244
68
|
login: record.login ?? null,
|
|
245
69
|
expiresAt: record.expiresAt
|
|
246
70
|
});
|
|
247
71
|
}
|
|
248
|
-
async
|
|
72
|
+
async getChallenge(challenge) {
|
|
249
73
|
const model = await this.challenges.findByPk(challenge);
|
|
250
|
-
|
|
251
|
-
|
|
74
|
+
return model ? this.toChallengeRecord(model) : null;
|
|
75
|
+
}
|
|
76
|
+
async consumeChallenge(challenge) {
|
|
77
|
+
const sequelize = this.challenges.sequelize;
|
|
78
|
+
if (!sequelize) {
|
|
79
|
+
throw new Error('Challenge model is not bound to a Sequelize instance');
|
|
252
80
|
}
|
|
253
|
-
|
|
81
|
+
return sequelize.transaction({ isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }, async (transaction) => {
|
|
82
|
+
const model = await this.challenges.findByPk(challenge, { transaction, lock: true });
|
|
83
|
+
if (!model) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
await model.destroy({ transaction });
|
|
87
|
+
return this.toChallengeRecord(model);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async cleanupChallenges(now) {
|
|
91
|
+
await this.challenges.destroy({ where: { expiresAt: { [Op.lte]: now } } });
|
|
92
|
+
}
|
|
93
|
+
toChallengeRecord(model) {
|
|
254
94
|
return {
|
|
255
95
|
challenge: model.challenge,
|
|
256
96
|
action: model.action,
|
|
257
|
-
userId: model.userId
|
|
97
|
+
userId: model.userId !== null ? String(model.userId) : undefined,
|
|
258
98
|
login: model.login ?? undefined,
|
|
259
99
|
expiresAt: model.expiresAt
|
|
260
100
|
};
|
|
261
101
|
}
|
|
262
|
-
|
|
263
|
-
|
|
102
|
+
toStoredCredential(model) {
|
|
103
|
+
return {
|
|
104
|
+
userId: String(model.userId),
|
|
105
|
+
credentialId: model.credentialId,
|
|
106
|
+
publicKey: model.publicKey,
|
|
107
|
+
counter: model.counter,
|
|
108
|
+
transports: (model.transports ?? undefined),
|
|
109
|
+
backedUp: model.backedUp,
|
|
110
|
+
deviceType: model.deviceType,
|
|
111
|
+
label: model.label ?? undefined,
|
|
112
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
113
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
114
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
115
|
+
createdOs: model.createdOs ?? undefined,
|
|
116
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
117
|
+
createdIp: model.createdIp ?? undefined,
|
|
118
|
+
createdAt: model.createdAt ?? undefined,
|
|
119
|
+
updatedAt: model.updatedAt ?? undefined
|
|
120
|
+
};
|
|
264
121
|
}
|
|
265
122
|
}
|
|
@@ -66,21 +66,51 @@ async function spkiToCosePublicKey(spki) {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
const spkiView = new Uint8Array(spki);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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 {
|
|
72
112
|
return null;
|
|
73
113
|
}
|
|
74
|
-
const x = raw.slice(1, 33);
|
|
75
|
-
const y = raw.slice(33, 65);
|
|
76
|
-
const coseMap = new Map([
|
|
77
|
-
[1, 2], // kty: EC2
|
|
78
|
-
[3, -7], // alg: ES256
|
|
79
|
-
[-1, 1], // crv: P-256
|
|
80
|
-
[-2, x],
|
|
81
|
-
[-3, y]
|
|
82
|
-
]);
|
|
83
|
-
return Buffer.from(isoCBOR.encode(coseMap));
|
|
84
114
|
}
|
|
85
115
|
catch {
|
|
86
116
|
return null;
|
|
@@ -110,6 +140,10 @@ export class PasskeyService {
|
|
|
110
140
|
}
|
|
111
141
|
async verifyResponse(params) {
|
|
112
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
|
+
}
|
|
113
147
|
const record = await this.adapter.consumeChallenge(params.expectedChallenge);
|
|
114
148
|
if (!record) {
|
|
115
149
|
return { verified: false };
|
|
@@ -204,7 +238,7 @@ export class PasskeyService {
|
|
|
204
238
|
expectedChallenge: record.challenge,
|
|
205
239
|
expectedOrigin: this.config.origins,
|
|
206
240
|
expectedRPID: this.config.rpId,
|
|
207
|
-
requireUserVerification:
|
|
241
|
+
requireUserVerification: this.requireUserVerification()
|
|
208
242
|
});
|
|
209
243
|
if (!result.verified || !result.registrationInfo) {
|
|
210
244
|
if (!result.verified) {
|
|
@@ -298,7 +332,7 @@ export class PasskeyService {
|
|
|
298
332
|
expectedOrigin: this.config.origins,
|
|
299
333
|
expectedRPID: this.config.rpId,
|
|
300
334
|
credential: storedAuthData,
|
|
301
|
-
requireUserVerification:
|
|
335
|
+
requireUserVerification: this.requireUserVerification()
|
|
302
336
|
});
|
|
303
337
|
if (!result.verified) {
|
|
304
338
|
const err = result.error ?? result;
|
|
@@ -322,4 +356,7 @@ export class PasskeyService {
|
|
|
322
356
|
createExpiry() {
|
|
323
357
|
return new Date(Date.now() + this.config.timeoutMs);
|
|
324
358
|
}
|
|
359
|
+
requireUserVerification() {
|
|
360
|
+
return this.config.userVerification !== 'discouraged';
|
|
361
|
+
}
|
|
325
362
|
}
|
|
@@ -55,6 +55,7 @@ export interface PasskeyStorageAdapter {
|
|
|
55
55
|
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
56
56
|
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
57
57
|
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
58
|
+
getChallenge?(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
58
59
|
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
59
60
|
cleanupChallenges?(now: Date): Promise<void>;
|
|
60
61
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { DataTypes, type InitOptions, type Model, type Sequelize } from 'sequelize';
|
|
1
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>;
|
|
2
5
|
export declare function normalizeTablePrefix(prefix?: string): string | undefined;
|
|
3
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[];
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
1
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
|
+
}
|
|
2
17
|
export function normalizeTablePrefix(prefix) {
|
|
3
18
|
if (!prefix) {
|
|
4
19
|
return undefined;
|
|
@@ -10,3 +25,24 @@ export function applyTablePrefix(prefix, tableName) {
|
|
|
10
25
|
const normalized = normalizeTablePrefix(prefix);
|
|
11
26
|
return normalized ? `${normalized}${tableName}` : tableName;
|
|
12
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.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,8 +1,13 @@
|
|
|
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;
|
|
5
8
|
private readonly tokensByUser;
|
|
9
|
+
private readonly maxTokens?;
|
|
10
|
+
constructor(options?: MemoryTokenStoreOptions);
|
|
6
11
|
private indexToken;
|
|
7
12
|
private unindexToken;
|
|
8
13
|
private removeByRefreshToken;
|
|
@@ -20,4 +25,5 @@ export declare class MemoryTokenStore extends TokenStore {
|
|
|
20
25
|
includeExpired?: boolean;
|
|
21
26
|
}): Promise<Token[]>;
|
|
22
27
|
close(): Promise<void>;
|
|
28
|
+
private enforceCapacity;
|
|
23
29
|
}
|
package/dist/esm/token/memory.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TokenStore } from './base.js';
|
|
2
2
|
function comparableUserId(value) {
|
|
3
|
-
if (value === undefined
|
|
3
|
+
if (value === undefined) {
|
|
4
4
|
return undefined;
|
|
5
5
|
}
|
|
6
6
|
return String(value);
|
|
@@ -45,10 +45,14 @@ function matchesQuery(record, query, includeExpired) {
|
|
|
45
45
|
return true;
|
|
46
46
|
}
|
|
47
47
|
export class MemoryTokenStore extends TokenStore {
|
|
48
|
-
constructor() {
|
|
49
|
-
super(
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
super();
|
|
50
50
|
this.tokens = new Map();
|
|
51
51
|
this.tokensByUser = new Map();
|
|
52
|
+
this.maxTokens =
|
|
53
|
+
typeof options.maxTokens === 'number' && Number.isFinite(options.maxTokens) && options.maxTokens > 0
|
|
54
|
+
? Math.floor(options.maxTokens)
|
|
55
|
+
: undefined;
|
|
52
56
|
}
|
|
53
57
|
indexToken(token) {
|
|
54
58
|
const userId = comparableUserId(token.userId);
|
|
@@ -115,6 +119,7 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
115
119
|
this.removeByRefreshToken(stored.refreshToken);
|
|
116
120
|
this.tokens.set(stored.refreshToken, stored);
|
|
117
121
|
this.indexToken(stored);
|
|
122
|
+
this.enforceCapacity();
|
|
118
123
|
}
|
|
119
124
|
async get(query, opts) {
|
|
120
125
|
if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
|
|
@@ -162,8 +167,12 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
162
167
|
merged[key] = value;
|
|
163
168
|
}
|
|
164
169
|
};
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
if (params.accessToken !== undefined && params.accessToken !== null) {
|
|
171
|
+
merged.accessToken = params.accessToken;
|
|
172
|
+
}
|
|
173
|
+
if (params.expires !== undefined && params.expires !== null) {
|
|
174
|
+
merged.expires = params.expires;
|
|
175
|
+
}
|
|
167
176
|
maybeAssign('scope');
|
|
168
177
|
maybeAssign('label');
|
|
169
178
|
maybeAssign('domain');
|
|
@@ -174,8 +183,12 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
174
183
|
maybeAssign('os');
|
|
175
184
|
maybeAssign('refreshTtlSeconds');
|
|
176
185
|
maybeAssign('loginType');
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
187
|
+
merged.issuedAt = params.issuedAt;
|
|
188
|
+
}
|
|
189
|
+
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
190
|
+
merged.lastSeenAt = params.lastSeenAt;
|
|
191
|
+
}
|
|
179
192
|
maybeAssign('sessionCookie');
|
|
180
193
|
const normalized = this.normalizeToken(merged);
|
|
181
194
|
const previousUserId = token.userId;
|
|
@@ -207,4 +220,16 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
207
220
|
async close() {
|
|
208
221
|
return;
|
|
209
222
|
}
|
|
223
|
+
enforceCapacity() {
|
|
224
|
+
if (!this.maxTokens) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
while (this.tokens.size > this.maxTokens) {
|
|
228
|
+
const oldestRefresh = this.tokens.keys().next().value;
|
|
229
|
+
if (!oldestRefresh) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.removeByRefreshToken(oldestRefresh);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
210
235
|
}
|
|
@@ -52,10 +52,7 @@ export declare class SequelizeTokenStore extends TokenStore {
|
|
|
52
52
|
close(): Promise<void>;
|
|
53
53
|
private normalizeUserId;
|
|
54
54
|
private resolveRealUserId;
|
|
55
|
-
private encodeStringArray;
|
|
56
|
-
private decodeStringArray;
|
|
57
55
|
private encodeScope;
|
|
58
|
-
private decodeScope;
|
|
59
56
|
private toTokenRecord;
|
|
60
57
|
}
|
|
61
58
|
export {};
|