@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
|
@@ -7,18 +7,6 @@ const sequelize_utils_js_1 = require("../sequelize-utils.js");
|
|
|
7
7
|
const base_js_1 = require("./base.js");
|
|
8
8
|
class TokenModel extends sequelize_1.Model {
|
|
9
9
|
}
|
|
10
|
-
function tokenTableOptions(sequelize, tablePrefix) {
|
|
11
|
-
const opts = {
|
|
12
|
-
sequelize,
|
|
13
|
-
tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, 'jwttokens'),
|
|
14
|
-
timestamps: false
|
|
15
|
-
};
|
|
16
|
-
if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
17
|
-
opts.charset = 'utf8mb4';
|
|
18
|
-
opts.collate = 'utf8mb4_unicode_ci';
|
|
19
|
-
}
|
|
20
|
-
return opts;
|
|
21
|
-
}
|
|
22
10
|
function initTokenModel(sequelize, options = {}) {
|
|
23
11
|
const tableName = (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'jwttokens');
|
|
24
12
|
const usePrefixedIndexNames = tableName !== 'jwttokens';
|
|
@@ -26,9 +14,7 @@ function initTokenModel(sequelize, options = {}) {
|
|
|
26
14
|
const refreshIndexName = usePrefixedIndexNames ? `${tableName}_refresh_unique` : 'jwt_refresh_unique';
|
|
27
15
|
TokenModel.init({
|
|
28
16
|
token_id: {
|
|
29
|
-
type: sequelize_utils_js_1.
|
|
30
|
-
? sequelize_1.DataTypes.INTEGER.UNSIGNED
|
|
31
|
-
: sequelize_1.DataTypes.INTEGER,
|
|
17
|
+
type: (0, sequelize_utils_js_1.integerIdType)(sequelize),
|
|
32
18
|
autoIncrement: true,
|
|
33
19
|
allowNull: false,
|
|
34
20
|
primaryKey: true
|
|
@@ -125,7 +111,7 @@ function initTokenModel(sequelize, options = {}) {
|
|
|
125
111
|
defaultValue: '[]'
|
|
126
112
|
}
|
|
127
113
|
}, {
|
|
128
|
-
...
|
|
114
|
+
...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'jwttokens', options.tablePrefix, { timestamps: false }),
|
|
129
115
|
indexes: [
|
|
130
116
|
{ name: accessIndexName, unique: true, fields: ['access'] },
|
|
131
117
|
{ name: refreshIndexName, unique: true, fields: ['refresh'] }
|
|
@@ -164,43 +150,50 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
164
150
|
const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
|
|
165
151
|
const sessionCookie = normalized.sessionCookie ?? false;
|
|
166
152
|
const removalWhere = { user_id: resolvedUserId };
|
|
167
|
-
if (
|
|
153
|
+
if (record.domain !== undefined) {
|
|
168
154
|
removalWhere.domain = domain;
|
|
169
155
|
}
|
|
170
|
-
if (
|
|
156
|
+
if (record.fingerprint !== undefined) {
|
|
171
157
|
removalWhere.fingerprint = fingerprint;
|
|
172
158
|
}
|
|
173
159
|
if (normalized.clientId) {
|
|
174
160
|
removalWhere.client_id = normalized.clientId;
|
|
175
161
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
162
|
+
const sequelize = this.Tokens.sequelize;
|
|
163
|
+
if (!sequelize) {
|
|
164
|
+
throw new Error('Token model is not bound to a Sequelize instance');
|
|
165
|
+
}
|
|
166
|
+
await sequelize.transaction(async (transaction) => {
|
|
167
|
+
await this.Tokens.destroy({ where: removalWhere, transaction });
|
|
168
|
+
// Access/refresh columns are unique. Remove stale collisions before insert to avoid
|
|
169
|
+
// transient uniqueness failures during retries/rotation edge-cases.
|
|
170
|
+
await this.Tokens.destroy({
|
|
171
|
+
where: {
|
|
172
|
+
[sequelize_1.Op.or]: [{ access: normalized.accessToken ?? '' }, { refresh: normalized.refreshToken }]
|
|
173
|
+
},
|
|
174
|
+
transaction
|
|
175
|
+
});
|
|
176
|
+
await this.Tokens.create({
|
|
177
|
+
user_id: resolvedUserId,
|
|
178
|
+
real_user_id: resolvedRealUserId,
|
|
179
|
+
access: normalized.accessToken ?? '',
|
|
180
|
+
refresh: normalized.refreshToken,
|
|
181
|
+
expires: normalized.expires,
|
|
182
|
+
issued_at: issuedAt,
|
|
183
|
+
last_seen_at: lastSeenAt,
|
|
184
|
+
domain,
|
|
185
|
+
fingerprint,
|
|
186
|
+
label,
|
|
187
|
+
browser,
|
|
188
|
+
device,
|
|
189
|
+
ip,
|
|
190
|
+
os,
|
|
191
|
+
client_id: normalized.clientId ?? null,
|
|
192
|
+
scope: this.encodeScope(normalized.scope),
|
|
193
|
+
login_type: loginType,
|
|
194
|
+
refresh_ttl_seconds: refreshTtlSeconds,
|
|
195
|
+
session_cookie: sessionCookie
|
|
196
|
+
}, { transaction });
|
|
204
197
|
});
|
|
205
198
|
}
|
|
206
199
|
async get(query, opts) {
|
|
@@ -269,11 +262,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
269
262
|
where.client_id = params.clientId;
|
|
270
263
|
}
|
|
271
264
|
const updates = {};
|
|
272
|
-
if (params.accessToken !== undefined) {
|
|
273
|
-
updates.access = params.accessToken
|
|
265
|
+
if (params.accessToken !== undefined && params.accessToken !== null) {
|
|
266
|
+
updates.access = params.accessToken;
|
|
274
267
|
}
|
|
275
|
-
if (params.expires !== undefined) {
|
|
276
|
-
updates.expires = params.expires
|
|
268
|
+
if (params.expires !== undefined && params.expires !== null) {
|
|
269
|
+
updates.expires = params.expires;
|
|
277
270
|
}
|
|
278
271
|
if (params.scope !== undefined) {
|
|
279
272
|
updates.scope = this.encodeScope(params.scope);
|
|
@@ -311,11 +304,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
311
304
|
if (params.sessionCookie !== undefined) {
|
|
312
305
|
updates.session_cookie = params.sessionCookie;
|
|
313
306
|
}
|
|
314
|
-
if (params.issuedAt !== undefined) {
|
|
315
|
-
updates.issued_at = params.issuedAt
|
|
307
|
+
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
308
|
+
updates.issued_at = params.issuedAt;
|
|
316
309
|
}
|
|
317
|
-
if (params.lastSeenAt !== undefined) {
|
|
318
|
-
updates.last_seen_at = params.lastSeenAt
|
|
310
|
+
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
311
|
+
updates.last_seen_at = params.lastSeenAt;
|
|
319
312
|
}
|
|
320
313
|
if (Object.keys(updates).length === 0) {
|
|
321
314
|
return false;
|
|
@@ -352,41 +345,17 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
352
345
|
}
|
|
353
346
|
return value;
|
|
354
347
|
}
|
|
355
|
-
encodeStringArray(values) {
|
|
356
|
-
return JSON.stringify(values ?? []);
|
|
357
|
-
}
|
|
358
|
-
decodeStringArray(raw) {
|
|
359
|
-
if (!raw) {
|
|
360
|
-
return [];
|
|
361
|
-
}
|
|
362
|
-
try {
|
|
363
|
-
const parsed = JSON.parse(raw);
|
|
364
|
-
if (Array.isArray(parsed)) {
|
|
365
|
-
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
// ignore malformed values
|
|
370
|
-
}
|
|
371
|
-
return raw
|
|
372
|
-
.split(/\s+/)
|
|
373
|
-
.map((entry) => entry.trim())
|
|
374
|
-
.filter((entry) => entry.length > 0);
|
|
375
|
-
}
|
|
376
348
|
encodeScope(scope) {
|
|
377
349
|
if (!scope || (Array.isArray(scope) && scope.length === 0)) {
|
|
378
350
|
return '[]';
|
|
379
351
|
}
|
|
380
352
|
if (Array.isArray(scope)) {
|
|
381
|
-
return
|
|
353
|
+
return (0, sequelize_utils_js_1.encodeStringArray)(scope);
|
|
382
354
|
}
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
decodeScope(raw) {
|
|
386
|
-
return this.decodeStringArray(raw);
|
|
355
|
+
return (0, sequelize_utils_js_1.encodeStringArray)(scope.split(/\s+/).filter((entry) => entry.length > 0));
|
|
387
356
|
}
|
|
388
357
|
toTokenRecord(model) {
|
|
389
|
-
const scope =
|
|
358
|
+
const scope = (0, sequelize_utils_js_1.decodeStringArray)(model.scope);
|
|
390
359
|
const normalized = this.normalizeToken({
|
|
391
360
|
userId: model.user_id,
|
|
392
361
|
refreshToken: model.refresh,
|
package/dist/cjs/user/base.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare abstract class UserStore<User, PublicUser> {
|
|
|
9
9
|
bcryptRounds?: number;
|
|
10
10
|
bcryptPepper?: string;
|
|
11
11
|
});
|
|
12
|
+
private applyPepper;
|
|
12
13
|
protected hashPassword(plain: string): Promise<string>;
|
|
13
14
|
verifyPassword(plain: string, hashed: string): Promise<boolean>;
|
|
14
15
|
protected normalizeUserInput(input: Partial<CreateUserInput>): CreateUserInput;
|
package/dist/cjs/user/base.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.UserStore = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
8
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
8
9
|
class UserStore {
|
|
9
10
|
constructor(opts = {}) {
|
|
@@ -15,12 +16,18 @@ class UserStore {
|
|
|
15
16
|
this.bcryptPepper =
|
|
16
17
|
typeof opts.bcryptPepper === 'string' && opts.bcryptPepper.length > 0 ? opts.bcryptPepper : undefined;
|
|
17
18
|
}
|
|
19
|
+
applyPepper(plain) {
|
|
20
|
+
if (!this.bcryptPepper) {
|
|
21
|
+
return plain;
|
|
22
|
+
}
|
|
23
|
+
return (0, node_crypto_1.createHmac)('sha256', this.bcryptPepper).update(plain).digest('hex');
|
|
24
|
+
}
|
|
18
25
|
async hashPassword(plain) {
|
|
19
|
-
const candidate = this.
|
|
26
|
+
const candidate = this.applyPepper(plain);
|
|
20
27
|
return bcryptjs_1.default.hash(candidate, this.bcryptRounds);
|
|
21
28
|
}
|
|
22
29
|
async verifyPassword(plain, hashed) {
|
|
23
|
-
const candidate = this.
|
|
30
|
+
const candidate = this.applyPepper(plain);
|
|
24
31
|
return bcryptjs_1.default.compare(candidate, hashed);
|
|
25
32
|
}
|
|
26
33
|
normalizeUserInput(input) {
|
|
@@ -35,8 +42,8 @@ class UserStore {
|
|
|
35
42
|
toPublic(user) {
|
|
36
43
|
const mapped = this.toPublicUser(user);
|
|
37
44
|
if (mapped && typeof mapped === 'object') {
|
|
38
|
-
const
|
|
39
|
-
|
|
45
|
+
const rest = { ...mapped };
|
|
46
|
+
delete rest.password;
|
|
40
47
|
return rest;
|
|
41
48
|
}
|
|
42
49
|
return mapped;
|
|
@@ -14,12 +14,14 @@ export interface MemoryUserStoreOptions<UserAttributes extends MemoryUserAttribu
|
|
|
14
14
|
toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
|
|
15
15
|
userIdFactory?: () => number;
|
|
16
16
|
startingUserId?: number;
|
|
17
|
+
maxUsers?: number;
|
|
17
18
|
}
|
|
18
19
|
export declare class MemoryUserStore<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
|
|
19
20
|
private readonly usersById;
|
|
20
21
|
private readonly loginToId;
|
|
21
22
|
private readonly emailToId;
|
|
22
23
|
private readonly userIdFactory;
|
|
24
|
+
private readonly maxUsers?;
|
|
23
25
|
private nextUserId;
|
|
24
26
|
constructor(options?: MemoryUserStoreOptions<UserAttributes, PublicUserShape>);
|
|
25
27
|
findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
|
package/dist/cjs/user/memory.js
CHANGED
|
@@ -6,7 +6,6 @@ const base_js_1 = require("./base.js");
|
|
|
6
6
|
function cloneUser(user) {
|
|
7
7
|
return { ...user };
|
|
8
8
|
}
|
|
9
|
-
const normalizeUserId = user_id_js_1.normalizeNumericUserId;
|
|
10
9
|
class MemoryUserStore extends base_js_1.UserStore {
|
|
11
10
|
constructor(options = {}) {
|
|
12
11
|
super({
|
|
@@ -18,6 +17,10 @@ class MemoryUserStore extends base_js_1.UserStore {
|
|
|
18
17
|
this.loginToId = new Map();
|
|
19
18
|
this.emailToId = new Map();
|
|
20
19
|
this.nextUserId = Number.isFinite(options.startingUserId) ? Number(options.startingUserId) : 1;
|
|
20
|
+
this.maxUsers =
|
|
21
|
+
typeof options.maxUsers === 'number' && Number.isFinite(options.maxUsers) && options.maxUsers > 0
|
|
22
|
+
? Math.floor(options.maxUsers)
|
|
23
|
+
: undefined;
|
|
21
24
|
this.userIdFactory =
|
|
22
25
|
options.userIdFactory ??
|
|
23
26
|
(() => {
|
|
@@ -52,7 +55,7 @@ class MemoryUserStore extends base_js_1.UserStore {
|
|
|
52
55
|
}
|
|
53
56
|
async findById(id) {
|
|
54
57
|
try {
|
|
55
|
-
const numeric =
|
|
58
|
+
const numeric = (0, user_id_js_1.normalizeNumericUserId)(id);
|
|
56
59
|
const user = this.usersById.get(numeric);
|
|
57
60
|
return user ? cloneUser(user) : null;
|
|
58
61
|
}
|
|
@@ -70,6 +73,9 @@ class MemoryUserStore extends base_js_1.UserStore {
|
|
|
70
73
|
if (this.usersById.has(userId)) {
|
|
71
74
|
throw new Error(`User ${userId} already exists`);
|
|
72
75
|
}
|
|
76
|
+
if (this.maxUsers !== undefined && this.usersById.size >= this.maxUsers) {
|
|
77
|
+
throw new Error('MemoryUserStore maxUsers limit reached');
|
|
78
|
+
}
|
|
73
79
|
if (this.loginToId.has(normalizedInput.login)) {
|
|
74
80
|
throw new Error(`User with login ${normalizedInput.login} already exists`);
|
|
75
81
|
}
|
|
@@ -6,26 +6,11 @@ const sequelize_1 = require("sequelize");
|
|
|
6
6
|
const user_id_js_1 = require("../auth-api/user-id.js");
|
|
7
7
|
const sequelize_utils_js_1 = require("../sequelize-utils.js");
|
|
8
8
|
const base_js_1 = require("./base.js");
|
|
9
|
-
function integerIdType(sequelize) {
|
|
10
|
-
return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
|
|
11
|
-
}
|
|
12
|
-
function userTableOptions(sequelize, tablePrefix) {
|
|
13
|
-
const opts = {
|
|
14
|
-
sequelize,
|
|
15
|
-
tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, 'users'),
|
|
16
|
-
timestamps: false
|
|
17
|
-
};
|
|
18
|
-
if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
19
|
-
opts.charset = 'utf8mb4';
|
|
20
|
-
opts.collate = 'utf8mb4_unicode_ci';
|
|
21
|
-
}
|
|
22
|
-
return opts;
|
|
23
|
-
}
|
|
24
9
|
class AuthUserModel extends sequelize_1.Model {
|
|
25
10
|
}
|
|
26
11
|
exports.AuthUserModel = AuthUserModel;
|
|
27
12
|
function initAuthUserModel(sequelize, options = {}) {
|
|
28
|
-
const idType = integerIdType(sequelize);
|
|
13
|
+
const idType = (0, sequelize_utils_js_1.integerIdType)(sequelize);
|
|
29
14
|
AuthUserModel.init({
|
|
30
15
|
user_id: {
|
|
31
16
|
type: idType,
|
|
@@ -48,7 +33,7 @@ function initAuthUserModel(sequelize, options = {}) {
|
|
|
48
33
|
allowNull: false
|
|
49
34
|
}
|
|
50
35
|
}, {
|
|
51
|
-
...
|
|
36
|
+
...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'users', options.tablePrefix, { timestamps: false })
|
|
52
37
|
});
|
|
53
38
|
return AuthUserModel;
|
|
54
39
|
}
|
|
@@ -115,11 +100,16 @@ class SequelizeUserStore extends base_js_1.UserStore {
|
|
|
115
100
|
if (providedId !== undefined && providedId !== null && Number.isFinite(providedId)) {
|
|
116
101
|
defaults.user_id = Number(providedId);
|
|
117
102
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
103
|
+
try {
|
|
104
|
+
const model = await this.Users.create(defaults);
|
|
105
|
+
return this.toUserRecord(model);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof sequelize_1.UniqueConstraintError) {
|
|
109
|
+
throw new Error(`User with login ${rest.login} or email ${rest.email} already exists`);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
123
113
|
}
|
|
124
114
|
async upsertUser(input) {
|
|
125
115
|
const normalized = this.normalizeUserInput(input);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
-
import
|
|
8
|
+
import { access, readFile } from 'node:fs/promises';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import cookieParser from 'cookie-parser';
|
|
@@ -14,6 +14,7 @@ import express from 'express';
|
|
|
14
14
|
import multer from 'multer';
|
|
15
15
|
import { nullAuthModule } from './auth-api/module.js';
|
|
16
16
|
import { nullAuthAdapter } from './auth-api/storage.js';
|
|
17
|
+
import { toOptionalStringId } from './auth-api/user-id.js';
|
|
17
18
|
import { buildAuthCookieOptions } from './auth-cookie-options.js';
|
|
18
19
|
import { TokenStore } from './token/base.js';
|
|
19
20
|
class JwtHelperStore extends TokenStore {
|
|
@@ -267,7 +268,9 @@ function collectClientIpChain(req) {
|
|
|
267
268
|
}
|
|
268
269
|
const realIp = req.headers['x-real-ip'];
|
|
269
270
|
if (Array.isArray(realIp)) {
|
|
270
|
-
|
|
271
|
+
for (const value of realIp) {
|
|
272
|
+
pushNormalized(normalizeIpAddress(value));
|
|
273
|
+
}
|
|
271
274
|
}
|
|
272
275
|
else if (typeof realIp === 'string') {
|
|
273
276
|
pushNormalized(normalizeIpAddress(realIp));
|
|
@@ -421,6 +424,10 @@ export class ApiServer {
|
|
|
421
424
|
this.canImpersonateAdapter = canImpersonate ?? null;
|
|
422
425
|
this.storageAdapter = this.getServerAuthAdapter();
|
|
423
426
|
}
|
|
427
|
+
if ((this.config.authApi || this.config.authStores) &&
|
|
428
|
+
(!this.config.accessSecret || !this.config.refreshSecret)) {
|
|
429
|
+
console.warn('[api-server-base] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
|
|
430
|
+
}
|
|
424
431
|
this.app = express();
|
|
425
432
|
// Mount API modules and any custom endpoints under `apiBasePath` on this router so we can keep
|
|
426
433
|
// the API 404 handler ordered last without relying on Express internals.
|
|
@@ -608,8 +615,8 @@ export class ApiServer {
|
|
|
608
615
|
async getToken(query, opts) {
|
|
609
616
|
const normalized = {
|
|
610
617
|
...query,
|
|
611
|
-
userId:
|
|
612
|
-
ruid:
|
|
618
|
+
userId: toOptionalStringId(query.userId),
|
|
619
|
+
ruid: toOptionalStringId(query.ruid)
|
|
613
620
|
};
|
|
614
621
|
if (this.tokenStoreAdapter) {
|
|
615
622
|
return this.tokenStoreAdapter.get(normalized, opts);
|
|
@@ -623,8 +630,8 @@ export class ApiServer {
|
|
|
623
630
|
async deleteToken(query) {
|
|
624
631
|
const normalized = {
|
|
625
632
|
...query,
|
|
626
|
-
userId:
|
|
627
|
-
ruid:
|
|
633
|
+
userId: toOptionalStringId(query.userId),
|
|
634
|
+
ruid: toOptionalStringId(query.ruid)
|
|
628
635
|
};
|
|
629
636
|
if (this.tokenStoreAdapter) {
|
|
630
637
|
return this.tokenStoreAdapter.delete(normalized);
|
|
@@ -787,7 +794,7 @@ export class ApiServer {
|
|
|
787
794
|
res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
|
|
788
795
|
});
|
|
789
796
|
}
|
|
790
|
-
loadSwaggerSpec() {
|
|
797
|
+
async loadSwaggerSpec() {
|
|
791
798
|
const candidates = [path.resolve(process.cwd(), 'docs/swagger/openapi.json')];
|
|
792
799
|
if (typeof __dirname === 'string') {
|
|
793
800
|
candidates.push(path.resolve(__dirname, '../../docs/swagger/openapi.json'));
|
|
@@ -802,11 +809,14 @@ export class ApiServer {
|
|
|
802
809
|
// Ignore resolution failures; fall back to any existing candidate.
|
|
803
810
|
}
|
|
804
811
|
for (const candidate of candidates) {
|
|
805
|
-
|
|
812
|
+
try {
|
|
813
|
+
await access(candidate);
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
806
816
|
continue;
|
|
807
817
|
}
|
|
808
818
|
try {
|
|
809
|
-
const raw =
|
|
819
|
+
const raw = await readFile(candidate, 'utf8');
|
|
810
820
|
return JSON.parse(raw);
|
|
811
821
|
}
|
|
812
822
|
catch {
|
|
@@ -824,12 +834,12 @@ export class ApiServer {
|
|
|
824
834
|
const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
|
|
825
835
|
const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
|
|
826
836
|
const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
|
|
827
|
-
let
|
|
828
|
-
this.app.get(path, (_req, res) => {
|
|
829
|
-
if (
|
|
830
|
-
|
|
837
|
+
let specPromise;
|
|
838
|
+
this.app.get(path, async (_req, res) => {
|
|
839
|
+
if (!specPromise) {
|
|
840
|
+
specPromise = this.loadSwaggerSpec();
|
|
831
841
|
}
|
|
832
|
-
const spec =
|
|
842
|
+
const spec = await specPromise;
|
|
833
843
|
if (!spec) {
|
|
834
844
|
res.status(500).json({
|
|
835
845
|
success: false,
|
|
@@ -1042,7 +1052,7 @@ export class ApiServer {
|
|
|
1042
1052
|
let tokenData;
|
|
1043
1053
|
let error;
|
|
1044
1054
|
let expired = false;
|
|
1045
|
-
if (!token
|
|
1055
|
+
if (!token) {
|
|
1046
1056
|
if (authType === 'maybe') {
|
|
1047
1057
|
if (!this.config.refreshMaybe) {
|
|
1048
1058
|
return null;
|
|
@@ -1195,9 +1205,6 @@ export class ApiServer {
|
|
|
1195
1205
|
if (rawReal === null) {
|
|
1196
1206
|
return effectiveUserId;
|
|
1197
1207
|
}
|
|
1198
|
-
if (typeof rawReal === 'number' && rawReal === 0) {
|
|
1199
|
-
return effectiveUserId;
|
|
1200
|
-
}
|
|
1201
1208
|
return rawReal;
|
|
1202
1209
|
}
|
|
1203
1210
|
useExpress(pathOrHandler, ...handlers) {
|
|
@@ -1421,9 +1428,29 @@ export class ApiServer {
|
|
|
1421
1428
|
console.log('URL:', url);
|
|
1422
1429
|
console.log('Method:', req.method);
|
|
1423
1430
|
console.log('Query Params:', req.query || {});
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1431
|
+
const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
|
|
1432
|
+
const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
|
|
1433
|
+
if (body && typeof body === 'object') {
|
|
1434
|
+
for (const key of sensitiveBodyKeys) {
|
|
1435
|
+
if (key in body) {
|
|
1436
|
+
body[key] = '[REDACTED]';
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
console.log('Body Params:', body || {});
|
|
1441
|
+
const cookies = req.cookies ? { ...req.cookies } : {};
|
|
1442
|
+
const sensitiveCookieKeys = [this.config.accessCookie, this.config.refreshCookie];
|
|
1443
|
+
for (const key of sensitiveCookieKeys) {
|
|
1444
|
+
if (key in cookies) {
|
|
1445
|
+
cookies[key] = '[REDACTED]';
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
console.log('Cookies:', cookies);
|
|
1449
|
+
const headers = { ...req.headers };
|
|
1450
|
+
if (headers.authorization) {
|
|
1451
|
+
headers.authorization = '[REDACTED]';
|
|
1452
|
+
}
|
|
1453
|
+
console.log('Headers:', headers);
|
|
1427
1454
|
console.log('------------------------');
|
|
1428
1455
|
}
|
|
1429
1456
|
formatDebugValue(value, maxLength = 50, seen = new WeakSet()) {
|
|
@@ -155,6 +155,9 @@ class AuthModule extends BaseAuthModule {
|
|
|
155
155
|
return candidate ? {} : { sessionCookie: true, refreshTtlSeconds: this.sessionRefreshTtlSeconds() };
|
|
156
156
|
}
|
|
157
157
|
if (typeof candidate === 'number') {
|
|
158
|
+
if (candidate === 0) {
|
|
159
|
+
return { sessionCookie: true, refreshTtlSeconds: this.sessionRefreshTtlSeconds() };
|
|
160
|
+
}
|
|
158
161
|
const ttl = this.normalizeRefreshTtlSeconds(candidate);
|
|
159
162
|
return ttl ? { sessionCookie: false, refreshTtlSeconds: ttl } : {};
|
|
160
163
|
}
|
|
@@ -447,20 +450,12 @@ class AuthModule extends BaseAuthModule {
|
|
|
447
450
|
this.assertAuthReady();
|
|
448
451
|
const { login, password, ...metadata } = this.parseLoginBody(apiReq);
|
|
449
452
|
const user = await this.storage.getUser(login);
|
|
450
|
-
|
|
453
|
+
const hash = user ? this.storage.getUserPasswordHash(user) : '';
|
|
454
|
+
const verified = user ? await this.storage.verifyPassword(password, hash) : false;
|
|
455
|
+
if (!user || !verified) {
|
|
451
456
|
throw new ApiError({
|
|
452
457
|
code: 400,
|
|
453
|
-
message: 'Invalid credentials'
|
|
454
|
-
errors: { login: 'Unknown user' }
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
const hash = this.storage.getUserPasswordHash(user);
|
|
458
|
-
const verified = await this.storage.verifyPassword(password, hash);
|
|
459
|
-
if (!verified) {
|
|
460
|
-
throw new ApiError({
|
|
461
|
-
code: 400,
|
|
462
|
-
message: 'Invalid credentials',
|
|
463
|
-
errors: { password: 'Wrong password' }
|
|
458
|
+
message: 'Invalid credentials'
|
|
464
459
|
});
|
|
465
460
|
}
|
|
466
461
|
const pair = await this.issueTokens(apiReq, user, metadata);
|
|
@@ -502,6 +497,7 @@ class AuthModule extends BaseAuthModule {
|
|
|
502
497
|
refreshTtlSeconds: sessionPrefs.refreshTtlSeconds ?? stored.refreshTtlSeconds,
|
|
503
498
|
sessionCookie: sessionPrefs.sessionCookie ?? stored.sessionCookie
|
|
504
499
|
};
|
|
500
|
+
await this.storage.deleteToken({ refreshToken: providedToken });
|
|
505
501
|
const pair = await this.issueTokens(apiReq, user, metadata);
|
|
506
502
|
const publicUser = this.storage.filterUser(user);
|
|
507
503
|
return [200, { ...pair, user: publicUser }];
|
|
@@ -911,7 +907,7 @@ class AuthModule extends BaseAuthModule {
|
|
|
911
907
|
}
|
|
912
908
|
}
|
|
913
909
|
}
|
|
914
|
-
else if (!clientSecretProvided && client.clientSecret) {
|
|
910
|
+
else if (!clientSecretProvided && (client.hasSecret ?? Boolean(client.clientSecret))) {
|
|
915
911
|
throw new ApiError({ code: 400, message: 'Client authentication required when no PKCE challenge present' });
|
|
916
912
|
}
|
|
917
913
|
const user = await this.getUserOrThrow(record.userId, 'User not found');
|
|
@@ -946,6 +942,7 @@ class AuthModule extends BaseAuthModule {
|
|
|
946
942
|
throw new ApiError({ code: 400, message: 'Refresh token issued to another client' });
|
|
947
943
|
}
|
|
948
944
|
const user = await this.getUserOrThrow(stored.userId ?? verify.data.uid, 'User not found');
|
|
945
|
+
await this.storage.deleteToken({ refreshToken });
|
|
949
946
|
const tokens = await this.issueTokens(apiReq, user, {
|
|
950
947
|
clientId: client.clientId,
|
|
951
948
|
scope: stored.scope,
|
|
@@ -954,11 +951,7 @@ class AuthModule extends BaseAuthModule {
|
|
|
954
951
|
loginType: stored.loginType ?? 'oauth'
|
|
955
952
|
});
|
|
956
953
|
this.clearOAuthCookies(apiReq);
|
|
957
|
-
const scope = Array.isArray(stored.scope)
|
|
958
|
-
? stored.scope
|
|
959
|
-
: typeof stored.scope === 'string'
|
|
960
|
-
? stored.scope.split(/\s+/).filter((entry) => entry.length > 0)
|
|
961
|
-
: [];
|
|
954
|
+
const scope = Array.isArray(stored.scope) ? stored.scope : [];
|
|
962
955
|
return [200, this.buildTokenResponse(tokens, client, scope)];
|
|
963
956
|
}
|
|
964
957
|
clearOAuthCookies(apiReq) {
|
|
@@ -1021,7 +1014,7 @@ class AuthModule extends BaseAuthModule {
|
|
|
1021
1014
|
if (!client) {
|
|
1022
1015
|
throw new ApiError({ code: 400, message: 'Unknown client_id' });
|
|
1023
1016
|
}
|
|
1024
|
-
const requiresSecret =
|
|
1017
|
+
const requiresSecret = client.hasSecret ?? Boolean(client.clientSecret);
|
|
1025
1018
|
if (requiresSecret) {
|
|
1026
1019
|
if (!secretProvided) {
|
|
1027
1020
|
throw new ApiError({ code: 400, message: 'Client authentication is required' });
|
|
@@ -1057,17 +1050,10 @@ class AuthModule extends BaseAuthModule {
|
|
|
1057
1050
|
const password = toStringOrNull(body.password);
|
|
1058
1051
|
if (login && password) {
|
|
1059
1052
|
const user = await this.storage.getUser(login);
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const verified = await this.storage.verifyPassword(password, hash);
|
|
1065
|
-
if (!verified) {
|
|
1066
|
-
throw new ApiError({
|
|
1067
|
-
code: 400,
|
|
1068
|
-
message: 'Invalid credentials',
|
|
1069
|
-
errors: { password: 'Wrong password' }
|
|
1070
|
-
});
|
|
1053
|
+
const hash = user ? this.storage.getUserPasswordHash(user) : '';
|
|
1054
|
+
const verified = user ? await this.storage.verifyPassword(password, hash) : false;
|
|
1055
|
+
if (!user || !verified) {
|
|
1056
|
+
throw new ApiError({ code: 400, message: 'Invalid credentials' });
|
|
1071
1057
|
}
|
|
1072
1058
|
return user;
|
|
1073
1059
|
}
|
|
@@ -4,6 +4,7 @@ import { MemoryPasskeyStore } from '../passkey/memory.js';
|
|
|
4
4
|
import { MemoryTokenStore } from '../token/memory.js';
|
|
5
5
|
import { MemoryUserStore } from '../user/memory.js';
|
|
6
6
|
import { CompositeAuthAdapter } from './compat-auth-storage.js';
|
|
7
|
+
import { toOptionalStringId } from './user-id.js';
|
|
7
8
|
export class MemAuthStore {
|
|
8
9
|
constructor(params = {}) {
|
|
9
10
|
this.userStore = new MemoryUserStore({
|
|
@@ -70,16 +71,16 @@ export class MemAuthStore {
|
|
|
70
71
|
async getToken(query, opts) {
|
|
71
72
|
const normalized = {
|
|
72
73
|
...query,
|
|
73
|
-
userId:
|
|
74
|
-
ruid:
|
|
74
|
+
userId: toOptionalStringId(query.userId),
|
|
75
|
+
ruid: toOptionalStringId(query.ruid)
|
|
75
76
|
};
|
|
76
77
|
return this.adapter.getToken(normalized, opts);
|
|
77
78
|
}
|
|
78
79
|
async deleteToken(query) {
|
|
79
80
|
const normalized = {
|
|
80
81
|
...query,
|
|
81
|
-
userId:
|
|
82
|
-
ruid:
|
|
82
|
+
userId: toOptionalStringId(query.userId),
|
|
83
|
+
ruid: toOptionalStringId(query.ruid)
|
|
83
84
|
};
|
|
84
85
|
return this.adapter.deleteToken(normalized);
|
|
85
86
|
}
|