@technomoron/apicore-server 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/cjs/api-module.cjs +34 -0
- package/dist/cjs/api-module.d.ts +45 -0
- package/dist/cjs/apicore-server.cjs +1561 -0
- package/dist/cjs/apicore-server.d.ts +288 -0
- package/dist/cjs/auth-api/auth-module.cjs +1248 -0
- package/dist/cjs/auth-api/auth-module.d.ts +116 -0
- package/dist/cjs/auth-api/compat-auth-storage.cjs +128 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/cjs/auth-api/mem-auth-store.cjs +121 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/cjs/auth-api/module.cjs +25 -0
- package/dist/cjs/auth-api/module.d.ts +20 -0
- package/dist/cjs/auth-api/schemas.cjs +171 -0
- package/dist/cjs/auth-api/schemas.d.ts +21 -0
- package/dist/cjs/auth-api/sql-auth-store.cjs +179 -0
- package/dist/cjs/auth-api/sql-auth-store.d.ts +87 -0
- package/dist/cjs/auth-api/storage.cjs +102 -0
- package/dist/cjs/auth-api/storage.d.ts +38 -0
- package/dist/cjs/auth-api/types.cjs +2 -0
- package/dist/cjs/auth-api/types.d.ts +34 -0
- package/dist/cjs/auth-api/user-id.cjs +47 -0
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-cookie-options.cjs +66 -0
- package/dist/cjs/auth-cookie-options.d.ts +13 -0
- package/dist/cjs/base/client-info.cjs +285 -0
- package/dist/cjs/base/client-info.d.ts +27 -0
- package/dist/cjs/base/error-utils.cjs +50 -0
- package/dist/cjs/base/error-utils.d.ts +16 -0
- package/dist/cjs/base/request-utils.cjs +27 -0
- package/dist/cjs/base/request-utils.d.ts +8 -0
- package/dist/cjs/index.cjs +51 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/limiter/auth-rate-limiter.cjs +35 -0
- package/dist/cjs/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/cjs/limiter/fixed-window.cjs +41 -0
- package/dist/cjs/limiter/fixed-window.d.ts +11 -0
- package/dist/cjs/oauth/base.cjs +7 -0
- package/dist/cjs/oauth/base.d.ts +17 -0
- package/dist/cjs/oauth/memory.cjs +135 -0
- package/dist/cjs/oauth/memory.d.ts +22 -0
- package/dist/cjs/oauth/models.cjs +47 -0
- package/dist/cjs/oauth/models.d.ts +50 -0
- package/dist/cjs/oauth/sequelize.cjs +159 -0
- package/dist/cjs/oauth/sequelize.d.ts +30 -0
- package/dist/cjs/oauth/types.cjs +3 -0
- package/dist/cjs/oauth/types.d.ts +51 -0
- package/dist/cjs/passkey/base.cjs +7 -0
- package/dist/cjs/passkey/base.d.ts +28 -0
- package/dist/cjs/passkey/config.cjs +26 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/memory.cjs +123 -0
- package/dist/cjs/passkey/memory.d.ts +34 -0
- package/dist/cjs/passkey/models.cjs +142 -0
- package/dist/cjs/passkey/models.d.ts +34 -0
- package/dist/cjs/passkey/sequelize.cjs +126 -0
- package/dist/cjs/passkey/sequelize.d.ts +42 -0
- package/dist/cjs/passkey/service.cjs +413 -0
- package/dist/cjs/passkey/service.d.ts +21 -0
- package/dist/cjs/passkey/types.cjs +2 -0
- package/dist/cjs/passkey/types.d.ts +84 -0
- package/dist/cjs/sequelize-utils.cjs +56 -0
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/token/base.cjs +120 -0
- package/dist/cjs/token/base.d.ts +46 -0
- package/dist/cjs/token/memory.cjs +234 -0
- package/dist/cjs/token/memory.d.ts +29 -0
- package/dist/cjs/token/sequelize.cjs +400 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/types.cjs +2 -0
- package/dist/cjs/token/types.d.ts +34 -0
- package/dist/cjs/upload/memory.cjs +92 -0
- package/dist/cjs/upload/memory.d.ts +17 -0
- package/dist/cjs/upload/tus-module.cjs +270 -0
- package/dist/cjs/upload/tus-module.d.ts +38 -0
- package/dist/cjs/upload/types.cjs +2 -0
- package/dist/cjs/upload/types.d.ts +28 -0
- package/dist/cjs/user/base.cjs +53 -0
- package/dist/cjs/user/base.d.ts +36 -0
- package/dist/cjs/user/memory.cjs +194 -0
- package/dist/cjs/user/memory.d.ts +37 -0
- package/dist/cjs/user/sequelize.cjs +194 -0
- package/dist/cjs/user/sequelize.d.ts +46 -0
- package/dist/cjs/user/types.cjs +2 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/esm/api-module.d.ts +45 -0
- package/dist/esm/api-module.js +30 -0
- package/dist/esm/apicore-server.d.ts +288 -0
- package/dist/esm/apicore-server.js +1552 -0
- package/dist/esm/auth-api/auth-module.d.ts +116 -0
- package/dist/esm/auth-api/auth-module.js +1246 -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 +117 -0
- package/dist/esm/auth-api/module.d.ts +20 -0
- package/dist/esm/auth-api/module.js +21 -0
- package/dist/esm/auth-api/schemas.d.ts +21 -0
- package/dist/esm/auth-api/schemas.js +168 -0
- package/dist/esm/auth-api/sql-auth-store.d.ts +87 -0
- package/dist/esm/auth-api/sql-auth-store.js +175 -0
- package/dist/esm/auth-api/storage.d.ts +38 -0
- package/dist/esm/auth-api/storage.js +98 -0
- package/dist/esm/auth-api/types.d.ts +34 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +41 -0
- package/dist/esm/auth-cookie-options.d.ts +13 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/base/client-info.d.ts +27 -0
- package/dist/esm/base/client-info.js +282 -0
- package/dist/esm/base/error-utils.d.ts +16 -0
- package/dist/esm/base/error-utils.js +44 -0
- package/dist/esm/base/request-utils.d.ts +8 -0
- package/dist/esm/base/request-utils.js +23 -0
- package/dist/esm/index.d.ts +34 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/esm/limiter/auth-rate-limiter.js +32 -0
- package/dist/esm/limiter/fixed-window.d.ts +11 -0
- package/dist/esm/limiter/fixed-window.js +37 -0
- package/dist/esm/oauth/base.d.ts +17 -0
- package/dist/esm/oauth/base.js +3 -0
- package/dist/esm/oauth/memory.d.ts +22 -0
- package/dist/esm/oauth/memory.js +128 -0
- package/dist/esm/oauth/models.d.ts +50 -0
- package/dist/esm/oauth/models.js +38 -0
- package/dist/esm/oauth/sequelize.d.ts +30 -0
- package/dist/esm/oauth/sequelize.js +148 -0
- package/dist/esm/oauth/types.d.ts +51 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +28 -0
- package/dist/esm/passkey/base.js +3 -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 +34 -0
- package/dist/esm/passkey/memory.js +119 -0
- package/dist/esm/passkey/models.d.ts +34 -0
- package/dist/esm/passkey/models.js +135 -0
- package/dist/esm/passkey/sequelize.d.ts +42 -0
- package/dist/esm/passkey/sequelize.js +122 -0
- package/dist/esm/passkey/service.d.ts +21 -0
- package/dist/esm/passkey/service.js +376 -0
- package/dist/esm/passkey/types.d.ts +84 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +47 -0
- package/dist/esm/token/base.d.ts +46 -0
- package/dist/esm/token/base.js +113 -0
- package/dist/esm/token/memory.d.ts +29 -0
- package/dist/esm/token/memory.js +230 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +396 -0
- package/dist/esm/token/types.d.ts +34 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/upload/memory.d.ts +17 -0
- package/dist/esm/upload/memory.js +86 -0
- package/dist/esm/upload/tus-module.d.ts +38 -0
- package/dist/esm/upload/tus-module.js +266 -0
- package/dist/esm/upload/types.d.ts +28 -0
- package/dist/esm/upload/types.js +1 -0
- package/dist/esm/user/base.d.ts +36 -0
- package/dist/esm/user/base.js +46 -0
- package/dist/esm/user/memory.d.ts +37 -0
- package/dist/esm/user/memory.js +190 -0
- package/dist/esm/user/sequelize.d.ts +46 -0
- package/dist/esm/user/sequelize.js +188 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/docs/swagger/openapi.json +2162 -0
- package/package.json +131 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SequelizeUserStore = exports.AuthUserModel = void 0;
|
|
4
|
+
exports.initAuthUserModel = initAuthUserModel;
|
|
5
|
+
const sequelize_1 = require("sequelize");
|
|
6
|
+
const user_id_js_1 = require("../auth-api/user-id.cjs");
|
|
7
|
+
const sequelize_utils_js_1 = require("../sequelize-utils.cjs");
|
|
8
|
+
const base_js_1 = require("./base.cjs");
|
|
9
|
+
class AuthUserModel extends sequelize_1.Model {
|
|
10
|
+
}
|
|
11
|
+
exports.AuthUserModel = AuthUserModel;
|
|
12
|
+
function initAuthUserModel(sequelize, options = {}) {
|
|
13
|
+
const idType = (0, sequelize_utils_js_1.integerIdType)(sequelize);
|
|
14
|
+
AuthUserModel.init({
|
|
15
|
+
user_id: {
|
|
16
|
+
type: idType,
|
|
17
|
+
autoIncrement: true,
|
|
18
|
+
allowNull: false,
|
|
19
|
+
primaryKey: true
|
|
20
|
+
},
|
|
21
|
+
login: {
|
|
22
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
23
|
+
allowNull: false,
|
|
24
|
+
unique: true
|
|
25
|
+
},
|
|
26
|
+
email: {
|
|
27
|
+
type: sequelize_1.DataTypes.STRING(100),
|
|
28
|
+
allowNull: false,
|
|
29
|
+
unique: true
|
|
30
|
+
},
|
|
31
|
+
password: {
|
|
32
|
+
type: sequelize_1.DataTypes.STRING(255),
|
|
33
|
+
allowNull: false
|
|
34
|
+
}
|
|
35
|
+
}, {
|
|
36
|
+
...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'users', options.tablePrefix, { timestamps: false })
|
|
37
|
+
});
|
|
38
|
+
return AuthUserModel;
|
|
39
|
+
}
|
|
40
|
+
class SequelizeUserStore extends base_js_1.UserStore {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
super({
|
|
43
|
+
toPublic: options.toPublic,
|
|
44
|
+
bcryptRounds: options.bcryptRounds,
|
|
45
|
+
bcryptPepper: options.bcryptPepper
|
|
46
|
+
});
|
|
47
|
+
if (!options?.sequelize) {
|
|
48
|
+
throw new Error('SequelizeUserStore requires a configured Sequelize instance');
|
|
49
|
+
}
|
|
50
|
+
this.Users = options.userModel
|
|
51
|
+
? options.userModel
|
|
52
|
+
: (options.userModelFactory ?? initAuthUserModel)(options.sequelize, {
|
|
53
|
+
tablePrefix: options.tablePrefix
|
|
54
|
+
});
|
|
55
|
+
this.recordMapper =
|
|
56
|
+
options.recordMapper ??
|
|
57
|
+
((model) => SequelizeUserStore.mapModelToUser(model));
|
|
58
|
+
}
|
|
59
|
+
async findUser(identifier) {
|
|
60
|
+
const where = [];
|
|
61
|
+
try {
|
|
62
|
+
where.push({ user_id: this.normalizeUserId(identifier) });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
if (typeof identifier === 'string') {
|
|
68
|
+
where.push({ login: identifier });
|
|
69
|
+
where.push({ email: identifier });
|
|
70
|
+
}
|
|
71
|
+
if (where.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const model = await this.Users.findOne({ where: { [sequelize_1.Op.or]: where } });
|
|
75
|
+
return model ? this.toUserRecord(model) : null;
|
|
76
|
+
}
|
|
77
|
+
async findById(id) {
|
|
78
|
+
try {
|
|
79
|
+
const model = await this.Users.findByPk(this.normalizeUserId(id));
|
|
80
|
+
return model ? this.toUserRecord(model) : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async findByLoginOrEmail(loginOrEmail) {
|
|
87
|
+
const model = await this.Users.findOne({
|
|
88
|
+
where: { [sequelize_1.Op.or]: [{ login: loginOrEmail }, { email: loginOrEmail }] }
|
|
89
|
+
});
|
|
90
|
+
return model ? this.toUserRecord(model) : null;
|
|
91
|
+
}
|
|
92
|
+
async createUser(input) {
|
|
93
|
+
const normalized = this.normalizeUserInput(input);
|
|
94
|
+
const { password, ...rest } = normalized;
|
|
95
|
+
const defaults = {
|
|
96
|
+
...rest,
|
|
97
|
+
password: password ? await this.hashPassword(password) : ''
|
|
98
|
+
};
|
|
99
|
+
const providedId = input.user_id;
|
|
100
|
+
if (providedId !== undefined && providedId !== null && Number.isFinite(providedId)) {
|
|
101
|
+
defaults.user_id = Number(providedId);
|
|
102
|
+
}
|
|
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
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async upsertUser(input) {
|
|
115
|
+
const normalized = this.normalizeUserInput(input);
|
|
116
|
+
const providedId = input.user_id;
|
|
117
|
+
if (providedId !== undefined) {
|
|
118
|
+
const model = await this.Users.findByPk(Number(providedId));
|
|
119
|
+
if (!model) {
|
|
120
|
+
throw new Error(`User ${String(providedId)} not found`);
|
|
121
|
+
}
|
|
122
|
+
const next = { ...normalized };
|
|
123
|
+
if (normalized.password && normalized.password.length > 0) {
|
|
124
|
+
next.password = await this.hashPassword(normalized.password);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
delete next.password;
|
|
128
|
+
}
|
|
129
|
+
await model.set(next);
|
|
130
|
+
try {
|
|
131
|
+
await model.save();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof sequelize_1.UniqueConstraintError) {
|
|
135
|
+
throw new Error(`User with login ${normalized.login} or email ${normalized.email} already exists`);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
return this.toUserRecord(model);
|
|
140
|
+
}
|
|
141
|
+
return this.createUser(input);
|
|
142
|
+
}
|
|
143
|
+
async updateUser(id, patch) {
|
|
144
|
+
const model = await this.Users.findByPk(this.normalizeUserId(id));
|
|
145
|
+
if (!model) {
|
|
146
|
+
throw new Error(`User ${String(id)} not found`);
|
|
147
|
+
}
|
|
148
|
+
const updates = { ...patch };
|
|
149
|
+
if (patch.password && patch.password.length > 0) {
|
|
150
|
+
updates.password = await this.hashPassword(patch.password);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
delete updates.password;
|
|
154
|
+
}
|
|
155
|
+
await model.set(updates);
|
|
156
|
+
try {
|
|
157
|
+
await model.save();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error instanceof sequelize_1.UniqueConstraintError) {
|
|
161
|
+
throw new Error(`User with login or email already exists`);
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
return this.toUserRecord(model);
|
|
166
|
+
}
|
|
167
|
+
async setPasswordHash(id, hash) {
|
|
168
|
+
const [affected] = await this.Users.update({ password: hash }, { where: { user_id: this.normalizeUserId(id) } });
|
|
169
|
+
if (affected === 0) {
|
|
170
|
+
throw new Error(`User ${String(id)} not found`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
getPasswordHash(user) {
|
|
174
|
+
return user.password;
|
|
175
|
+
}
|
|
176
|
+
getUserId(user) {
|
|
177
|
+
return user.user_id;
|
|
178
|
+
}
|
|
179
|
+
toUserRecord(model) {
|
|
180
|
+
return this.recordMapper(model);
|
|
181
|
+
}
|
|
182
|
+
static mapModelToUser(model) {
|
|
183
|
+
return {
|
|
184
|
+
user_id: model.get('user_id'),
|
|
185
|
+
login: model.get('login'),
|
|
186
|
+
email: model.get('email'),
|
|
187
|
+
password: model.get('password')
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
normalizeUserId(identifier) {
|
|
191
|
+
return (0, user_id_js_1.normalizeNumericUserId)(identifier);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
exports.SequelizeUserStore = SequelizeUserStore;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CreationOptional, Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
+
import { UserStore } from './base.js';
|
|
3
|
+
import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
|
|
4
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
5
|
+
export declare class AuthUserModel extends Model<InferAttributes<AuthUserModel>, InferCreationAttributes<AuthUserModel>> implements InferAttributes<AuthUserModel> {
|
|
6
|
+
user_id: CreationOptional<number>;
|
|
7
|
+
login: string;
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
export type AuthUserAttributes = InferAttributes<AuthUserModel>;
|
|
12
|
+
export type AuthUserCreationAttributes = InferCreationAttributes<AuthUserModel>;
|
|
13
|
+
export declare function initAuthUserModel(sequelize: Sequelize, options?: {
|
|
14
|
+
tablePrefix?: string;
|
|
15
|
+
}): typeof AuthUserModel;
|
|
16
|
+
export type GenericUserModel = Model<Record<string, unknown>, Record<string, unknown>>;
|
|
17
|
+
export type GenericUserModelStatic = ModelStatic<GenericUserModel>;
|
|
18
|
+
export interface SequelizeUserStoreOptions<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> {
|
|
19
|
+
bcryptRounds?: number;
|
|
20
|
+
bcryptPepper?: string;
|
|
21
|
+
sequelize: Sequelize;
|
|
22
|
+
tablePrefix?: string;
|
|
23
|
+
userModel?: GenericUserModelStatic;
|
|
24
|
+
userModelFactory?: (sequelize: Sequelize, options?: {
|
|
25
|
+
tablePrefix?: string;
|
|
26
|
+
}) => GenericUserModelStatic;
|
|
27
|
+
recordMapper?: (model: GenericUserModel) => UserAttributes;
|
|
28
|
+
toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
|
|
29
|
+
}
|
|
30
|
+
export declare class SequelizeUserStore<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
|
|
31
|
+
readonly Users: GenericUserModelStatic;
|
|
32
|
+
private readonly recordMapper;
|
|
33
|
+
constructor(options: SequelizeUserStoreOptions<UserAttributes, PublicUserShape>);
|
|
34
|
+
findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
|
|
35
|
+
findById(id: AuthIdentifier): Promise<UserAttributes | null>;
|
|
36
|
+
findByLoginOrEmail(loginOrEmail: string): Promise<UserAttributes | null>;
|
|
37
|
+
createUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
38
|
+
upsertUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
39
|
+
updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<UserAttributes>;
|
|
40
|
+
setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
41
|
+
getPasswordHash(user: UserAttributes): string | null;
|
|
42
|
+
getUserId(user: UserAttributes): AuthIdentifier;
|
|
43
|
+
protected toUserRecord(model: GenericUserModel): UserAttributes;
|
|
44
|
+
private static mapModelToUser;
|
|
45
|
+
private normalizeUserId;
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BcryptHasherOptions {
|
|
2
|
+
rounds?: number;
|
|
3
|
+
pepper?: string;
|
|
4
|
+
}
|
|
5
|
+
export type CreateUserInput = Record<string, unknown> & {
|
|
6
|
+
login: string;
|
|
7
|
+
email: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
};
|
|
10
|
+
export type UpdateUserInput = Partial<CreateUserInput>;
|
|
11
|
+
export type PublicUserMapper<User, PublicUser> = (user: User) => PublicUser;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ApiRequest } from './apicore-server.js';
|
|
2
|
+
export type ApiHandlerResult<Data = unknown> = [number] | [number, Data] | [number, Data, string];
|
|
3
|
+
export type ApiHandler<Data = unknown> = (apiReq: ApiRequest) => Promise<ApiHandlerResult<Data>>;
|
|
4
|
+
export type ApiAuthType = 'none' | 'maybe' | 'yes' | 'strict' | 'apikey';
|
|
5
|
+
export type ApiAuthClass = 'any' | 'admin';
|
|
6
|
+
export interface ApiKey {
|
|
7
|
+
/** Real user identity — who the key belongs to. */
|
|
8
|
+
uid: unknown;
|
|
9
|
+
/** Effective user identity — who the key acts as. When set, the request is treated as impersonation (isImpersonating() === true). */
|
|
10
|
+
euid?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export type ApiRoute = {
|
|
13
|
+
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
14
|
+
path: string;
|
|
15
|
+
handler: ApiHandler;
|
|
16
|
+
auth: {
|
|
17
|
+
type: ApiAuthType;
|
|
18
|
+
req: ApiAuthClass;
|
|
19
|
+
};
|
|
20
|
+
schema?: {
|
|
21
|
+
body?: Record<string, unknown>;
|
|
22
|
+
querystring?: Record<string, unknown>;
|
|
23
|
+
params?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** Base class for feature modules mounted into {@link ApiServer}. */
|
|
27
|
+
export declare class ApiModule<T = unknown> {
|
|
28
|
+
private _server?;
|
|
29
|
+
/** The server instance this module is mounted on. */
|
|
30
|
+
get server(): T;
|
|
31
|
+
set server(value: T);
|
|
32
|
+
/** Route namespace under `apiBasePath`. */
|
|
33
|
+
namespace: string;
|
|
34
|
+
mountpath: string;
|
|
35
|
+
static defaultNamespace: string;
|
|
36
|
+
constructor(opts?: {
|
|
37
|
+
namespace?: string;
|
|
38
|
+
});
|
|
39
|
+
/** Optional pre-mount configuration validation hook. */
|
|
40
|
+
checkConfig(): boolean;
|
|
41
|
+
/** Return all routes exposed by this module. */
|
|
42
|
+
defineRoutes(): ApiRoute[];
|
|
43
|
+
/** Optional mount-time hook for modules that need direct server integration. */
|
|
44
|
+
onMount(): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Base class for feature modules mounted into {@link ApiServer}. */
|
|
2
|
+
export class ApiModule {
|
|
3
|
+
/** The server instance this module is mounted on. */
|
|
4
|
+
get server() {
|
|
5
|
+
if (this._server === undefined) {
|
|
6
|
+
throw new Error('ApiModule.server is not set. Mount the module with ApiServer.api(...) before using it.');
|
|
7
|
+
}
|
|
8
|
+
return this._server;
|
|
9
|
+
}
|
|
10
|
+
set server(value) {
|
|
11
|
+
this._server = value;
|
|
12
|
+
}
|
|
13
|
+
constructor(opts = {}) {
|
|
14
|
+
this.mountpath = '';
|
|
15
|
+
this.namespace = opts.namespace ?? this.constructor.defaultNamespace ?? '';
|
|
16
|
+
}
|
|
17
|
+
/** Optional pre-mount configuration validation hook. */
|
|
18
|
+
checkConfig() {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/** Return all routes exposed by this module. */
|
|
22
|
+
defineRoutes() {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
/** Optional mount-time hook for modules that need direct server integration. */
|
|
26
|
+
onMount() {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
ApiModule.defaultNamespace = '';
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bjørn Erik Jacobsen / Technomoron
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify';
|
|
8
|
+
import { ApiModule } from './api-module.js';
|
|
9
|
+
import { type ClientInfo } from './base/client-info.js';
|
|
10
|
+
import { TokenStore, type JwtDecodeResult, type JwtSignPayload, type JwtSignResult, type JwtVerifyResult } from './token/base.js';
|
|
11
|
+
import type { ApiAuthClass, ApiAuthType, ApiHandler, ApiKey } from './api-module.js';
|
|
12
|
+
import type { AuthProviderModule } from './auth-api/module.js';
|
|
13
|
+
import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
|
|
14
|
+
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
|
|
15
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
16
|
+
import type { Token } from './token/types.js';
|
|
17
|
+
import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
18
|
+
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
19
|
+
export type { ClientAgentProfile, ClientInfo } from './base/client-info.js';
|
|
20
|
+
export interface ApiCookieOptions {
|
|
21
|
+
httpOnly?: boolean;
|
|
22
|
+
secure?: boolean;
|
|
23
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
24
|
+
domain?: string;
|
|
25
|
+
path?: string;
|
|
26
|
+
maxAge?: number;
|
|
27
|
+
expires?: Date;
|
|
28
|
+
}
|
|
29
|
+
export interface ApiUploadedFile {
|
|
30
|
+
fieldname: string;
|
|
31
|
+
originalname: string;
|
|
32
|
+
encoding: string;
|
|
33
|
+
mimetype: string;
|
|
34
|
+
size?: number;
|
|
35
|
+
buffer?: Buffer;
|
|
36
|
+
filepath?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ExtendedReq {
|
|
39
|
+
method: string;
|
|
40
|
+
url: string;
|
|
41
|
+
originalUrl?: string;
|
|
42
|
+
headers: Record<string, string | string[] | undefined>;
|
|
43
|
+
query: Record<string, unknown>;
|
|
44
|
+
body: unknown;
|
|
45
|
+
params: Record<string, unknown>;
|
|
46
|
+
cookies?: Record<string, string>;
|
|
47
|
+
ip?: string;
|
|
48
|
+
ips?: string[];
|
|
49
|
+
socket?: {
|
|
50
|
+
remoteAddress?: string;
|
|
51
|
+
};
|
|
52
|
+
protocol?: string;
|
|
53
|
+
file?: ApiUploadedFile;
|
|
54
|
+
files?: ApiUploadedFile[] | Record<string, ApiUploadedFile[]>;
|
|
55
|
+
apiReq?: ApiRequest;
|
|
56
|
+
}
|
|
57
|
+
export interface ApiResponse {
|
|
58
|
+
locals: Record<string, unknown>;
|
|
59
|
+
headersSent: boolean;
|
|
60
|
+
status: (code: number) => ApiResponse;
|
|
61
|
+
json: (payload: unknown) => void;
|
|
62
|
+
send: (payload: unknown) => void;
|
|
63
|
+
cookie: (name: string, value: string, options?: ApiCookieOptions) => void;
|
|
64
|
+
clearCookie: (name: string, options?: ApiCookieOptions) => void;
|
|
65
|
+
}
|
|
66
|
+
export interface ApiTokenData extends JwtPayload, Partial<Token> {
|
|
67
|
+
uid: unknown;
|
|
68
|
+
iat?: number;
|
|
69
|
+
exp?: number;
|
|
70
|
+
}
|
|
71
|
+
export type ApiAuthMethod = 'bearer' | 'cookie' | 'param' | 'apikey' | null;
|
|
72
|
+
export interface ApiRequest {
|
|
73
|
+
server: ApiServer;
|
|
74
|
+
req: ExtendedReq;
|
|
75
|
+
res: ApiResponse;
|
|
76
|
+
tokenData?: ApiTokenData | null;
|
|
77
|
+
token?: string;
|
|
78
|
+
authToken?: Token | null;
|
|
79
|
+
apiKey?: ApiKey | null;
|
|
80
|
+
authMethod?: ApiAuthMethod;
|
|
81
|
+
clientInfo?: ClientInfo;
|
|
82
|
+
realUid?: AuthIdentifier | null;
|
|
83
|
+
getClientInfo: () => ClientInfo;
|
|
84
|
+
getClientIp: () => string | null;
|
|
85
|
+
getClientIpChain: () => string[];
|
|
86
|
+
getRealUid: () => AuthIdentifier | null;
|
|
87
|
+
isImpersonating: () => boolean;
|
|
88
|
+
}
|
|
89
|
+
export type ExpressApiRequest = ExtendedReq;
|
|
90
|
+
export interface ExpressApiLocals {
|
|
91
|
+
apiReq?: ApiRequest;
|
|
92
|
+
}
|
|
93
|
+
type CompatNext = (error?: unknown) => void;
|
|
94
|
+
type CompatRequestHandler = (req: ExtendedReq, res: ApiResponse, next: CompatNext) => unknown;
|
|
95
|
+
type CompatErrorHandler = (err: unknown, req: ExtendedReq, res: ApiResponse, next: CompatNext) => unknown;
|
|
96
|
+
export { ApiModule } from './api-module.js';
|
|
97
|
+
export type { ApiHandler, ApiAuthType, ApiAuthClass, ApiRoute, ApiKey } from './api-module.js';
|
|
98
|
+
export interface ApiErrorParams {
|
|
99
|
+
code?: number;
|
|
100
|
+
message?: unknown;
|
|
101
|
+
data?: unknown;
|
|
102
|
+
errors?: Record<string, string>;
|
|
103
|
+
}
|
|
104
|
+
export declare class ApiError extends Error {
|
|
105
|
+
code: number;
|
|
106
|
+
data: unknown;
|
|
107
|
+
errors: Record<string, string>;
|
|
108
|
+
constructor({ code, message, data, errors }: ApiErrorParams);
|
|
109
|
+
}
|
|
110
|
+
export interface ApiServerConf {
|
|
111
|
+
apiPort: number;
|
|
112
|
+
apiHost: string;
|
|
113
|
+
uploadPath: string;
|
|
114
|
+
uploadMax: number;
|
|
115
|
+
staticDirs?: Record<string, string>;
|
|
116
|
+
origins: string[];
|
|
117
|
+
debug: boolean;
|
|
118
|
+
apiBasePath: string;
|
|
119
|
+
swaggerEnabled?: boolean;
|
|
120
|
+
swaggerPath?: string;
|
|
121
|
+
accessSecret: string;
|
|
122
|
+
refreshSecret: string;
|
|
123
|
+
cookieDomain: string;
|
|
124
|
+
cookiePath?: string;
|
|
125
|
+
cookieSameSite?: 'lax' | 'strict' | 'none';
|
|
126
|
+
cookieSecure?: boolean | 'auto';
|
|
127
|
+
cookieHttpOnly?: boolean;
|
|
128
|
+
apiKeyPrefix: string;
|
|
129
|
+
apiKeyEnabled: boolean;
|
|
130
|
+
tokenParam: string;
|
|
131
|
+
tokenParamLocation: 'body' | 'query' | 'body-query';
|
|
132
|
+
accessCookie: string;
|
|
133
|
+
refreshCookie: string;
|
|
134
|
+
accessExpiry: number;
|
|
135
|
+
refreshExpiry: number;
|
|
136
|
+
sessionRefreshExpiry: number;
|
|
137
|
+
authApi: boolean;
|
|
138
|
+
devMode: boolean;
|
|
139
|
+
hydrateGetBody: boolean;
|
|
140
|
+
validateTokens: boolean;
|
|
141
|
+
refreshMaybe: boolean;
|
|
142
|
+
apiVersion: string;
|
|
143
|
+
minClientVersion: string;
|
|
144
|
+
tokenStore?: TokenStore;
|
|
145
|
+
onStartError?: (error: Error) => void;
|
|
146
|
+
/**
|
|
147
|
+
* Controls trust in proxy headers (X-Forwarded-For, Forwarded, X-Real-IP).
|
|
148
|
+
* - `true` (default): trust all forwarded headers
|
|
149
|
+
* - `false`: only use the socket address, ignore forwarded headers
|
|
150
|
+
* - number: trust that many rightmost forwarded entries (e.g., 1 = one reverse proxy)
|
|
151
|
+
*/
|
|
152
|
+
trustProxy: boolean | number;
|
|
153
|
+
}
|
|
154
|
+
/** Core Fastify-based API server with module mounting and auth integration hooks. */
|
|
155
|
+
export declare class ApiServer {
|
|
156
|
+
readonly app: ((req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void) & {
|
|
157
|
+
listen: (...args: unknown[]) => import('node:http').Server;
|
|
158
|
+
};
|
|
159
|
+
readonly fastify: FastifyInstance;
|
|
160
|
+
readonly config: ApiServerConf;
|
|
161
|
+
readonly startedAt: number;
|
|
162
|
+
private readonly apiBasePath;
|
|
163
|
+
private finalized;
|
|
164
|
+
private storageAdapter;
|
|
165
|
+
private moduleAdapter;
|
|
166
|
+
private apiErrorHandlerInstalled;
|
|
167
|
+
private tokenStoreAdapter;
|
|
168
|
+
private readonly jwtHelper;
|
|
169
|
+
private compatGlobalErrorHandler;
|
|
170
|
+
private readonly readyPromise;
|
|
171
|
+
private currReqDeprecationWarned;
|
|
172
|
+
get currReq(): ApiRequest | null;
|
|
173
|
+
set currReq(_value: ApiRequest | null);
|
|
174
|
+
constructor(config?: Partial<ApiServerConf>);
|
|
175
|
+
private setupRuntime;
|
|
176
|
+
private readRawBody;
|
|
177
|
+
private parseRawBody;
|
|
178
|
+
private assertNotFinalized;
|
|
179
|
+
finalize(): this;
|
|
180
|
+
authStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
|
|
181
|
+
useAuthStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
|
|
182
|
+
authModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
183
|
+
useAuthModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
184
|
+
getAuthStorage<UserRow = unknown, SafeUser = unknown>(): AuthAdapter<UserRow, SafeUser>;
|
|
185
|
+
getAuthModule<UserRow = unknown>(): AuthProviderModule<UserRow>;
|
|
186
|
+
setTokenStore(store: TokenStore): this;
|
|
187
|
+
getTokenStore(): TokenStore | null;
|
|
188
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
189
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
190
|
+
getUser(identifier: AuthIdentifier): Promise<unknown | null>;
|
|
191
|
+
getUserPasswordHash(user: unknown): string;
|
|
192
|
+
getUserId(user: unknown): AuthIdentifier;
|
|
193
|
+
filterUser(user: unknown): unknown;
|
|
194
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
195
|
+
storeToken(data: Token): Promise<void>;
|
|
196
|
+
getToken(query: Partial<Token> & {
|
|
197
|
+
userId?: AuthIdentifier;
|
|
198
|
+
ruid?: AuthIdentifier;
|
|
199
|
+
}, opts?: {
|
|
200
|
+
includeExpired?: boolean;
|
|
201
|
+
}): Promise<Token | null>;
|
|
202
|
+
deleteToken(query: Partial<Token> & {
|
|
203
|
+
userId?: AuthIdentifier;
|
|
204
|
+
ruid?: AuthIdentifier;
|
|
205
|
+
}): Promise<number>;
|
|
206
|
+
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
207
|
+
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
208
|
+
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
209
|
+
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
210
|
+
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
211
|
+
consumeAuthCode(code: string, clientId: string): Promise<AuthCodeData | null>;
|
|
212
|
+
canImpersonate(params: {
|
|
213
|
+
realUserId: AuthIdentifier;
|
|
214
|
+
effectiveUserId: AuthIdentifier;
|
|
215
|
+
}): Promise<boolean>;
|
|
216
|
+
jwtSign(payload: JwtSignPayload, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
217
|
+
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
218
|
+
jwtDecode<T>(token: string, options?: import('jsonwebtoken').DecodeOptions): JwtDecodeResult<T>;
|
|
219
|
+
getApiKey<T = ApiKey>(token: string): Promise<T | null>;
|
|
220
|
+
authenticateUser(params: {
|
|
221
|
+
login: string;
|
|
222
|
+
password: string;
|
|
223
|
+
}): Promise<boolean>;
|
|
224
|
+
updateToken(updates: Partial<Token> & {
|
|
225
|
+
refreshToken: string;
|
|
226
|
+
}): Promise<boolean>;
|
|
227
|
+
guessExceptionText(error: unknown, defMsg?: string): string;
|
|
228
|
+
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
229
|
+
/**
|
|
230
|
+
* Authenticate and authorise an incoming Fastify request outside of the
|
|
231
|
+
* standard `defineRoutes` pipeline (e.g. TUS upload routes that need full
|
|
232
|
+
* control over their own response format).
|
|
233
|
+
*
|
|
234
|
+
* Throws `ApiError` on auth failure — callers should catch it and respond
|
|
235
|
+
* with the appropriate HTTP status code.
|
|
236
|
+
*/
|
|
237
|
+
resolveRequest(request: FastifyRequest, reply: FastifyReply, auth: {
|
|
238
|
+
type: ApiAuthType;
|
|
239
|
+
req?: ApiAuthClass;
|
|
240
|
+
}): Promise<ApiRequest>;
|
|
241
|
+
private installStaticDirs;
|
|
242
|
+
private installPingHandler;
|
|
243
|
+
private loadSwaggerSpec;
|
|
244
|
+
private readPackageVersion;
|
|
245
|
+
private installSwaggerHandler;
|
|
246
|
+
private normalizeApiBasePath;
|
|
247
|
+
private installApiNotFoundHandler;
|
|
248
|
+
private installApiErrorHandler;
|
|
249
|
+
start(): this;
|
|
250
|
+
private internalServerErrorMessage;
|
|
251
|
+
private logUnhandledError;
|
|
252
|
+
private verifyJWT;
|
|
253
|
+
private jwtCookieOptions;
|
|
254
|
+
private setAccessCookie;
|
|
255
|
+
private tryRefreshAccessToken;
|
|
256
|
+
private authenticate;
|
|
257
|
+
private tryAuthenticateApiKey;
|
|
258
|
+
private resolveTokenFromRequest;
|
|
259
|
+
private readNamedValue;
|
|
260
|
+
private requiresAuthToken;
|
|
261
|
+
private shouldValidateStoredToken;
|
|
262
|
+
private assertStoredAccessToken;
|
|
263
|
+
private normalizeAuthIdentifier;
|
|
264
|
+
private extractTokenUserId;
|
|
265
|
+
private resolveRealUserId;
|
|
266
|
+
private toExtendedReq;
|
|
267
|
+
private createApiRequest;
|
|
268
|
+
useExpress(path: string, ...handlers: Array<CompatRequestHandler | CompatErrorHandler>): this;
|
|
269
|
+
useExpress(...handlers: Array<CompatRequestHandler | CompatErrorHandler>): this;
|
|
270
|
+
private runCompatHandlers;
|
|
271
|
+
private runCompatErrorHandlers;
|
|
272
|
+
expressAuth(auth: {
|
|
273
|
+
type: ApiAuthType;
|
|
274
|
+
req: ApiAuthClass;
|
|
275
|
+
}): CompatRequestHandler;
|
|
276
|
+
expressErrorHandler(): CompatErrorHandler;
|
|
277
|
+
private handleRequest;
|
|
278
|
+
protected handle_request(handler: ApiHandler, auth: {
|
|
279
|
+
type: ApiAuthType;
|
|
280
|
+
req: ApiAuthClass;
|
|
281
|
+
}): (req: ExtendedReq, res: ApiResponse, next: (error?: unknown) => void) => Promise<void>;
|
|
282
|
+
api<T extends ApiModule<unknown>>(module: T): this;
|
|
283
|
+
private joinRoutePath;
|
|
284
|
+
dumpRequest(apiReq: ApiRequest): void;
|
|
285
|
+
private formatDebugValue;
|
|
286
|
+
dumpResponse(apiReq: ApiRequest, payload: unknown, status: number): void;
|
|
287
|
+
}
|
|
288
|
+
export default ApiServer;
|