@technomoron/api-server-base 1.1.13 → 2.0.0-beta.10
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 +25 -2
- package/dist/cjs/api-server-base.cjs +448 -111
- package/dist/cjs/api-server-base.d.ts +91 -34
- package/dist/cjs/auth-api/auth-module.d.ts +105 -0
- package/dist/cjs/auth-api/auth-module.js +1180 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/cjs/auth-api/compat-auth-storage.js +128 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/cjs/auth-api/mem-auth-store.js +141 -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 +77 -0
- package/dist/cjs/auth-api/sql-auth-store.js +172 -0
- package/dist/cjs/auth-api/storage.d.ts +38 -0
- package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +17 -7
- package/dist/cjs/auth-api/types.d.ts +34 -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 +16 -0
- package/dist/cjs/passkey/base.js +6 -0
- package/dist/cjs/passkey/memory.d.ts +27 -0
- package/dist/cjs/passkey/memory.js +86 -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 +55 -0
- package/dist/cjs/passkey/sequelize.js +220 -0
- package/dist/cjs/passkey/service.d.ts +20 -0
- package/dist/cjs/passkey/service.js +356 -0
- package/dist/cjs/passkey/types.d.ts +78 -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 +91 -34
- package/dist/esm/api-server-base.js +447 -110
- package/dist/esm/auth-api/auth-module.d.ts +105 -0
- package/dist/esm/auth-api/auth-module.js +1178 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/esm/auth-api/compat-auth-storage.js +124 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/esm/auth-api/mem-auth-store.js +137 -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 +77 -0
- package/dist/esm/auth-api/sql-auth-store.js +168 -0
- package/dist/esm/auth-api/storage.d.ts +38 -0
- package/dist/esm/{auth-storage.js → auth-api/storage.js} +15 -5
- package/dist/esm/auth-api/types.d.ts +34 -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 +16 -0
- package/dist/esm/passkey/base.js +2 -0
- package/dist/esm/passkey/memory.d.ts +27 -0
- package/dist/esm/passkey/memory.js +82 -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 +55 -0
- package/dist/esm/passkey/sequelize.js +216 -0
- package/dist/esm/passkey/service.d.ts +20 -0
- package/dist/esm/passkey/service.js +319 -0
- package/dist/esm/passkey/types.d.ts +78 -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 +13 -3
- package/dist/cjs/auth-storage.d.ts +0 -133
- package/dist/esm/auth-storage.d.ts +0 -133
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs';
|
|
2
|
+
import { DataTypes, Model } from 'sequelize';
|
|
3
|
+
import { OAuthStore } from './base.js';
|
|
4
|
+
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
5
|
+
function integerIdType(sequelize) {
|
|
6
|
+
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
7
|
+
}
|
|
8
|
+
function tableOptions(sequelize, tableName, extra) {
|
|
9
|
+
const opts = { sequelize, tableName };
|
|
10
|
+
if (extra) {
|
|
11
|
+
Object.assign(opts, extra);
|
|
12
|
+
}
|
|
13
|
+
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
14
|
+
opts.charset = 'utf8mb4';
|
|
15
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
16
|
+
}
|
|
17
|
+
return opts;
|
|
18
|
+
}
|
|
19
|
+
export class OAuthClientModel extends Model {
|
|
20
|
+
}
|
|
21
|
+
export function initOAuthClientModel(sequelize) {
|
|
22
|
+
OAuthClientModel.init({
|
|
23
|
+
client_id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
|
|
24
|
+
client_secret: { type: DataTypes.STRING(255), allowNull: false, defaultValue: '' },
|
|
25
|
+
name: { type: DataTypes.STRING(128), allowNull: true, defaultValue: null },
|
|
26
|
+
redirect_uris: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
|
|
27
|
+
scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
|
|
28
|
+
metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null },
|
|
29
|
+
first_party: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
|
|
30
|
+
}, tableOptions(sequelize, 'oauth_clients', { timestamps: false }));
|
|
31
|
+
return OAuthClientModel;
|
|
32
|
+
}
|
|
33
|
+
export class OAuthCodeModel extends Model {
|
|
34
|
+
}
|
|
35
|
+
export function initOAuthCodeModel(sequelize) {
|
|
36
|
+
const idType = integerIdType(sequelize);
|
|
37
|
+
OAuthCodeModel.init({
|
|
38
|
+
code: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
|
|
39
|
+
client_id: { type: DataTypes.STRING(128), allowNull: false },
|
|
40
|
+
user_id: { type: idType, allowNull: false },
|
|
41
|
+
redirect_uri: { type: DataTypes.TEXT, allowNull: false },
|
|
42
|
+
scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
|
|
43
|
+
code_challenge: { type: DataTypes.STRING(255), allowNull: true, defaultValue: null },
|
|
44
|
+
code_challenge_method: { type: DataTypes.STRING(10), allowNull: true, defaultValue: null },
|
|
45
|
+
expires: { type: DataTypes.DATE, allowNull: false },
|
|
46
|
+
metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null }
|
|
47
|
+
}, tableOptions(sequelize, 'oauth_codes', { timestamps: false }));
|
|
48
|
+
return OAuthCodeModel;
|
|
49
|
+
}
|
|
50
|
+
function encodeStringArray(values) {
|
|
51
|
+
return JSON.stringify(values ?? []);
|
|
52
|
+
}
|
|
53
|
+
function decodeStringArray(raw) {
|
|
54
|
+
if (!raw) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (Array.isArray(parsed)) {
|
|
60
|
+
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore malformed values
|
|
65
|
+
}
|
|
66
|
+
return raw
|
|
67
|
+
.split(/\s+/)
|
|
68
|
+
.map((entry) => entry.trim())
|
|
69
|
+
.filter((entry) => entry.length > 0);
|
|
70
|
+
}
|
|
71
|
+
function serializeMetadata(metadata) {
|
|
72
|
+
if (!metadata) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(metadata);
|
|
76
|
+
}
|
|
77
|
+
function parseMetadata(raw) {
|
|
78
|
+
if (!raw) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(raw);
|
|
83
|
+
if (parsed && typeof parsed === 'object') {
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// ignore
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
function normalizeUserId(identifier) {
|
|
93
|
+
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
94
|
+
return identifier;
|
|
95
|
+
}
|
|
96
|
+
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
97
|
+
return Number(identifier);
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
100
|
+
}
|
|
101
|
+
export class SequelizeOAuthStore extends OAuthStore {
|
|
102
|
+
constructor(options) {
|
|
103
|
+
super();
|
|
104
|
+
if (!options?.sequelize) {
|
|
105
|
+
throw new Error('SequelizeOAuthStore requires an initialised Sequelize instance');
|
|
106
|
+
}
|
|
107
|
+
this.clients = options.clientModel ?? (options.clientModelFactory ?? initOAuthClientModel)(options.sequelize);
|
|
108
|
+
this.codes = options.codeModel ?? (options.codeModelFactory ?? initOAuthCodeModel)(options.sequelize);
|
|
109
|
+
this.bcryptRounds = options.bcryptRounds ?? 12;
|
|
110
|
+
}
|
|
111
|
+
async getClient(clientId) {
|
|
112
|
+
const model = await this.clients.findByPk(clientId);
|
|
113
|
+
return model ? this.toOAuthClient(model) : null;
|
|
114
|
+
}
|
|
115
|
+
async createClient(input) {
|
|
116
|
+
const existing = await this.clients.findByPk(input.clientId);
|
|
117
|
+
const hashedSecret = input.clientSecret !== undefined && input.clientSecret !== null
|
|
118
|
+
? await bcrypt.hash(input.clientSecret, this.bcryptRounds)
|
|
119
|
+
: (existing?.client_secret ?? '');
|
|
120
|
+
const redirectUris = input.redirectUris ?? (existing ? decodeStringArray(existing.redirect_uris) : undefined);
|
|
121
|
+
const scope = input.scope ?? (existing ? decodeStringArray(existing.scope) : undefined);
|
|
122
|
+
const metadata = input.metadata ?? (existing ? parseMetadata(existing.metadata) : undefined);
|
|
123
|
+
await this.clients.upsert({
|
|
124
|
+
client_id: input.clientId,
|
|
125
|
+
client_secret: hashedSecret,
|
|
126
|
+
name: input.name ?? existing?.name ?? null,
|
|
127
|
+
redirect_uris: encodeStringArray(redirectUris),
|
|
128
|
+
scope: encodeStringArray(scope),
|
|
129
|
+
metadata: serializeMetadata(metadata),
|
|
130
|
+
first_party: input.firstParty ?? existing?.first_party ?? false
|
|
131
|
+
});
|
|
132
|
+
const model = await this.clients.findByPk(input.clientId);
|
|
133
|
+
if (!model) {
|
|
134
|
+
throw new Error(`Unable to persist OAuth client ${input.clientId}`);
|
|
135
|
+
}
|
|
136
|
+
return this.toOAuthClient(model);
|
|
137
|
+
}
|
|
138
|
+
async verifyClientSecret(clientId, clientSecret) {
|
|
139
|
+
const model = await this.clients.findByPk(clientId);
|
|
140
|
+
if (!model) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (!model.client_secret) {
|
|
144
|
+
return !clientSecret || clientSecret.length === 0;
|
|
145
|
+
}
|
|
146
|
+
if (!clientSecret) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return bcrypt.compare(clientSecret, model.client_secret);
|
|
150
|
+
}
|
|
151
|
+
async createAuthCode(code) {
|
|
152
|
+
await this.codes.create({
|
|
153
|
+
code: code.code,
|
|
154
|
+
client_id: code.clientId,
|
|
155
|
+
user_id: normalizeUserId(code.userId),
|
|
156
|
+
redirect_uri: code.redirectUri ?? '',
|
|
157
|
+
scope: encodeStringArray(code.scope),
|
|
158
|
+
code_challenge: code.codeChallenge ?? null,
|
|
159
|
+
code_challenge_method: code.codeChallengeMethod ?? null,
|
|
160
|
+
expires: code.expiresAt,
|
|
161
|
+
metadata: serializeMetadata(code.metadata)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async consumeAuthCode(code) {
|
|
165
|
+
const model = await this.codes.findByPk(code);
|
|
166
|
+
if (!model) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
await model.destroy();
|
|
170
|
+
return this.toAuthCode(model);
|
|
171
|
+
}
|
|
172
|
+
async close() {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
toOAuthClient(model) {
|
|
176
|
+
return {
|
|
177
|
+
clientId: model.client_id,
|
|
178
|
+
clientSecret: model.client_secret,
|
|
179
|
+
name: model.name ?? undefined,
|
|
180
|
+
redirectUris: decodeStringArray(model.redirect_uris),
|
|
181
|
+
scope: decodeStringArray(model.scope),
|
|
182
|
+
metadata: parseMetadata(model.metadata),
|
|
183
|
+
firstParty: model.first_party ?? false
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
toAuthCode(model) {
|
|
187
|
+
return {
|
|
188
|
+
code: model.code,
|
|
189
|
+
clientId: model.client_id,
|
|
190
|
+
userId: model.user_id,
|
|
191
|
+
redirectUri: model.redirect_uri,
|
|
192
|
+
scope: decodeStringArray(model.scope),
|
|
193
|
+
codeChallenge: model.code_challenge ?? undefined,
|
|
194
|
+
codeChallengeMethod: model.code_challenge_method ?? undefined,
|
|
195
|
+
expiresAt: model.expires,
|
|
196
|
+
metadata: parseMetadata(model.metadata)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
2
|
+
export interface OAuthClient {
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret?: string;
|
|
5
|
+
firstParty?: boolean;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
name?: string;
|
|
8
|
+
redirectUris: string[];
|
|
9
|
+
scope?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface AuthCodeData {
|
|
12
|
+
code: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
codeChallenge?: string;
|
|
15
|
+
codeChallengeMethod?: 'plain' | 'S256';
|
|
16
|
+
expiresAt: Date;
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
redirectUri?: string;
|
|
19
|
+
scope?: string[];
|
|
20
|
+
userId: AuthIdentifier;
|
|
21
|
+
}
|
|
22
|
+
export type AuthCode = AuthCodeData;
|
|
23
|
+
export type AuthCodeRequest = Omit<AuthCodeData, 'code' | 'expiresAt'> & {
|
|
24
|
+
code?: string;
|
|
25
|
+
expiresInSeconds?: number;
|
|
26
|
+
};
|
|
27
|
+
export interface OAuthStartParams {
|
|
28
|
+
provider: string;
|
|
29
|
+
redirectUri?: string;
|
|
30
|
+
scope?: string | string[];
|
|
31
|
+
state?: string;
|
|
32
|
+
extras?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
export interface OAuthStartResult extends Record<string, unknown> {
|
|
35
|
+
url: string;
|
|
36
|
+
state?: string;
|
|
37
|
+
codeVerifier?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface OAuthCallbackParams {
|
|
40
|
+
provider: string;
|
|
41
|
+
query: Record<string, string | string[]>;
|
|
42
|
+
body: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
export interface OAuthCallbackResult<PublicUser> extends Record<string, unknown> {
|
|
45
|
+
user: PublicUser;
|
|
46
|
+
tokens?: {
|
|
47
|
+
accessToken: string;
|
|
48
|
+
refreshToken: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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 deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
10
|
+
abstract findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
11
|
+
abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
12
|
+
abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
13
|
+
abstract saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
14
|
+
abstract consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
15
|
+
abstract cleanupChallenges(now: Date): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
21
|
+
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
22
|
+
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
23
|
+
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
24
|
+
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
25
|
+
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
26
|
+
cleanupChallenges(now: Date): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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 deleteCredential(credentialId) {
|
|
38
|
+
const key = encodeCredentialId(credentialId);
|
|
39
|
+
return this.credentials.delete(key);
|
|
40
|
+
}
|
|
41
|
+
async findCredentialById(credentialId) {
|
|
42
|
+
const record = this.credentials.get(encodeCredentialId(credentialId));
|
|
43
|
+
return record ? cloneCredential(record) : null;
|
|
44
|
+
}
|
|
45
|
+
async saveCredential(record) {
|
|
46
|
+
this.credentials.set(encodeCredentialId(record.credentialId), {
|
|
47
|
+
...record,
|
|
48
|
+
userId: normalizeUserId(record.userId),
|
|
49
|
+
credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
|
|
50
|
+
transports: record.transports ? [...record.transports] : undefined
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async updateCredentialCounter(credentialId, counter) {
|
|
54
|
+
const key = encodeCredentialId(credentialId);
|
|
55
|
+
const existing = this.credentials.get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
existing.counter = counter;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async saveChallenge(record) {
|
|
61
|
+
this.challenges.set(record.challenge, {
|
|
62
|
+
...record,
|
|
63
|
+
userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
|
|
64
|
+
metadata: record.metadata ? { ...record.metadata } : {}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async consumeChallenge(challenge) {
|
|
68
|
+
const record = this.challenges.get(challenge);
|
|
69
|
+
if (!record) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
this.challenges.delete(challenge);
|
|
73
|
+
return { ...record, metadata: record.metadata ? { ...record.metadata } : {} };
|
|
74
|
+
}
|
|
75
|
+
async cleanupChallenges(now) {
|
|
76
|
+
for (const [challenge, record] of this.challenges.entries()) {
|
|
77
|
+
if (record.expiresAt && new Date(record.expiresAt) <= now) {
|
|
78
|
+
this.challenges.delete(challenge);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -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,55 @@
|
|
|
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
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
48
|
+
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
49
|
+
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
50
|
+
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
51
|
+
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
52
|
+
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
53
|
+
cleanupChallenges(now: Date): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export {};
|