@technomoron/api-server-base 1.1.13 → 2.0.0-beta.2
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 +181 -74
- package/dist/cjs/api-server-base.d.ts +66 -29
- package/dist/cjs/auth-api/auth-module.d.ts +96 -0
- package/dist/cjs/auth-api/auth-module.js +1032 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +55 -0
- package/dist/cjs/auth-api/compat-auth-storage.js +116 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +66 -0
- package/dist/cjs/auth-api/mem-auth-store.js +135 -0
- package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +75 -0
- package/dist/cjs/auth-api/sql-auth-store.js +166 -0
- package/dist/cjs/auth-api/storage.d.ts +36 -0
- package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +2 -2
- package/dist/cjs/auth-api/types.d.ts +29 -0
- package/dist/cjs/auth-api/types.js +2 -0
- package/dist/cjs/index.cjs +41 -7
- package/dist/cjs/index.d.ts +29 -5
- package/dist/cjs/oauth/base.d.ts +10 -0
- package/dist/cjs/oauth/base.js +6 -0
- package/dist/cjs/oauth/memory.d.ts +16 -0
- package/dist/cjs/oauth/memory.js +99 -0
- package/dist/cjs/oauth/models.d.ts +45 -0
- package/dist/cjs/oauth/models.js +58 -0
- package/dist/cjs/oauth/sequelize.d.ts +68 -0
- package/dist/cjs/oauth/sequelize.js +210 -0
- package/dist/cjs/oauth/types.d.ts +50 -0
- package/dist/cjs/oauth/types.js +3 -0
- package/dist/cjs/passkey/base.d.ts +15 -0
- package/dist/cjs/passkey/base.js +6 -0
- package/dist/cjs/passkey/memory.d.ts +26 -0
- package/dist/cjs/passkey/memory.js +82 -0
- package/dist/cjs/passkey/models.d.ts +25 -0
- package/dist/cjs/passkey/models.js +115 -0
- package/dist/cjs/passkey/sequelize.d.ts +54 -0
- package/dist/cjs/passkey/sequelize.js +211 -0
- package/dist/cjs/passkey/service.d.ts +17 -0
- package/dist/cjs/passkey/service.js +221 -0
- package/dist/cjs/passkey/types.d.ts +75 -0
- package/dist/cjs/passkey/types.js +2 -0
- package/dist/cjs/token/base.d.ts +38 -0
- package/dist/cjs/token/base.js +114 -0
- package/dist/cjs/token/memory.d.ts +19 -0
- package/dist/cjs/token/memory.js +149 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/sequelize.js +404 -0
- package/dist/cjs/token/types.d.ts +27 -0
- package/dist/cjs/token/types.js +2 -0
- package/dist/cjs/user/base.d.ts +26 -0
- package/dist/cjs/user/base.js +45 -0
- package/dist/cjs/user/memory.d.ts +35 -0
- package/dist/cjs/user/memory.js +173 -0
- package/dist/cjs/user/sequelize.d.ts +41 -0
- package/dist/cjs/user/sequelize.js +182 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/cjs/user/types.js +2 -0
- package/dist/esm/api-server-base.d.ts +66 -29
- package/dist/esm/api-server-base.js +179 -72
- package/dist/esm/auth-api/auth-module.d.ts +96 -0
- package/dist/esm/auth-api/auth-module.js +1030 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +55 -0
- package/dist/esm/auth-api/compat-auth-storage.js +112 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +66 -0
- package/dist/esm/auth-api/mem-auth-store.js +131 -0
- package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +75 -0
- package/dist/esm/auth-api/sql-auth-store.js +162 -0
- package/dist/esm/auth-api/storage.d.ts +36 -0
- package/dist/esm/{auth-storage.js → auth-api/storage.js} +2 -2
- package/dist/esm/auth-api/types.d.ts +29 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/index.d.ts +29 -5
- package/dist/esm/index.js +19 -2
- package/dist/esm/oauth/base.d.ts +10 -0
- package/dist/esm/oauth/base.js +2 -0
- package/dist/esm/oauth/memory.d.ts +16 -0
- package/dist/esm/oauth/memory.js +92 -0
- package/dist/esm/oauth/models.d.ts +45 -0
- package/dist/esm/oauth/models.js +51 -0
- package/dist/esm/oauth/sequelize.d.ts +68 -0
- package/dist/esm/oauth/sequelize.js +199 -0
- package/dist/esm/oauth/types.d.ts +50 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +15 -0
- package/dist/esm/passkey/base.js +2 -0
- package/dist/esm/passkey/memory.d.ts +26 -0
- package/dist/esm/passkey/memory.js +78 -0
- package/dist/esm/passkey/models.d.ts +25 -0
- package/dist/esm/passkey/models.js +108 -0
- package/dist/esm/passkey/sequelize.d.ts +54 -0
- package/dist/esm/passkey/sequelize.js +207 -0
- package/dist/esm/passkey/service.d.ts +17 -0
- package/dist/esm/passkey/service.js +217 -0
- package/dist/esm/passkey/types.d.ts +75 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/token/base.d.ts +38 -0
- package/dist/esm/token/base.js +107 -0
- package/dist/esm/token/memory.d.ts +19 -0
- package/dist/esm/token/memory.js +145 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +400 -0
- package/dist/esm/token/types.d.ts +27 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/user/base.d.ts +26 -0
- package/dist/esm/user/base.js +38 -0
- package/dist/esm/user/memory.d.ts +35 -0
- package/dist/esm/user/memory.js +169 -0
- package/dist/esm/user/sequelize.d.ts +41 -0
- package/dist/esm/user/sequelize.js +176 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/package.json +11 -3
- package/dist/cjs/auth-storage.d.ts +0 -133
- package/dist/esm/auth-storage.d.ts +0 -133
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PasskeyChallengeRecord, PasskeyStorageAdapter, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
|
|
2
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
export declare abstract class PasskeyStore implements PasskeyStorageAdapter {
|
|
4
|
+
abstract resolveUser(params: {
|
|
5
|
+
userId?: AuthIdentifier;
|
|
6
|
+
login?: string;
|
|
7
|
+
}): Promise<PasskeyUserDescriptor | null>;
|
|
8
|
+
abstract listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
9
|
+
abstract findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
10
|
+
abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
11
|
+
abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
12
|
+
abstract saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
13
|
+
abstract consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
14
|
+
abstract cleanupChallenges(now: Date): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PasskeyStore } from './base.js';
|
|
2
|
+
import type { PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
|
|
3
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
4
|
+
export interface MemoryPasskeyStoreOptions {
|
|
5
|
+
resolveUser: (params: {
|
|
6
|
+
userId?: AuthIdentifier;
|
|
7
|
+
login?: string;
|
|
8
|
+
}) => Promise<PasskeyUserDescriptor | null>;
|
|
9
|
+
}
|
|
10
|
+
export declare class MemoryPasskeyStore extends PasskeyStore {
|
|
11
|
+
private readonly resolveUserFn;
|
|
12
|
+
private readonly credentials;
|
|
13
|
+
private readonly challenges;
|
|
14
|
+
constructor(options: MemoryPasskeyStoreOptions);
|
|
15
|
+
resolveUser(params: {
|
|
16
|
+
userId?: AuthIdentifier;
|
|
17
|
+
login?: string;
|
|
18
|
+
}): Promise<PasskeyUserDescriptor | null>;
|
|
19
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
20
|
+
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
21
|
+
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
22
|
+
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
23
|
+
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
24
|
+
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
25
|
+
cleanupChallenges(now: Date): Promise<void>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PasskeyStore } from './base.js';
|
|
2
|
+
function encodeCredentialId(value) {
|
|
3
|
+
return Buffer.isBuffer(value) ? value.toString('base64') : value;
|
|
4
|
+
}
|
|
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
|
+
function cloneCredential(record) {
|
|
15
|
+
return {
|
|
16
|
+
...record,
|
|
17
|
+
credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
|
|
18
|
+
transports: record.transports ? [...record.transports] : undefined
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export class MemoryPasskeyStore extends PasskeyStore {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super();
|
|
24
|
+
this.credentials = new Map();
|
|
25
|
+
this.challenges = new Map();
|
|
26
|
+
this.resolveUserFn = options.resolveUser;
|
|
27
|
+
}
|
|
28
|
+
async resolveUser(params) {
|
|
29
|
+
return this.resolveUserFn(params);
|
|
30
|
+
}
|
|
31
|
+
async listUserCredentials(userId) {
|
|
32
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
33
|
+
return [...this.credentials.values()]
|
|
34
|
+
.filter((record) => normalizeUserId(record.userId) === normalizedUserId)
|
|
35
|
+
.map((record) => cloneCredential(record));
|
|
36
|
+
}
|
|
37
|
+
async findCredentialById(credentialId) {
|
|
38
|
+
const record = this.credentials.get(encodeCredentialId(credentialId));
|
|
39
|
+
return record ? cloneCredential(record) : null;
|
|
40
|
+
}
|
|
41
|
+
async saveCredential(record) {
|
|
42
|
+
this.credentials.set(encodeCredentialId(record.credentialId), {
|
|
43
|
+
...record,
|
|
44
|
+
userId: normalizeUserId(record.userId),
|
|
45
|
+
credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
|
|
46
|
+
transports: record.transports ? [...record.transports] : undefined
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async updateCredentialCounter(credentialId, counter) {
|
|
50
|
+
const key = encodeCredentialId(credentialId);
|
|
51
|
+
const existing = this.credentials.get(key);
|
|
52
|
+
if (existing) {
|
|
53
|
+
existing.counter = counter;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async saveChallenge(record) {
|
|
57
|
+
this.challenges.set(record.challenge, {
|
|
58
|
+
...record,
|
|
59
|
+
userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
|
|
60
|
+
metadata: record.metadata ? { ...record.metadata } : {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async consumeChallenge(challenge) {
|
|
64
|
+
const record = this.challenges.get(challenge);
|
|
65
|
+
if (!record) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
this.challenges.delete(challenge);
|
|
69
|
+
return { ...record, metadata: record.metadata ? { ...record.metadata } : {} };
|
|
70
|
+
}
|
|
71
|
+
async cleanupChallenges(now) {
|
|
72
|
+
for (const [challenge, record] of this.challenges.entries()) {
|
|
73
|
+
if (record.expiresAt && new Date(record.expiresAt) <= now) {
|
|
74
|
+
this.challenges.delete(challenge);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
+
import type { PasskeyChallengeMetadata } from './service.js';
|
|
3
|
+
export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
|
|
4
|
+
credentialId: Buffer;
|
|
5
|
+
userId: number;
|
|
6
|
+
publicKey: Buffer;
|
|
7
|
+
counter: number;
|
|
8
|
+
transports: string[] | null;
|
|
9
|
+
backedUp: boolean;
|
|
10
|
+
deviceType: string;
|
|
11
|
+
createdAt?: Date;
|
|
12
|
+
updatedAt?: Date;
|
|
13
|
+
}
|
|
14
|
+
export declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallengeModel>, InferCreationAttributes<PasskeyChallengeModel>> {
|
|
15
|
+
challenge: string;
|
|
16
|
+
action: 'register' | 'authenticate';
|
|
17
|
+
userId: number | null;
|
|
18
|
+
login: string | null;
|
|
19
|
+
metadata: PasskeyChallengeMetadata | null;
|
|
20
|
+
expiresAt: Date;
|
|
21
|
+
createdAt?: Date;
|
|
22
|
+
updatedAt?: Date;
|
|
23
|
+
}
|
|
24
|
+
export declare function initPasskeyCredentialModel(sequelize: Sequelize): ModelStatic<PasskeyCredentialModel>;
|
|
25
|
+
export declare function initPasskeyChallengeModel(sequelize: Sequelize): ModelStatic<PasskeyChallengeModel>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
3
|
+
function integerIdType(sequelize) {
|
|
4
|
+
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
5
|
+
}
|
|
6
|
+
export class PasskeyCredentialModel extends Model {
|
|
7
|
+
}
|
|
8
|
+
export class PasskeyChallengeModel extends Model {
|
|
9
|
+
}
|
|
10
|
+
export function initPasskeyCredentialModel(sequelize) {
|
|
11
|
+
const idType = integerIdType(sequelize);
|
|
12
|
+
return PasskeyCredentialModel.init({
|
|
13
|
+
credentialId: {
|
|
14
|
+
field: 'credential_id',
|
|
15
|
+
type: DataTypes.STRING(768),
|
|
16
|
+
primaryKey: true,
|
|
17
|
+
allowNull: false,
|
|
18
|
+
get() {
|
|
19
|
+
const raw = this.getDataValue('credentialId');
|
|
20
|
+
if (!raw) {
|
|
21
|
+
return raw;
|
|
22
|
+
}
|
|
23
|
+
if (Buffer.isBuffer(raw)) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
return Buffer.from(raw, 'base64');
|
|
27
|
+
},
|
|
28
|
+
set(value) {
|
|
29
|
+
const encoded = typeof value === 'string' ? value : value.toString('base64');
|
|
30
|
+
this.setDataValue('credentialId', encoded);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
userId: {
|
|
34
|
+
field: 'user_id',
|
|
35
|
+
type: idType,
|
|
36
|
+
allowNull: false
|
|
37
|
+
},
|
|
38
|
+
publicKey: {
|
|
39
|
+
field: 'public_key',
|
|
40
|
+
type: DataTypes.BLOB,
|
|
41
|
+
allowNull: false
|
|
42
|
+
},
|
|
43
|
+
counter: {
|
|
44
|
+
type: DataTypes.INTEGER,
|
|
45
|
+
allowNull: false,
|
|
46
|
+
defaultValue: 0
|
|
47
|
+
},
|
|
48
|
+
transports: {
|
|
49
|
+
type: DataTypes.JSON,
|
|
50
|
+
allowNull: true
|
|
51
|
+
},
|
|
52
|
+
backedUp: {
|
|
53
|
+
field: 'backed_up',
|
|
54
|
+
type: DataTypes.BOOLEAN,
|
|
55
|
+
allowNull: false,
|
|
56
|
+
defaultValue: false
|
|
57
|
+
},
|
|
58
|
+
deviceType: {
|
|
59
|
+
field: 'device_type',
|
|
60
|
+
type: DataTypes.STRING(32),
|
|
61
|
+
allowNull: false,
|
|
62
|
+
defaultValue: 'multiDevice'
|
|
63
|
+
}
|
|
64
|
+
}, {
|
|
65
|
+
sequelize,
|
|
66
|
+
tableName: 'passkey_credentials',
|
|
67
|
+
timestamps: true,
|
|
68
|
+
underscored: true
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function initPasskeyChallengeModel(sequelize) {
|
|
72
|
+
const idType = integerIdType(sequelize);
|
|
73
|
+
return PasskeyChallengeModel.init({
|
|
74
|
+
challenge: {
|
|
75
|
+
type: DataTypes.STRING(255),
|
|
76
|
+
primaryKey: true,
|
|
77
|
+
allowNull: false
|
|
78
|
+
},
|
|
79
|
+
action: {
|
|
80
|
+
type: DataTypes.STRING(16),
|
|
81
|
+
allowNull: false
|
|
82
|
+
},
|
|
83
|
+
userId: {
|
|
84
|
+
field: 'user_id',
|
|
85
|
+
type: idType,
|
|
86
|
+
allowNull: true
|
|
87
|
+
},
|
|
88
|
+
login: {
|
|
89
|
+
type: DataTypes.STRING(128),
|
|
90
|
+
allowNull: true
|
|
91
|
+
},
|
|
92
|
+
metadata: {
|
|
93
|
+
type: DataTypes.JSON,
|
|
94
|
+
allowNull: true
|
|
95
|
+
},
|
|
96
|
+
expiresAt: {
|
|
97
|
+
field: 'expires_at',
|
|
98
|
+
type: DataTypes.DATE,
|
|
99
|
+
allowNull: false
|
|
100
|
+
}
|
|
101
|
+
}, {
|
|
102
|
+
sequelize,
|
|
103
|
+
tableName: 'passkey_challenges',
|
|
104
|
+
timestamps: true,
|
|
105
|
+
underscored: true,
|
|
106
|
+
indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
+
import { PasskeyStore } from './base.js';
|
|
3
|
+
import type { PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
|
|
4
|
+
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
|
+
export interface SequelizePasskeyStoreOptions {
|
|
27
|
+
sequelize: Sequelize;
|
|
28
|
+
credentialModel?: ModelStatic<PasskeyCredentialModel>;
|
|
29
|
+
challengeModel?: ModelStatic<PasskeyChallengeModel>;
|
|
30
|
+
credentialModelFactory?: (sequelize: Sequelize) => ModelStatic<PasskeyCredentialModel>;
|
|
31
|
+
challengeModelFactory?: (sequelize: Sequelize) => ModelStatic<PasskeyChallengeModel>;
|
|
32
|
+
resolveUser: (params: {
|
|
33
|
+
userId?: AuthIdentifier;
|
|
34
|
+
login?: string;
|
|
35
|
+
}) => Promise<PasskeyUserDescriptor | null>;
|
|
36
|
+
}
|
|
37
|
+
export declare class SequelizePasskeyStore extends PasskeyStore {
|
|
38
|
+
private readonly resolveUserFn;
|
|
39
|
+
private readonly credentials;
|
|
40
|
+
private readonly challenges;
|
|
41
|
+
constructor(options: SequelizePasskeyStoreOptions);
|
|
42
|
+
resolveUser(params: {
|
|
43
|
+
userId?: AuthIdentifier;
|
|
44
|
+
login?: string;
|
|
45
|
+
}): Promise<PasskeyUserDescriptor | null>;
|
|
46
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
47
|
+
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
48
|
+
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
49
|
+
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
50
|
+
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
51
|
+
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
52
|
+
cleanupChallenges(now: Date): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { DataTypes, Model, Op } from 'sequelize';
|
|
2
|
+
import { PasskeyStore } from './base.js';
|
|
3
|
+
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
4
|
+
function integerIdType(sequelize) {
|
|
5
|
+
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
6
|
+
}
|
|
7
|
+
function encodeCredentialId(value) {
|
|
8
|
+
return Buffer.isBuffer(value) ? value.toString('base64') : value;
|
|
9
|
+
}
|
|
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
|
+
export class SequelizePasskeyStore extends PasskeyStore {
|
|
123
|
+
constructor(options) {
|
|
124
|
+
super();
|
|
125
|
+
if (!options?.sequelize) {
|
|
126
|
+
throw new Error('SequelizePasskeyStore requires an initialised Sequelize instance');
|
|
127
|
+
}
|
|
128
|
+
this.resolveUserFn = options.resolveUser;
|
|
129
|
+
this.credentials =
|
|
130
|
+
options.credentialModel ??
|
|
131
|
+
(options.credentialModelFactory ?? initPasskeyCredentialModel)(options.sequelize);
|
|
132
|
+
this.challenges =
|
|
133
|
+
options.challengeModel ?? (options.challengeModelFactory ?? initPasskeyChallengeModel)(options.sequelize);
|
|
134
|
+
}
|
|
135
|
+
async resolveUser(params) {
|
|
136
|
+
return this.resolveUserFn(params);
|
|
137
|
+
}
|
|
138
|
+
async listUserCredentials(userId) {
|
|
139
|
+
const models = await this.credentials.findAll({ where: { userId: normalizeUserId(userId) } });
|
|
140
|
+
return models.map((model) => ({
|
|
141
|
+
userId: model.userId,
|
|
142
|
+
credentialId: model.credentialId,
|
|
143
|
+
publicKey: model.publicKey,
|
|
144
|
+
counter: model.counter,
|
|
145
|
+
transports: (model.transports ?? undefined),
|
|
146
|
+
backedUp: model.backedUp,
|
|
147
|
+
deviceType: model.deviceType
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
async findCredentialById(credentialId) {
|
|
151
|
+
const model = await this.credentials.findByPk(encodeCredentialId(credentialId));
|
|
152
|
+
if (!model) {
|
|
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
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async saveCredential(record) {
|
|
166
|
+
await this.credentials.upsert({
|
|
167
|
+
credentialId: record.credentialId,
|
|
168
|
+
userId: normalizeUserId(record.userId),
|
|
169
|
+
publicKey: record.publicKey,
|
|
170
|
+
counter: record.counter,
|
|
171
|
+
transports: record.transports ?? null,
|
|
172
|
+
backedUp: record.backedUp,
|
|
173
|
+
deviceType: record.deviceType
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async updateCredentialCounter(credentialId, counter) {
|
|
177
|
+
await this.credentials.update({ counter }, { where: { credentialId: encodeCredentialId(credentialId) } });
|
|
178
|
+
}
|
|
179
|
+
async saveChallenge(record) {
|
|
180
|
+
await this.challenges.upsert({
|
|
181
|
+
challenge: record.challenge,
|
|
182
|
+
action: record.action,
|
|
183
|
+
userId: record.userId !== undefined ? normalizeUserId(record.userId) : null,
|
|
184
|
+
login: record.login ?? null,
|
|
185
|
+
metadata: (record.metadata ?? {}),
|
|
186
|
+
expiresAt: record.expiresAt
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async consumeChallenge(challenge) {
|
|
190
|
+
const model = await this.challenges.findByPk(challenge);
|
|
191
|
+
if (!model) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
await model.destroy();
|
|
195
|
+
return {
|
|
196
|
+
challenge: model.challenge,
|
|
197
|
+
action: model.action,
|
|
198
|
+
userId: model.userId ?? undefined,
|
|
199
|
+
login: model.login ?? undefined,
|
|
200
|
+
expiresAt: model.expiresAt,
|
|
201
|
+
metadata: model.metadata ?? {}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async cleanupChallenges(now) {
|
|
205
|
+
await this.challenges.destroy({ where: { expiresAt: { [Op.lte]: now } } });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig } from './types.js';
|
|
2
|
+
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyChallengeMetadata, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter } from './types.js';
|
|
3
|
+
type Logger = Pick<typeof console, 'error' | 'warn'>;
|
|
4
|
+
export declare class PasskeyService {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly adapter;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(config: PasskeyServiceConfig, adapter: PasskeyStorageAdapter, logger?: Logger);
|
|
9
|
+
createChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
10
|
+
verifyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
11
|
+
private createRegistrationChallenge;
|
|
12
|
+
private createAuthenticationChallenge;
|
|
13
|
+
private verifyRegistration;
|
|
14
|
+
private verifyAuthentication;
|
|
15
|
+
private requireUser;
|
|
16
|
+
private createExpiry;
|
|
17
|
+
}
|