@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.txt +81 -28
- package/dist/cjs/api-module.cjs +9 -0
- package/dist/cjs/api-module.d.ts +7 -4
- package/dist/cjs/api-server-base.cjs +607 -99
- package/dist/cjs/api-server-base.d.ts +80 -23
- package/dist/cjs/auth-api/auth-module.d.ts +23 -3
- package/dist/cjs/auth-api/auth-module.js +320 -124
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
- package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/mem-auth-store.js +14 -28
- package/dist/cjs/auth-api/module.d.ts +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/cjs/auth-api/sql-auth-store.js +43 -30
- package/dist/cjs/auth-api/storage.d.ts +6 -4
- package/dist/cjs/auth-api/storage.js +15 -5
- package/dist/cjs/auth-api/types.d.ts +7 -2
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-api/user-id.js +38 -0
- package/dist/cjs/auth-cookie-options.d.ts +11 -0
- package/dist/cjs/auth-cookie-options.js +66 -0
- package/dist/cjs/index.cjs +4 -14
- package/dist/cjs/index.d.ts +4 -9
- package/dist/cjs/oauth/memory.d.ts +6 -0
- package/dist/cjs/oauth/memory.js +44 -11
- package/dist/cjs/oauth/models.d.ts +7 -2
- package/dist/cjs/oauth/models.js +10 -21
- package/dist/cjs/oauth/sequelize.d.ts +10 -48
- package/dist/cjs/oauth/sequelize.js +44 -99
- package/dist/cjs/oauth/types.d.ts +1 -0
- package/dist/cjs/passkey/base.d.ts +2 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/config.js +26 -0
- package/dist/cjs/passkey/memory.d.ts +8 -0
- package/dist/cjs/passkey/memory.js +57 -16
- package/dist/cjs/passkey/models.d.ts +13 -4
- package/dist/cjs/passkey/models.js +41 -14
- package/dist/cjs/passkey/sequelize.d.ts +13 -25
- package/dist/cjs/passkey/sequelize.js +68 -153
- package/dist/cjs/passkey/service.d.ts +6 -2
- package/dist/cjs/passkey/service.js +205 -27
- package/dist/cjs/passkey/types.d.ts +18 -9
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/sequelize-utils.js +57 -0
- package/dist/cjs/token/base.d.ts +2 -1
- package/dist/cjs/token/base.js +3 -1
- package/dist/cjs/token/memory.d.ts +10 -0
- package/dist/cjs/token/memory.js +122 -32
- package/dist/cjs/token/sequelize.d.ts +4 -4
- package/dist/cjs/token/sequelize.js +67 -85
- package/dist/cjs/token/types.d.ts +8 -1
- package/dist/cjs/user/base.d.ts +1 -0
- package/dist/cjs/user/base.js +11 -4
- package/dist/cjs/user/memory.d.ts +2 -0
- package/dist/cjs/user/memory.js +9 -10
- package/dist/cjs/user/sequelize.d.ts +7 -2
- package/dist/cjs/user/sequelize.js +19 -32
- package/dist/esm/api-module.d.ts +7 -4
- package/dist/esm/api-module.js +9 -0
- package/dist/esm/api-server-base.d.ts +80 -23
- package/dist/esm/api-server-base.js +608 -100
- package/dist/esm/auth-api/auth-module.d.ts +23 -3
- package/dist/esm/auth-api/auth-module.js +321 -125
- package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/esm/auth-api/compat-auth-storage.js +13 -1
- package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/mem-auth-store.js +14 -28
- package/dist/esm/auth-api/module.d.ts +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/esm/auth-api/sql-auth-store.js +43 -30
- package/dist/esm/auth-api/storage.d.ts +6 -4
- package/dist/esm/auth-api/storage.js +13 -3
- package/dist/esm/auth-api/types.d.ts +7 -2
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +32 -0
- package/dist/esm/auth-cookie-options.d.ts +11 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/index.d.ts +4 -9
- package/dist/esm/index.js +2 -7
- package/dist/esm/oauth/memory.d.ts +6 -0
- package/dist/esm/oauth/memory.js +44 -11
- package/dist/esm/oauth/models.d.ts +7 -2
- package/dist/esm/oauth/models.js +6 -19
- package/dist/esm/oauth/sequelize.d.ts +10 -48
- package/dist/esm/oauth/sequelize.js +32 -87
- package/dist/esm/oauth/types.d.ts +1 -0
- package/dist/esm/passkey/base.d.ts +2 -0
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.d.ts +8 -0
- package/dist/esm/passkey/memory.js +57 -16
- package/dist/esm/passkey/models.d.ts +13 -4
- package/dist/esm/passkey/models.js +39 -12
- package/dist/esm/passkey/sequelize.d.ts +13 -25
- package/dist/esm/passkey/sequelize.js +69 -154
- package/dist/esm/passkey/service.d.ts +6 -2
- package/dist/esm/passkey/service.js +173 -28
- package/dist/esm/passkey/types.d.ts +18 -9
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +48 -0
- package/dist/esm/token/base.d.ts +2 -1
- package/dist/esm/token/base.js +3 -1
- package/dist/esm/token/memory.d.ts +10 -0
- package/dist/esm/token/memory.js +122 -32
- package/dist/esm/token/sequelize.d.ts +4 -4
- package/dist/esm/token/sequelize.js +67 -85
- package/dist/esm/token/types.d.ts +8 -1
- package/dist/esm/user/base.d.ts +1 -0
- package/dist/esm/user/base.js +11 -4
- package/dist/esm/user/memory.d.ts +2 -0
- package/dist/esm/user/memory.js +9 -10
- package/dist/esm/user/sequelize.d.ts +7 -2
- package/dist/esm/user/sequelize.js +19 -32
- package/docs/swagger/openapi.json +1876 -0
- package/package.json +84 -34
package/dist/cjs/token/memory.js
CHANGED
|
@@ -3,18 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MemoryTokenStore = void 0;
|
|
4
4
|
const base_js_1 = require("./base.js");
|
|
5
5
|
function comparableUserId(value) {
|
|
6
|
-
if (value === undefined
|
|
6
|
+
if (value === undefined) {
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
9
|
return String(value);
|
|
10
10
|
}
|
|
11
11
|
function cloneToken(record) {
|
|
12
|
-
// this.normalizeToken is not available in static context; caller passes through instance.
|
|
13
|
-
// cloning handled via store instance methods.
|
|
14
|
-
const normalized = record;
|
|
15
12
|
return {
|
|
16
|
-
...
|
|
17
|
-
scope:
|
|
13
|
+
...record,
|
|
14
|
+
scope: record.scope ? [...record.scope] : undefined,
|
|
15
|
+
expires: record.expires ? new Date(record.expires) : undefined,
|
|
16
|
+
issuedAt: record.issuedAt ? new Date(record.issuedAt) : undefined,
|
|
17
|
+
lastSeenAt: record.lastSeenAt ? new Date(record.lastSeenAt) : undefined
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
function matchesQuery(record, query, includeExpired) {
|
|
@@ -48,17 +48,63 @@ function matchesQuery(record, query, includeExpired) {
|
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
50
|
class MemoryTokenStore extends base_js_1.TokenStore {
|
|
51
|
-
constructor() {
|
|
52
|
-
super(
|
|
53
|
-
this.tokens =
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
super();
|
|
53
|
+
this.tokens = new Map();
|
|
54
|
+
this.tokensByUser = new Map();
|
|
55
|
+
this.maxTokens =
|
|
56
|
+
typeof options.maxTokens === 'number' && Number.isFinite(options.maxTokens) && options.maxTokens > 0
|
|
57
|
+
? Math.floor(options.maxTokens)
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
indexToken(token) {
|
|
61
|
+
const userId = comparableUserId(token.userId);
|
|
62
|
+
if (!userId) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let userTokens = this.tokensByUser.get(userId);
|
|
66
|
+
if (!userTokens) {
|
|
67
|
+
userTokens = new Set();
|
|
68
|
+
this.tokensByUser.set(userId, userTokens);
|
|
69
|
+
}
|
|
70
|
+
userTokens.add(token.refreshToken);
|
|
71
|
+
}
|
|
72
|
+
unindexToken(token) {
|
|
73
|
+
const userId = comparableUserId(token.userId);
|
|
74
|
+
if (!userId) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const userTokens = this.tokensByUser.get(userId);
|
|
78
|
+
if (!userTokens) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
userTokens.delete(token.refreshToken);
|
|
82
|
+
if (userTokens.size === 0) {
|
|
83
|
+
this.tokensByUser.delete(userId);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
removeByRefreshToken(refreshToken) {
|
|
87
|
+
const existing = this.tokens.get(refreshToken);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.unindexToken(existing);
|
|
92
|
+
this.tokens.delete(refreshToken);
|
|
54
93
|
}
|
|
55
94
|
async save(record) {
|
|
56
95
|
const stored = this.normalizeToken(record);
|
|
57
96
|
const normalizedUserId = comparableUserId(stored.userId);
|
|
97
|
+
if (!normalizedUserId) {
|
|
98
|
+
throw new Error('userId is required');
|
|
99
|
+
}
|
|
58
100
|
const domainProvided = record.domain !== undefined;
|
|
59
101
|
const fingerprintProvided = record.fingerprint !== undefined;
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
const userRefreshTokens = [...(this.tokensByUser.get(normalizedUserId) ?? [])];
|
|
103
|
+
for (const refreshToken of userRefreshTokens) {
|
|
104
|
+
const existing = this.tokens.get(refreshToken);
|
|
105
|
+
if (!existing) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
62
108
|
if (comparableUserId(existing.userId) !== normalizedUserId) {
|
|
63
109
|
continue;
|
|
64
110
|
}
|
|
@@ -71,44 +117,52 @@ class MemoryTokenStore extends base_js_1.TokenStore {
|
|
|
71
117
|
if (fingerprintProvided && existing.fingerprint !== stored.fingerprint) {
|
|
72
118
|
continue;
|
|
73
119
|
}
|
|
74
|
-
this.
|
|
120
|
+
this.removeByRefreshToken(existing.refreshToken);
|
|
75
121
|
}
|
|
76
|
-
this.
|
|
122
|
+
this.removeByRefreshToken(stored.refreshToken);
|
|
123
|
+
this.tokens.set(stored.refreshToken, stored);
|
|
124
|
+
this.indexToken(stored);
|
|
125
|
+
this.enforceCapacity();
|
|
77
126
|
}
|
|
78
127
|
async get(query, opts) {
|
|
79
128
|
if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
|
|
80
129
|
throw new Error('At least one token lookup field must be provided');
|
|
81
130
|
}
|
|
82
131
|
const includeExpired = opts?.includeExpired ?? false;
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
if (query.refreshToken) {
|
|
133
|
+
const record = this.tokens.get(query.refreshToken);
|
|
134
|
+
return record && matchesQuery(record, query, includeExpired) ? cloneToken(record) : null;
|
|
135
|
+
}
|
|
136
|
+
for (const token of this.tokens.values()) {
|
|
137
|
+
if (matchesQuery(token, query, includeExpired)) {
|
|
138
|
+
return cloneToken(token);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
85
142
|
}
|
|
86
143
|
async delete(query) {
|
|
87
144
|
if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
|
|
88
145
|
return 0;
|
|
89
146
|
}
|
|
90
147
|
let removed = 0;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
148
|
+
const refreshTokens = [...this.tokens.keys()];
|
|
149
|
+
for (const refreshToken of refreshTokens) {
|
|
150
|
+
const token = this.tokens.get(refreshToken);
|
|
151
|
+
if (token && matchesQuery(token, query, true)) {
|
|
152
|
+
this.removeByRefreshToken(refreshToken);
|
|
94
153
|
removed += 1;
|
|
95
154
|
}
|
|
96
155
|
}
|
|
97
156
|
return removed;
|
|
98
157
|
}
|
|
99
158
|
async update(params) {
|
|
100
|
-
const token = this.tokens.
|
|
101
|
-
if (record.refreshToken !== params.refreshToken) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
if (params.clientId && record.clientId !== params.clientId) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
108
|
-
});
|
|
159
|
+
const token = this.tokens.get(params.refreshToken);
|
|
109
160
|
if (!token) {
|
|
110
161
|
return false;
|
|
111
162
|
}
|
|
163
|
+
if (params.clientId && token.clientId !== params.clientId) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
112
166
|
const merged = { ...token };
|
|
113
167
|
const maybeAssign = (key) => {
|
|
114
168
|
const value = params[key];
|
|
@@ -116,8 +170,12 @@ class MemoryTokenStore extends base_js_1.TokenStore {
|
|
|
116
170
|
merged[key] = value;
|
|
117
171
|
}
|
|
118
172
|
};
|
|
119
|
-
|
|
120
|
-
|
|
173
|
+
if (params.accessToken !== undefined && params.accessToken !== null) {
|
|
174
|
+
merged.accessToken = params.accessToken;
|
|
175
|
+
}
|
|
176
|
+
if (params.expires !== undefined && params.expires !== null) {
|
|
177
|
+
merged.expires = params.expires;
|
|
178
|
+
}
|
|
121
179
|
maybeAssign('scope');
|
|
122
180
|
maybeAssign('label');
|
|
123
181
|
maybeAssign('domain');
|
|
@@ -128,16 +186,36 @@ class MemoryTokenStore extends base_js_1.TokenStore {
|
|
|
128
186
|
maybeAssign('os');
|
|
129
187
|
maybeAssign('refreshTtlSeconds');
|
|
130
188
|
maybeAssign('loginType');
|
|
131
|
-
|
|
132
|
-
|
|
189
|
+
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
190
|
+
merged.issuedAt = params.issuedAt;
|
|
191
|
+
}
|
|
192
|
+
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
193
|
+
merged.lastSeenAt = params.lastSeenAt;
|
|
194
|
+
}
|
|
133
195
|
maybeAssign('sessionCookie');
|
|
134
196
|
const normalized = this.normalizeToken(merged);
|
|
197
|
+
const previousUserId = token.userId;
|
|
198
|
+
const previousRefreshToken = token.refreshToken;
|
|
199
|
+
const userChanged = comparableUserId(previousUserId) !== comparableUserId(normalized.userId);
|
|
135
200
|
Object.assign(token, normalized);
|
|
201
|
+
if (userChanged || previousRefreshToken !== token.refreshToken) {
|
|
202
|
+
this.unindexToken({ ...token, userId: previousUserId, refreshToken: previousRefreshToken });
|
|
203
|
+
this.indexToken(token);
|
|
204
|
+
if (previousRefreshToken !== token.refreshToken) {
|
|
205
|
+
this.tokens.delete(previousRefreshToken);
|
|
206
|
+
this.tokens.set(token.refreshToken, token);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
136
209
|
return true;
|
|
137
210
|
}
|
|
138
211
|
async list(userId, opts = {}) {
|
|
139
212
|
const includeExpired = opts.includeExpired ?? false;
|
|
140
|
-
const
|
|
213
|
+
const normalizedUserId = comparableUserId(userId);
|
|
214
|
+
const userRefreshTokens = normalizedUserId ? [...(this.tokensByUser.get(normalizedUserId) ?? [])] : [];
|
|
215
|
+
const filtered = userRefreshTokens
|
|
216
|
+
.map((refreshToken) => this.tokens.get(refreshToken))
|
|
217
|
+
.filter((token) => Boolean(token))
|
|
218
|
+
.filter((token) => matchesQuery(token, { userId: normalizedUserId }, includeExpired));
|
|
141
219
|
const offset = opts.offset ?? 0;
|
|
142
220
|
const limit = opts.limit ?? filtered.length;
|
|
143
221
|
return filtered.slice(offset, offset + limit).map(cloneToken);
|
|
@@ -145,5 +223,17 @@ class MemoryTokenStore extends base_js_1.TokenStore {
|
|
|
145
223
|
async close() {
|
|
146
224
|
return;
|
|
147
225
|
}
|
|
226
|
+
enforceCapacity() {
|
|
227
|
+
if (!this.maxTokens) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
while (this.tokens.size > this.maxTokens) {
|
|
231
|
+
const oldestRefresh = this.tokens.keys().next().value;
|
|
232
|
+
if (!oldestRefresh) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.removeByRefreshToken(oldestRefresh);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
148
238
|
}
|
|
149
239
|
exports.MemoryTokenStore = MemoryTokenStore;
|
|
@@ -27,8 +27,11 @@ export type TokenAttributes = InferAttributes<TokenModel>;
|
|
|
27
27
|
export type TokenCreationAttributes = InferCreationAttributes<TokenModel>;
|
|
28
28
|
export interface SequelizeTokenStoreOptions {
|
|
29
29
|
sequelize: Sequelize;
|
|
30
|
+
tablePrefix?: string;
|
|
30
31
|
tokenModel?: ModelStatic<TokenModel>;
|
|
31
|
-
tokenModelFactory?: (sequelize: Sequelize
|
|
32
|
+
tokenModelFactory?: (sequelize: Sequelize, options?: {
|
|
33
|
+
tablePrefix?: string;
|
|
34
|
+
}) => ModelStatic<TokenModel>;
|
|
32
35
|
}
|
|
33
36
|
export declare class SequelizeTokenStore extends TokenStore {
|
|
34
37
|
readonly Tokens: ModelStatic<TokenModel>;
|
|
@@ -49,10 +52,7 @@ export declare class SequelizeTokenStore extends TokenStore {
|
|
|
49
52
|
close(): Promise<void>;
|
|
50
53
|
private normalizeUserId;
|
|
51
54
|
private resolveRealUserId;
|
|
52
|
-
private encodeStringArray;
|
|
53
|
-
private decodeStringArray;
|
|
54
55
|
private encodeScope;
|
|
55
|
-
private decodeScope;
|
|
56
56
|
private toTokenRecord;
|
|
57
57
|
}
|
|
58
58
|
export {};
|
|
@@ -2,28 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SequelizeTokenStore = void 0;
|
|
4
4
|
const sequelize_1 = require("sequelize");
|
|
5
|
+
const user_id_js_1 = require("../auth-api/user-id.js");
|
|
6
|
+
const sequelize_utils_js_1 = require("../sequelize-utils.js");
|
|
5
7
|
const base_js_1 = require("./base.js");
|
|
6
|
-
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
7
8
|
class TokenModel extends sequelize_1.Model {
|
|
8
9
|
}
|
|
9
|
-
function
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
16
|
-
opts.charset = 'utf8mb4';
|
|
17
|
-
opts.collate = 'utf8mb4_unicode_ci';
|
|
18
|
-
}
|
|
19
|
-
return opts;
|
|
20
|
-
}
|
|
21
|
-
function initTokenModel(sequelize) {
|
|
10
|
+
function initTokenModel(sequelize, options = {}) {
|
|
11
|
+
const tableName = (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'jwttokens');
|
|
12
|
+
const usePrefixedIndexNames = tableName !== 'jwttokens';
|
|
13
|
+
const accessIndexName = usePrefixedIndexNames ? `${tableName}_access_unique` : 'jwt_access_unique';
|
|
14
|
+
const refreshIndexName = usePrefixedIndexNames ? `${tableName}_refresh_unique` : 'jwt_refresh_unique';
|
|
22
15
|
TokenModel.init({
|
|
23
16
|
token_id: {
|
|
24
|
-
type:
|
|
25
|
-
? sequelize_1.DataTypes.INTEGER.UNSIGNED
|
|
26
|
-
: sequelize_1.DataTypes.INTEGER,
|
|
17
|
+
type: (0, sequelize_utils_js_1.integerIdType)(sequelize),
|
|
27
18
|
autoIncrement: true,
|
|
28
19
|
allowNull: false,
|
|
29
20
|
primaryKey: true
|
|
@@ -52,11 +43,11 @@ function initTokenModel(sequelize) {
|
|
|
52
43
|
defaultValue: sequelize_1.DataTypes.NOW
|
|
53
44
|
},
|
|
54
45
|
access: {
|
|
55
|
-
type: sequelize_1.DataTypes.STRING(
|
|
46
|
+
type: sequelize_1.DataTypes.STRING(768),
|
|
56
47
|
allowNull: false
|
|
57
48
|
},
|
|
58
49
|
refresh: {
|
|
59
|
-
type: sequelize_1.DataTypes.STRING(
|
|
50
|
+
type: sequelize_1.DataTypes.STRING(768),
|
|
60
51
|
allowNull: false
|
|
61
52
|
},
|
|
62
53
|
domain: {
|
|
@@ -120,10 +111,10 @@ function initTokenModel(sequelize) {
|
|
|
120
111
|
defaultValue: '[]'
|
|
121
112
|
}
|
|
122
113
|
}, {
|
|
123
|
-
...
|
|
114
|
+
...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'jwttokens', options.tablePrefix, { timestamps: false }),
|
|
124
115
|
indexes: [
|
|
125
|
-
{ name:
|
|
126
|
-
{ name:
|
|
116
|
+
{ name: accessIndexName, unique: true, fields: ['access'] },
|
|
117
|
+
{ name: refreshIndexName, unique: true, fields: ['refresh'] }
|
|
127
118
|
]
|
|
128
119
|
});
|
|
129
120
|
return TokenModel;
|
|
@@ -134,7 +125,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
134
125
|
if (!options?.sequelize) {
|
|
135
126
|
throw new Error('SequelizeTokenStore requires an initialised Sequelize instance');
|
|
136
127
|
}
|
|
137
|
-
this.Tokens =
|
|
128
|
+
this.Tokens =
|
|
129
|
+
options.tokenModel ??
|
|
130
|
+
(options.tokenModelFactory ?? initTokenModel)(options.sequelize, {
|
|
131
|
+
tablePrefix: options.tablePrefix
|
|
132
|
+
});
|
|
138
133
|
}
|
|
139
134
|
async save(record) {
|
|
140
135
|
const normalized = this.normalizeToken(record);
|
|
@@ -155,36 +150,50 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
155
150
|
const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
|
|
156
151
|
const sessionCookie = normalized.sessionCookie ?? false;
|
|
157
152
|
const removalWhere = { user_id: resolvedUserId };
|
|
158
|
-
if (
|
|
153
|
+
if (record.domain !== undefined) {
|
|
159
154
|
removalWhere.domain = domain;
|
|
160
155
|
}
|
|
161
|
-
if (
|
|
156
|
+
if (record.fingerprint !== undefined) {
|
|
162
157
|
removalWhere.fingerprint = fingerprint;
|
|
163
158
|
}
|
|
164
159
|
if (normalized.clientId) {
|
|
165
160
|
removalWhere.client_id = normalized.clientId;
|
|
166
161
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 });
|
|
188
197
|
});
|
|
189
198
|
}
|
|
190
199
|
async get(query, opts) {
|
|
@@ -253,11 +262,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
253
262
|
where.client_id = params.clientId;
|
|
254
263
|
}
|
|
255
264
|
const updates = {};
|
|
256
|
-
if (params.accessToken !== undefined) {
|
|
257
|
-
updates.access = params.accessToken
|
|
265
|
+
if (params.accessToken !== undefined && params.accessToken !== null) {
|
|
266
|
+
updates.access = params.accessToken;
|
|
258
267
|
}
|
|
259
|
-
if (params.expires !== undefined) {
|
|
260
|
-
updates.expires = params.expires
|
|
268
|
+
if (params.expires !== undefined && params.expires !== null) {
|
|
269
|
+
updates.expires = params.expires;
|
|
261
270
|
}
|
|
262
271
|
if (params.scope !== undefined) {
|
|
263
272
|
updates.scope = this.encodeScope(params.scope);
|
|
@@ -295,11 +304,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
295
304
|
if (params.sessionCookie !== undefined) {
|
|
296
305
|
updates.session_cookie = params.sessionCookie;
|
|
297
306
|
}
|
|
298
|
-
if (params.issuedAt !== undefined) {
|
|
299
|
-
updates.issued_at = params.issuedAt
|
|
307
|
+
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
308
|
+
updates.issued_at = params.issuedAt;
|
|
300
309
|
}
|
|
301
|
-
if (params.lastSeenAt !== undefined) {
|
|
302
|
-
updates.last_seen_at = params.lastSeenAt
|
|
310
|
+
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
311
|
+
updates.last_seen_at = params.lastSeenAt;
|
|
303
312
|
}
|
|
304
313
|
if (Object.keys(updates).length === 0) {
|
|
305
314
|
return false;
|
|
@@ -324,10 +333,7 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
324
333
|
return;
|
|
325
334
|
}
|
|
326
335
|
normalizeUserId(identifier) {
|
|
327
|
-
|
|
328
|
-
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
329
|
-
}
|
|
330
|
-
return String(identifier);
|
|
336
|
+
return (0, user_id_js_1.normalizeStringUserId)(identifier);
|
|
331
337
|
}
|
|
332
338
|
resolveRealUserId(ruid) {
|
|
333
339
|
if (ruid === undefined || ruid === null) {
|
|
@@ -339,41 +345,17 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
|
339
345
|
}
|
|
340
346
|
return value;
|
|
341
347
|
}
|
|
342
|
-
encodeStringArray(values) {
|
|
343
|
-
return JSON.stringify(values ?? []);
|
|
344
|
-
}
|
|
345
|
-
decodeStringArray(raw) {
|
|
346
|
-
if (!raw) {
|
|
347
|
-
return [];
|
|
348
|
-
}
|
|
349
|
-
try {
|
|
350
|
-
const parsed = JSON.parse(raw);
|
|
351
|
-
if (Array.isArray(parsed)) {
|
|
352
|
-
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
// ignore malformed values
|
|
357
|
-
}
|
|
358
|
-
return raw
|
|
359
|
-
.split(/\s+/)
|
|
360
|
-
.map((entry) => entry.trim())
|
|
361
|
-
.filter((entry) => entry.length > 0);
|
|
362
|
-
}
|
|
363
348
|
encodeScope(scope) {
|
|
364
349
|
if (!scope || (Array.isArray(scope) && scope.length === 0)) {
|
|
365
350
|
return '[]';
|
|
366
351
|
}
|
|
367
352
|
if (Array.isArray(scope)) {
|
|
368
|
-
return
|
|
353
|
+
return (0, sequelize_utils_js_1.encodeStringArray)(scope);
|
|
369
354
|
}
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
decodeScope(raw) {
|
|
373
|
-
return this.decodeStringArray(raw);
|
|
355
|
+
return (0, sequelize_utils_js_1.encodeStringArray)(scope.split(/\s+/).filter((entry) => entry.length > 0));
|
|
374
356
|
}
|
|
375
357
|
toTokenRecord(model) {
|
|
376
|
-
const scope =
|
|
358
|
+
const scope = (0, sequelize_utils_js_1.decodeStringArray)(model.scope);
|
|
377
359
|
const normalized = this.normalizeToken({
|
|
378
360
|
userId: model.user_id,
|
|
379
361
|
refreshToken: model.refresh,
|
|
@@ -9,14 +9,21 @@ export interface Token {
|
|
|
9
9
|
status?: TokenStatus;
|
|
10
10
|
ruid?: string;
|
|
11
11
|
clientId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional session partition key. Token stores may use `domain` and `fingerprint`
|
|
14
|
+
* to replace previous sessions that match the same bucket.
|
|
15
|
+
*/
|
|
12
16
|
domain?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional device/session fingerprint used together with `domain` for session bucketing.
|
|
19
|
+
*/
|
|
13
20
|
fingerprint?: string;
|
|
14
21
|
label?: string;
|
|
15
22
|
browser?: string;
|
|
16
23
|
device?: string;
|
|
17
24
|
ip?: string;
|
|
18
25
|
os?: string;
|
|
19
|
-
scope?: string
|
|
26
|
+
scope?: string[];
|
|
20
27
|
loginType?: string;
|
|
21
28
|
refreshTtlSeconds?: number;
|
|
22
29
|
sessionCookie?: boolean;
|
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
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemoryUserStore = void 0;
|
|
4
|
+
const user_id_js_1 = require("../auth-api/user-id.js");
|
|
4
5
|
const base_js_1 = require("./base.js");
|
|
5
6
|
function cloneUser(user) {
|
|
6
7
|
return { ...user };
|
|
7
8
|
}
|
|
8
|
-
function normalizeUserId(identifier) {
|
|
9
|
-
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
10
|
-
return identifier;
|
|
11
|
-
}
|
|
12
|
-
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
13
|
-
return Number(identifier);
|
|
14
|
-
}
|
|
15
|
-
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
16
|
-
}
|
|
17
9
|
class MemoryUserStore extends base_js_1.UserStore {
|
|
18
10
|
constructor(options = {}) {
|
|
19
11
|
super({
|
|
@@ -25,6 +17,10 @@ class MemoryUserStore extends base_js_1.UserStore {
|
|
|
25
17
|
this.loginToId = new Map();
|
|
26
18
|
this.emailToId = new Map();
|
|
27
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;
|
|
28
24
|
this.userIdFactory =
|
|
29
25
|
options.userIdFactory ??
|
|
30
26
|
(() => {
|
|
@@ -59,7 +55,7 @@ class MemoryUserStore extends base_js_1.UserStore {
|
|
|
59
55
|
}
|
|
60
56
|
async findById(id) {
|
|
61
57
|
try {
|
|
62
|
-
const numeric =
|
|
58
|
+
const numeric = (0, user_id_js_1.normalizeNumericUserId)(id);
|
|
63
59
|
const user = this.usersById.get(numeric);
|
|
64
60
|
return user ? cloneUser(user) : null;
|
|
65
61
|
}
|
|
@@ -77,6 +73,9 @@ class MemoryUserStore extends base_js_1.UserStore {
|
|
|
77
73
|
if (this.usersById.has(userId)) {
|
|
78
74
|
throw new Error(`User ${userId} already exists`);
|
|
79
75
|
}
|
|
76
|
+
if (this.maxUsers !== undefined && this.usersById.size >= this.maxUsers) {
|
|
77
|
+
throw new Error('MemoryUserStore maxUsers limit reached');
|
|
78
|
+
}
|
|
80
79
|
if (this.loginToId.has(normalizedInput.login)) {
|
|
81
80
|
throw new Error(`User with login ${normalizedInput.login} already exists`);
|
|
82
81
|
}
|
|
@@ -10,15 +10,20 @@ export declare class AuthUserModel extends Model<InferAttributes<AuthUserModel>,
|
|
|
10
10
|
}
|
|
11
11
|
export type AuthUserAttributes = InferAttributes<AuthUserModel>;
|
|
12
12
|
export type AuthUserCreationAttributes = InferCreationAttributes<AuthUserModel>;
|
|
13
|
-
export declare function initAuthUserModel(sequelize: Sequelize
|
|
13
|
+
export declare function initAuthUserModel(sequelize: Sequelize, options?: {
|
|
14
|
+
tablePrefix?: string;
|
|
15
|
+
}): typeof AuthUserModel;
|
|
14
16
|
export type GenericUserModel = Model<Record<string, unknown>, Record<string, unknown>>;
|
|
15
17
|
export type GenericUserModelStatic = ModelStatic<GenericUserModel>;
|
|
16
18
|
export interface SequelizeUserStoreOptions<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> {
|
|
17
19
|
bcryptRounds?: number;
|
|
18
20
|
bcryptPepper?: string;
|
|
19
21
|
sequelize: Sequelize;
|
|
22
|
+
tablePrefix?: string;
|
|
20
23
|
userModel?: GenericUserModelStatic;
|
|
21
|
-
userModelFactory?: (sequelize: Sequelize
|
|
24
|
+
userModelFactory?: (sequelize: Sequelize, options?: {
|
|
25
|
+
tablePrefix?: string;
|
|
26
|
+
}) => GenericUserModelStatic;
|
|
22
27
|
recordMapper?: (model: GenericUserModel) => UserAttributes;
|
|
23
28
|
toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
|
|
24
29
|
}
|