@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,20 +1,13 @@
|
|
|
1
|
+
import { normalizeComparableUserId } from '../auth-api/user-id.js';
|
|
1
2
|
import { PasskeyStore } from './base.js';
|
|
2
3
|
function encodeCredentialId(value) {
|
|
3
4
|
return Buffer.isBuffer(value) ? value.toString('base64') : value;
|
|
4
5
|
}
|
|
5
|
-
function normalizeUserId(identifier) {
|
|
6
|
-
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
7
|
-
return identifier;
|
|
8
|
-
}
|
|
9
|
-
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
10
|
-
return Number(identifier);
|
|
11
|
-
}
|
|
12
|
-
return identifier;
|
|
13
|
-
}
|
|
14
6
|
function cloneCredential(record) {
|
|
15
7
|
return {
|
|
16
8
|
...record,
|
|
17
9
|
credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
|
|
10
|
+
publicKey: Buffer.from(record.publicKey),
|
|
18
11
|
transports: record.transports ? [...record.transports] : undefined
|
|
19
12
|
};
|
|
20
13
|
}
|
|
@@ -24,16 +17,32 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
24
17
|
this.credentials = new Map();
|
|
25
18
|
this.challenges = new Map();
|
|
26
19
|
this.resolveUserFn = options.resolveUser;
|
|
20
|
+
this.maxCredentials =
|
|
21
|
+
typeof options.maxCredentials === 'number' &&
|
|
22
|
+
Number.isFinite(options.maxCredentials) &&
|
|
23
|
+
options.maxCredentials > 0
|
|
24
|
+
? Math.floor(options.maxCredentials)
|
|
25
|
+
: undefined;
|
|
26
|
+
this.maxChallenges =
|
|
27
|
+
typeof options.maxChallenges === 'number' &&
|
|
28
|
+
Number.isFinite(options.maxChallenges) &&
|
|
29
|
+
options.maxChallenges > 0
|
|
30
|
+
? Math.floor(options.maxChallenges)
|
|
31
|
+
: undefined;
|
|
27
32
|
}
|
|
28
33
|
async resolveUser(params) {
|
|
29
34
|
return this.resolveUserFn(params);
|
|
30
35
|
}
|
|
31
36
|
async listUserCredentials(userId) {
|
|
32
|
-
const normalizedUserId =
|
|
37
|
+
const normalizedUserId = normalizeComparableUserId(userId);
|
|
33
38
|
return [...this.credentials.values()]
|
|
34
|
-
.filter((record) =>
|
|
39
|
+
.filter((record) => normalizeComparableUserId(record.userId) === normalizedUserId)
|
|
35
40
|
.map((record) => cloneCredential(record));
|
|
36
41
|
}
|
|
42
|
+
async deleteCredential(credentialId) {
|
|
43
|
+
const key = encodeCredentialId(credentialId);
|
|
44
|
+
return this.credentials.delete(key);
|
|
45
|
+
}
|
|
37
46
|
async findCredentialById(credentialId) {
|
|
38
47
|
const record = this.credentials.get(encodeCredentialId(credentialId));
|
|
39
48
|
return record ? cloneCredential(record) : null;
|
|
@@ -41,10 +50,11 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
41
50
|
async saveCredential(record) {
|
|
42
51
|
this.credentials.set(encodeCredentialId(record.credentialId), {
|
|
43
52
|
...record,
|
|
44
|
-
userId:
|
|
53
|
+
userId: normalizeComparableUserId(record.userId),
|
|
45
54
|
credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
|
|
46
55
|
transports: record.transports ? [...record.transports] : undefined
|
|
47
56
|
});
|
|
57
|
+
this.enforceCredentialCapacity();
|
|
48
58
|
}
|
|
49
59
|
async updateCredentialCounter(credentialId, counter) {
|
|
50
60
|
const key = encodeCredentialId(credentialId);
|
|
@@ -55,10 +65,17 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
55
65
|
}
|
|
56
66
|
async saveChallenge(record) {
|
|
57
67
|
this.challenges.set(record.challenge, {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
challenge: record.challenge,
|
|
69
|
+
action: record.action,
|
|
70
|
+
userId: record.userId !== undefined ? normalizeComparableUserId(record.userId) : undefined,
|
|
71
|
+
login: record.login ?? undefined,
|
|
72
|
+
expiresAt: record.expiresAt
|
|
61
73
|
});
|
|
74
|
+
this.enforceChallengeCapacity();
|
|
75
|
+
}
|
|
76
|
+
async getChallenge(challenge) {
|
|
77
|
+
const record = this.challenges.get(challenge);
|
|
78
|
+
return record ? { ...record } : null;
|
|
62
79
|
}
|
|
63
80
|
async consumeChallenge(challenge) {
|
|
64
81
|
const record = this.challenges.get(challenge);
|
|
@@ -66,7 +83,7 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
66
83
|
return null;
|
|
67
84
|
}
|
|
68
85
|
this.challenges.delete(challenge);
|
|
69
|
-
return { ...record
|
|
86
|
+
return { ...record };
|
|
70
87
|
}
|
|
71
88
|
async cleanupChallenges(now) {
|
|
72
89
|
for (const [challenge, record] of this.challenges.entries()) {
|
|
@@ -75,4 +92,28 @@ export class MemoryPasskeyStore extends PasskeyStore {
|
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
}
|
|
95
|
+
enforceCredentialCapacity() {
|
|
96
|
+
if (!this.maxCredentials) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
while (this.credentials.size > this.maxCredentials) {
|
|
100
|
+
const oldest = this.credentials.keys().next().value;
|
|
101
|
+
if (!oldest) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.credentials.delete(oldest);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
enforceChallengeCapacity() {
|
|
108
|
+
if (!this.maxChallenges) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
while (this.challenges.size > this.maxChallenges) {
|
|
112
|
+
const oldest = this.challenges.keys().next().value;
|
|
113
|
+
if (!oldest) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.challenges.delete(oldest);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
78
119
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
-
import type { PasskeyChallengeMetadata } from './service.js';
|
|
3
2
|
export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
|
|
4
3
|
credentialId: Buffer;
|
|
5
4
|
userId: number;
|
|
@@ -8,6 +7,13 @@ export declare class PasskeyCredentialModel extends Model<InferAttributes<Passke
|
|
|
8
7
|
transports: string[] | null;
|
|
9
8
|
backedUp: boolean;
|
|
10
9
|
deviceType: string;
|
|
10
|
+
label: string | null;
|
|
11
|
+
createdDomain: string | null;
|
|
12
|
+
createdUserAgent: string | null;
|
|
13
|
+
createdBrowser: string | null;
|
|
14
|
+
createdOs: string | null;
|
|
15
|
+
createdDevice: string | null;
|
|
16
|
+
createdIp: string | null;
|
|
11
17
|
createdAt?: Date;
|
|
12
18
|
updatedAt?: Date;
|
|
13
19
|
}
|
|
@@ -16,10 +22,13 @@ export declare class PasskeyChallengeModel extends Model<InferAttributes<Passkey
|
|
|
16
22
|
action: 'register' | 'authenticate';
|
|
17
23
|
userId: number | null;
|
|
18
24
|
login: string | null;
|
|
19
|
-
metadata: PasskeyChallengeMetadata | null;
|
|
20
25
|
expiresAt: Date;
|
|
21
26
|
createdAt?: Date;
|
|
22
27
|
updatedAt?: Date;
|
|
23
28
|
}
|
|
24
|
-
export declare function initPasskeyCredentialModel(sequelize: Sequelize
|
|
25
|
-
|
|
29
|
+
export declare function initPasskeyCredentialModel(sequelize: Sequelize, options?: {
|
|
30
|
+
tablePrefix?: string;
|
|
31
|
+
}): ModelStatic<PasskeyCredentialModel>;
|
|
32
|
+
export declare function initPasskeyChallengeModel(sequelize: Sequelize, options?: {
|
|
33
|
+
tablePrefix?: string;
|
|
34
|
+
}): ModelStatic<PasskeyChallengeModel>;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { DataTypes, Model } from 'sequelize';
|
|
2
|
-
|
|
3
|
-
function integerIdType(sequelize) {
|
|
4
|
-
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
5
|
-
}
|
|
2
|
+
import { applyTablePrefix, integerIdType } from '../sequelize-utils.js';
|
|
6
3
|
export class PasskeyCredentialModel extends Model {
|
|
7
4
|
}
|
|
8
5
|
export class PasskeyChallengeModel extends Model {
|
|
9
6
|
}
|
|
10
|
-
export function initPasskeyCredentialModel(sequelize) {
|
|
7
|
+
export function initPasskeyCredentialModel(sequelize, options = {}) {
|
|
11
8
|
const idType = integerIdType(sequelize);
|
|
12
9
|
return PasskeyCredentialModel.init({
|
|
13
10
|
credentialId: {
|
|
@@ -60,15 +57,49 @@ export function initPasskeyCredentialModel(sequelize) {
|
|
|
60
57
|
type: DataTypes.STRING(32),
|
|
61
58
|
allowNull: false,
|
|
62
59
|
defaultValue: 'multiDevice'
|
|
60
|
+
},
|
|
61
|
+
label: {
|
|
62
|
+
type: DataTypes.STRING(120),
|
|
63
|
+
allowNull: true
|
|
64
|
+
},
|
|
65
|
+
createdDomain: {
|
|
66
|
+
field: 'created_domain',
|
|
67
|
+
type: DataTypes.STRING(255),
|
|
68
|
+
allowNull: true
|
|
69
|
+
},
|
|
70
|
+
createdUserAgent: {
|
|
71
|
+
field: 'created_user_agent',
|
|
72
|
+
type: DataTypes.TEXT,
|
|
73
|
+
allowNull: true
|
|
74
|
+
},
|
|
75
|
+
createdBrowser: {
|
|
76
|
+
field: 'created_browser',
|
|
77
|
+
type: DataTypes.STRING(120),
|
|
78
|
+
allowNull: true
|
|
79
|
+
},
|
|
80
|
+
createdOs: {
|
|
81
|
+
field: 'created_os',
|
|
82
|
+
type: DataTypes.STRING(120),
|
|
83
|
+
allowNull: true
|
|
84
|
+
},
|
|
85
|
+
createdDevice: {
|
|
86
|
+
field: 'created_device',
|
|
87
|
+
type: DataTypes.STRING(120),
|
|
88
|
+
allowNull: true
|
|
89
|
+
},
|
|
90
|
+
createdIp: {
|
|
91
|
+
field: 'created_ip',
|
|
92
|
+
type: DataTypes.STRING(45),
|
|
93
|
+
allowNull: true
|
|
63
94
|
}
|
|
64
95
|
}, {
|
|
65
96
|
sequelize,
|
|
66
|
-
tableName: 'passkey_credentials',
|
|
97
|
+
tableName: applyTablePrefix(options.tablePrefix, 'passkey_credentials'),
|
|
67
98
|
timestamps: true,
|
|
68
99
|
underscored: true
|
|
69
100
|
});
|
|
70
101
|
}
|
|
71
|
-
export function initPasskeyChallengeModel(sequelize) {
|
|
102
|
+
export function initPasskeyChallengeModel(sequelize, options = {}) {
|
|
72
103
|
const idType = integerIdType(sequelize);
|
|
73
104
|
return PasskeyChallengeModel.init({
|
|
74
105
|
challenge: {
|
|
@@ -89,10 +120,6 @@ export function initPasskeyChallengeModel(sequelize) {
|
|
|
89
120
|
type: DataTypes.STRING(128),
|
|
90
121
|
allowNull: true
|
|
91
122
|
},
|
|
92
|
-
metadata: {
|
|
93
|
-
type: DataTypes.JSON,
|
|
94
|
-
allowNull: true
|
|
95
|
-
},
|
|
96
123
|
expiresAt: {
|
|
97
124
|
field: 'expires_at',
|
|
98
125
|
type: DataTypes.DATE,
|
|
@@ -100,7 +127,7 @@ export function initPasskeyChallengeModel(sequelize) {
|
|
|
100
127
|
}
|
|
101
128
|
}, {
|
|
102
129
|
sequelize,
|
|
103
|
-
tableName: 'passkey_challenges',
|
|
130
|
+
tableName: applyTablePrefix(options.tablePrefix, 'passkey_challenges'),
|
|
104
131
|
timestamps: true,
|
|
105
132
|
underscored: true,
|
|
106
133
|
indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
|
|
@@ -1,34 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
2
|
import { PasskeyStore } from './base.js';
|
|
3
|
+
import { PasskeyChallengeModel, PasskeyCredentialModel } from './models.js';
|
|
3
4
|
import type { PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
|
|
4
5
|
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
5
|
-
declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
|
|
6
|
-
credentialId: Buffer;
|
|
7
|
-
userId: number;
|
|
8
|
-
publicKey: Buffer;
|
|
9
|
-
counter: number;
|
|
10
|
-
transports: string[] | null;
|
|
11
|
-
backedUp: boolean;
|
|
12
|
-
deviceType: string;
|
|
13
|
-
createdAt?: Date;
|
|
14
|
-
updatedAt?: Date;
|
|
15
|
-
}
|
|
16
|
-
declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallengeModel>, InferCreationAttributes<PasskeyChallengeModel>> {
|
|
17
|
-
challenge: string;
|
|
18
|
-
action: 'register' | 'authenticate';
|
|
19
|
-
userId: number | null;
|
|
20
|
-
login: string | null;
|
|
21
|
-
metadata: Record<string, unknown> | null;
|
|
22
|
-
expiresAt: Date;
|
|
23
|
-
createdAt?: Date;
|
|
24
|
-
updatedAt?: Date;
|
|
25
|
-
}
|
|
26
6
|
export interface SequelizePasskeyStoreOptions {
|
|
27
7
|
sequelize: Sequelize;
|
|
8
|
+
tablePrefix?: string;
|
|
28
9
|
credentialModel?: ModelStatic<PasskeyCredentialModel>;
|
|
29
10
|
challengeModel?: ModelStatic<PasskeyChallengeModel>;
|
|
30
|
-
credentialModelFactory?: (sequelize: Sequelize
|
|
31
|
-
|
|
11
|
+
credentialModelFactory?: (sequelize: Sequelize, options?: {
|
|
12
|
+
tablePrefix?: string;
|
|
13
|
+
}) => ModelStatic<PasskeyCredentialModel>;
|
|
14
|
+
challengeModelFactory?: (sequelize: Sequelize, options?: {
|
|
15
|
+
tablePrefix?: string;
|
|
16
|
+
}) => ModelStatic<PasskeyChallengeModel>;
|
|
32
17
|
resolveUser: (params: {
|
|
33
18
|
userId?: AuthIdentifier;
|
|
34
19
|
login?: string;
|
|
@@ -44,11 +29,14 @@ export declare class SequelizePasskeyStore extends PasskeyStore {
|
|
|
44
29
|
login?: string;
|
|
45
30
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
46
31
|
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
32
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
47
33
|
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
48
34
|
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
49
35
|
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
50
36
|
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
37
|
+
getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
51
38
|
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
52
39
|
cleanupChallenges(now: Date): Promise<void>;
|
|
40
|
+
private toChallengeRecord;
|
|
41
|
+
private toStoredCredential;
|
|
53
42
|
}
|
|
54
|
-
export {};
|
|
@@ -1,124 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Op, Transaction } from 'sequelize';
|
|
2
|
+
import { normalizeNumericUserId } from '../auth-api/user-id.js';
|
|
2
3
|
import { PasskeyStore } from './base.js';
|
|
3
|
-
|
|
4
|
-
function integerIdType(sequelize) {
|
|
5
|
-
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
6
|
-
}
|
|
4
|
+
import { initPasskeyChallengeModel, initPasskeyCredentialModel } from './models.js';
|
|
7
5
|
function encodeCredentialId(value) {
|
|
8
6
|
return Buffer.isBuffer(value) ? value.toString('base64') : value;
|
|
9
7
|
}
|
|
10
|
-
function normalizeUserId(identifier) {
|
|
11
|
-
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
12
|
-
return identifier;
|
|
13
|
-
}
|
|
14
|
-
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
15
|
-
return Number(identifier);
|
|
16
|
-
}
|
|
17
|
-
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
18
|
-
}
|
|
19
|
-
class PasskeyCredentialModel extends Model {
|
|
20
|
-
}
|
|
21
|
-
class PasskeyChallengeModel extends Model {
|
|
22
|
-
}
|
|
23
|
-
function initPasskeyCredentialModel(sequelize) {
|
|
24
|
-
const idType = integerIdType(sequelize);
|
|
25
|
-
return PasskeyCredentialModel.init({
|
|
26
|
-
credentialId: {
|
|
27
|
-
field: 'credential_id',
|
|
28
|
-
type: DataTypes.STRING(768),
|
|
29
|
-
primaryKey: true,
|
|
30
|
-
allowNull: false,
|
|
31
|
-
get() {
|
|
32
|
-
const raw = this.getDataValue('credentialId');
|
|
33
|
-
if (!raw) {
|
|
34
|
-
return raw;
|
|
35
|
-
}
|
|
36
|
-
if (Buffer.isBuffer(raw)) {
|
|
37
|
-
return raw;
|
|
38
|
-
}
|
|
39
|
-
return Buffer.from(raw, 'base64');
|
|
40
|
-
},
|
|
41
|
-
set(value) {
|
|
42
|
-
const encoded = typeof value === 'string' ? value : value.toString('base64');
|
|
43
|
-
this.setDataValue('credentialId', encoded);
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
userId: {
|
|
47
|
-
field: 'user_id',
|
|
48
|
-
type: idType,
|
|
49
|
-
allowNull: false
|
|
50
|
-
},
|
|
51
|
-
publicKey: {
|
|
52
|
-
field: 'public_key',
|
|
53
|
-
type: DataTypes.BLOB,
|
|
54
|
-
allowNull: false
|
|
55
|
-
},
|
|
56
|
-
counter: {
|
|
57
|
-
type: DataTypes.INTEGER,
|
|
58
|
-
allowNull: false,
|
|
59
|
-
defaultValue: 0
|
|
60
|
-
},
|
|
61
|
-
transports: {
|
|
62
|
-
type: DataTypes.JSON,
|
|
63
|
-
allowNull: true
|
|
64
|
-
},
|
|
65
|
-
backedUp: {
|
|
66
|
-
field: 'backed_up',
|
|
67
|
-
type: DataTypes.BOOLEAN,
|
|
68
|
-
allowNull: false,
|
|
69
|
-
defaultValue: false
|
|
70
|
-
},
|
|
71
|
-
deviceType: {
|
|
72
|
-
field: 'device_type',
|
|
73
|
-
type: DataTypes.STRING(32),
|
|
74
|
-
allowNull: false,
|
|
75
|
-
defaultValue: 'multiDevice'
|
|
76
|
-
}
|
|
77
|
-
}, {
|
|
78
|
-
sequelize,
|
|
79
|
-
tableName: 'passkey_credentials',
|
|
80
|
-
timestamps: true,
|
|
81
|
-
underscored: true
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
function initPasskeyChallengeModel(sequelize) {
|
|
85
|
-
const idType = integerIdType(sequelize);
|
|
86
|
-
return PasskeyChallengeModel.init({
|
|
87
|
-
challenge: {
|
|
88
|
-
type: DataTypes.STRING(255),
|
|
89
|
-
primaryKey: true,
|
|
90
|
-
allowNull: false
|
|
91
|
-
},
|
|
92
|
-
action: {
|
|
93
|
-
type: DataTypes.STRING(16),
|
|
94
|
-
allowNull: false
|
|
95
|
-
},
|
|
96
|
-
userId: {
|
|
97
|
-
field: 'user_id',
|
|
98
|
-
type: idType,
|
|
99
|
-
allowNull: true
|
|
100
|
-
},
|
|
101
|
-
login: {
|
|
102
|
-
type: DataTypes.STRING(128),
|
|
103
|
-
allowNull: true
|
|
104
|
-
},
|
|
105
|
-
metadata: {
|
|
106
|
-
type: DataTypes.JSON,
|
|
107
|
-
allowNull: true
|
|
108
|
-
},
|
|
109
|
-
expiresAt: {
|
|
110
|
-
field: 'expires_at',
|
|
111
|
-
type: DataTypes.DATE,
|
|
112
|
-
allowNull: false
|
|
113
|
-
}
|
|
114
|
-
}, {
|
|
115
|
-
sequelize,
|
|
116
|
-
tableName: 'passkey_challenges',
|
|
117
|
-
timestamps: true,
|
|
118
|
-
underscored: true,
|
|
119
|
-
indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
8
|
export class SequelizePasskeyStore extends PasskeyStore {
|
|
123
9
|
constructor(options) {
|
|
124
10
|
super();
|
|
@@ -128,49 +14,47 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
128
14
|
this.resolveUserFn = options.resolveUser;
|
|
129
15
|
this.credentials =
|
|
130
16
|
options.credentialModel ??
|
|
131
|
-
(options.credentialModelFactory ?? initPasskeyCredentialModel)(options.sequelize
|
|
17
|
+
(options.credentialModelFactory ?? initPasskeyCredentialModel)(options.sequelize, {
|
|
18
|
+
tablePrefix: options.tablePrefix
|
|
19
|
+
});
|
|
132
20
|
this.challenges =
|
|
133
|
-
options.challengeModel ??
|
|
21
|
+
options.challengeModel ??
|
|
22
|
+
(options.challengeModelFactory ?? initPasskeyChallengeModel)(options.sequelize, {
|
|
23
|
+
tablePrefix: options.tablePrefix
|
|
24
|
+
});
|
|
134
25
|
}
|
|
135
26
|
async resolveUser(params) {
|
|
136
27
|
return this.resolveUserFn(params);
|
|
137
28
|
}
|
|
138
29
|
async listUserCredentials(userId) {
|
|
139
|
-
const models = await this.credentials.findAll({ where: { userId:
|
|
140
|
-
return models.map((model) => (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
backedUp: model.backedUp,
|
|
147
|
-
deviceType: model.deviceType
|
|
148
|
-
}));
|
|
30
|
+
const models = await this.credentials.findAll({ where: { userId: normalizeNumericUserId(userId) } });
|
|
31
|
+
return models.map((model) => this.toStoredCredential(model));
|
|
32
|
+
}
|
|
33
|
+
async deleteCredential(credentialId) {
|
|
34
|
+
const encoded = Buffer.isBuffer(credentialId) ? credentialId.toString('base64') : credentialId;
|
|
35
|
+
const deleted = await this.credentials.destroy({ where: { credentialId: encoded } });
|
|
36
|
+
return deleted > 0;
|
|
149
37
|
}
|
|
150
38
|
async findCredentialById(credentialId) {
|
|
151
39
|
const model = await this.credentials.findByPk(encodeCredentialId(credentialId));
|
|
152
|
-
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
userId: model.userId,
|
|
157
|
-
credentialId: model.credentialId,
|
|
158
|
-
publicKey: model.publicKey,
|
|
159
|
-
counter: model.counter,
|
|
160
|
-
transports: (model.transports ?? undefined),
|
|
161
|
-
backedUp: model.backedUp,
|
|
162
|
-
deviceType: model.deviceType
|
|
163
|
-
};
|
|
40
|
+
return model ? this.toStoredCredential(model) : null;
|
|
164
41
|
}
|
|
165
42
|
async saveCredential(record) {
|
|
166
43
|
await this.credentials.upsert({
|
|
167
44
|
credentialId: record.credentialId,
|
|
168
|
-
userId:
|
|
45
|
+
userId: normalizeNumericUserId(record.userId),
|
|
169
46
|
publicKey: record.publicKey,
|
|
170
47
|
counter: record.counter,
|
|
171
48
|
transports: record.transports ?? null,
|
|
172
49
|
backedUp: record.backedUp,
|
|
173
|
-
deviceType: record.deviceType
|
|
50
|
+
deviceType: record.deviceType,
|
|
51
|
+
label: record.label ?? null,
|
|
52
|
+
createdDomain: record.createdDomain ?? null,
|
|
53
|
+
createdUserAgent: record.createdUserAgent ?? null,
|
|
54
|
+
createdBrowser: record.createdBrowser ?? null,
|
|
55
|
+
createdOs: record.createdOs ?? null,
|
|
56
|
+
createdDevice: record.createdDevice ?? null,
|
|
57
|
+
createdIp: record.createdIp ?? null
|
|
174
58
|
});
|
|
175
59
|
}
|
|
176
60
|
async updateCredentialCounter(credentialId, counter) {
|
|
@@ -180,28 +64,59 @@ export class SequelizePasskeyStore extends PasskeyStore {
|
|
|
180
64
|
await this.challenges.upsert({
|
|
181
65
|
challenge: record.challenge,
|
|
182
66
|
action: record.action,
|
|
183
|
-
userId: record.userId !== undefined ?
|
|
67
|
+
userId: record.userId !== undefined ? normalizeNumericUserId(record.userId) : null,
|
|
184
68
|
login: record.login ?? null,
|
|
185
|
-
metadata: (record.metadata ?? {}),
|
|
186
69
|
expiresAt: record.expiresAt
|
|
187
70
|
});
|
|
188
71
|
}
|
|
189
|
-
async
|
|
72
|
+
async getChallenge(challenge) {
|
|
190
73
|
const model = await this.challenges.findByPk(challenge);
|
|
191
|
-
|
|
192
|
-
|
|
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');
|
|
193
80
|
}
|
|
194
|
-
|
|
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) {
|
|
195
94
|
return {
|
|
196
95
|
challenge: model.challenge,
|
|
197
96
|
action: model.action,
|
|
198
|
-
userId: model.userId
|
|
97
|
+
userId: model.userId !== null ? String(model.userId) : undefined,
|
|
199
98
|
login: model.login ?? undefined,
|
|
200
|
-
expiresAt: model.expiresAt
|
|
201
|
-
metadata: model.metadata ?? {}
|
|
99
|
+
expiresAt: model.expiresAt
|
|
202
100
|
};
|
|
203
101
|
}
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
};
|
|
206
121
|
}
|
|
207
122
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig } from './types.js';
|
|
2
|
-
|
|
1
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, StoredPasskeyCredential } from './types.js';
|
|
2
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter, StoredPasskeyCredential } from './types.js';
|
|
3
4
|
type Logger = Pick<typeof console, 'error' | 'warn'>;
|
|
4
5
|
export declare class PasskeyService {
|
|
5
6
|
private readonly config;
|
|
6
7
|
private readonly adapter;
|
|
7
8
|
private readonly logger;
|
|
8
9
|
constructor(config: PasskeyServiceConfig, adapter: PasskeyStorageAdapter, logger?: Logger);
|
|
10
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
11
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
9
12
|
createChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
10
13
|
verifyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
11
14
|
private createRegistrationChallenge;
|
|
@@ -14,4 +17,5 @@ export declare class PasskeyService {
|
|
|
14
17
|
private verifyAuthentication;
|
|
15
18
|
private requireUser;
|
|
16
19
|
private createExpiry;
|
|
20
|
+
private requireUserVerification;
|
|
17
21
|
}
|