@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/esm/token/memory.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { TokenStore } from './base.js';
|
|
2
2
|
function comparableUserId(value) {
|
|
3
|
-
if (value === undefined
|
|
3
|
+
if (value === undefined) {
|
|
4
4
|
return undefined;
|
|
5
5
|
}
|
|
6
6
|
return String(value);
|
|
7
7
|
}
|
|
8
8
|
function cloneToken(record) {
|
|
9
|
-
// this.normalizeToken is not available in static context; caller passes through instance.
|
|
10
|
-
// cloning handled via store instance methods.
|
|
11
|
-
const normalized = record;
|
|
12
9
|
return {
|
|
13
|
-
...
|
|
14
|
-
scope:
|
|
10
|
+
...record,
|
|
11
|
+
scope: record.scope ? [...record.scope] : undefined,
|
|
12
|
+
expires: record.expires ? new Date(record.expires) : undefined,
|
|
13
|
+
issuedAt: record.issuedAt ? new Date(record.issuedAt) : undefined,
|
|
14
|
+
lastSeenAt: record.lastSeenAt ? new Date(record.lastSeenAt) : undefined
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
function matchesQuery(record, query, includeExpired) {
|
|
@@ -45,17 +45,63 @@ function matchesQuery(record, query, includeExpired) {
|
|
|
45
45
|
return true;
|
|
46
46
|
}
|
|
47
47
|
export class MemoryTokenStore extends TokenStore {
|
|
48
|
-
constructor() {
|
|
49
|
-
super(
|
|
50
|
-
this.tokens =
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
super();
|
|
50
|
+
this.tokens = new Map();
|
|
51
|
+
this.tokensByUser = new Map();
|
|
52
|
+
this.maxTokens =
|
|
53
|
+
typeof options.maxTokens === 'number' && Number.isFinite(options.maxTokens) && options.maxTokens > 0
|
|
54
|
+
? Math.floor(options.maxTokens)
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
indexToken(token) {
|
|
58
|
+
const userId = comparableUserId(token.userId);
|
|
59
|
+
if (!userId) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let userTokens = this.tokensByUser.get(userId);
|
|
63
|
+
if (!userTokens) {
|
|
64
|
+
userTokens = new Set();
|
|
65
|
+
this.tokensByUser.set(userId, userTokens);
|
|
66
|
+
}
|
|
67
|
+
userTokens.add(token.refreshToken);
|
|
68
|
+
}
|
|
69
|
+
unindexToken(token) {
|
|
70
|
+
const userId = comparableUserId(token.userId);
|
|
71
|
+
if (!userId) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const userTokens = this.tokensByUser.get(userId);
|
|
75
|
+
if (!userTokens) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
userTokens.delete(token.refreshToken);
|
|
79
|
+
if (userTokens.size === 0) {
|
|
80
|
+
this.tokensByUser.delete(userId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
removeByRefreshToken(refreshToken) {
|
|
84
|
+
const existing = this.tokens.get(refreshToken);
|
|
85
|
+
if (!existing) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.unindexToken(existing);
|
|
89
|
+
this.tokens.delete(refreshToken);
|
|
51
90
|
}
|
|
52
91
|
async save(record) {
|
|
53
92
|
const stored = this.normalizeToken(record);
|
|
54
93
|
const normalizedUserId = comparableUserId(stored.userId);
|
|
94
|
+
if (!normalizedUserId) {
|
|
95
|
+
throw new Error('userId is required');
|
|
96
|
+
}
|
|
55
97
|
const domainProvided = record.domain !== undefined;
|
|
56
98
|
const fingerprintProvided = record.fingerprint !== undefined;
|
|
57
|
-
|
|
58
|
-
|
|
99
|
+
const userRefreshTokens = [...(this.tokensByUser.get(normalizedUserId) ?? [])];
|
|
100
|
+
for (const refreshToken of userRefreshTokens) {
|
|
101
|
+
const existing = this.tokens.get(refreshToken);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
59
105
|
if (comparableUserId(existing.userId) !== normalizedUserId) {
|
|
60
106
|
continue;
|
|
61
107
|
}
|
|
@@ -68,44 +114,52 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
68
114
|
if (fingerprintProvided && existing.fingerprint !== stored.fingerprint) {
|
|
69
115
|
continue;
|
|
70
116
|
}
|
|
71
|
-
this.
|
|
117
|
+
this.removeByRefreshToken(existing.refreshToken);
|
|
72
118
|
}
|
|
73
|
-
this.
|
|
119
|
+
this.removeByRefreshToken(stored.refreshToken);
|
|
120
|
+
this.tokens.set(stored.refreshToken, stored);
|
|
121
|
+
this.indexToken(stored);
|
|
122
|
+
this.enforceCapacity();
|
|
74
123
|
}
|
|
75
124
|
async get(query, opts) {
|
|
76
125
|
if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
|
|
77
126
|
throw new Error('At least one token lookup field must be provided');
|
|
78
127
|
}
|
|
79
128
|
const includeExpired = opts?.includeExpired ?? false;
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
if (query.refreshToken) {
|
|
130
|
+
const record = this.tokens.get(query.refreshToken);
|
|
131
|
+
return record && matchesQuery(record, query, includeExpired) ? cloneToken(record) : null;
|
|
132
|
+
}
|
|
133
|
+
for (const token of this.tokens.values()) {
|
|
134
|
+
if (matchesQuery(token, query, includeExpired)) {
|
|
135
|
+
return cloneToken(token);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
82
139
|
}
|
|
83
140
|
async delete(query) {
|
|
84
141
|
if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
|
|
85
142
|
return 0;
|
|
86
143
|
}
|
|
87
144
|
let removed = 0;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
145
|
+
const refreshTokens = [...this.tokens.keys()];
|
|
146
|
+
for (const refreshToken of refreshTokens) {
|
|
147
|
+
const token = this.tokens.get(refreshToken);
|
|
148
|
+
if (token && matchesQuery(token, query, true)) {
|
|
149
|
+
this.removeByRefreshToken(refreshToken);
|
|
91
150
|
removed += 1;
|
|
92
151
|
}
|
|
93
152
|
}
|
|
94
153
|
return removed;
|
|
95
154
|
}
|
|
96
155
|
async update(params) {
|
|
97
|
-
const token = this.tokens.
|
|
98
|
-
if (record.refreshToken !== params.refreshToken) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
if (params.clientId && record.clientId !== params.clientId) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
return true;
|
|
105
|
-
});
|
|
156
|
+
const token = this.tokens.get(params.refreshToken);
|
|
106
157
|
if (!token) {
|
|
107
158
|
return false;
|
|
108
159
|
}
|
|
160
|
+
if (params.clientId && token.clientId !== params.clientId) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
109
163
|
const merged = { ...token };
|
|
110
164
|
const maybeAssign = (key) => {
|
|
111
165
|
const value = params[key];
|
|
@@ -113,8 +167,12 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
113
167
|
merged[key] = value;
|
|
114
168
|
}
|
|
115
169
|
};
|
|
116
|
-
|
|
117
|
-
|
|
170
|
+
if (params.accessToken !== undefined && params.accessToken !== null) {
|
|
171
|
+
merged.accessToken = params.accessToken;
|
|
172
|
+
}
|
|
173
|
+
if (params.expires !== undefined && params.expires !== null) {
|
|
174
|
+
merged.expires = params.expires;
|
|
175
|
+
}
|
|
118
176
|
maybeAssign('scope');
|
|
119
177
|
maybeAssign('label');
|
|
120
178
|
maybeAssign('domain');
|
|
@@ -125,16 +183,36 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
125
183
|
maybeAssign('os');
|
|
126
184
|
maybeAssign('refreshTtlSeconds');
|
|
127
185
|
maybeAssign('loginType');
|
|
128
|
-
|
|
129
|
-
|
|
186
|
+
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
187
|
+
merged.issuedAt = params.issuedAt;
|
|
188
|
+
}
|
|
189
|
+
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
190
|
+
merged.lastSeenAt = params.lastSeenAt;
|
|
191
|
+
}
|
|
130
192
|
maybeAssign('sessionCookie');
|
|
131
193
|
const normalized = this.normalizeToken(merged);
|
|
194
|
+
const previousUserId = token.userId;
|
|
195
|
+
const previousRefreshToken = token.refreshToken;
|
|
196
|
+
const userChanged = comparableUserId(previousUserId) !== comparableUserId(normalized.userId);
|
|
132
197
|
Object.assign(token, normalized);
|
|
198
|
+
if (userChanged || previousRefreshToken !== token.refreshToken) {
|
|
199
|
+
this.unindexToken({ ...token, userId: previousUserId, refreshToken: previousRefreshToken });
|
|
200
|
+
this.indexToken(token);
|
|
201
|
+
if (previousRefreshToken !== token.refreshToken) {
|
|
202
|
+
this.tokens.delete(previousRefreshToken);
|
|
203
|
+
this.tokens.set(token.refreshToken, token);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
133
206
|
return true;
|
|
134
207
|
}
|
|
135
208
|
async list(userId, opts = {}) {
|
|
136
209
|
const includeExpired = opts.includeExpired ?? false;
|
|
137
|
-
const
|
|
210
|
+
const normalizedUserId = comparableUserId(userId);
|
|
211
|
+
const userRefreshTokens = normalizedUserId ? [...(this.tokensByUser.get(normalizedUserId) ?? [])] : [];
|
|
212
|
+
const filtered = userRefreshTokens
|
|
213
|
+
.map((refreshToken) => this.tokens.get(refreshToken))
|
|
214
|
+
.filter((token) => Boolean(token))
|
|
215
|
+
.filter((token) => matchesQuery(token, { userId: normalizedUserId }, includeExpired));
|
|
138
216
|
const offset = opts.offset ?? 0;
|
|
139
217
|
const limit = opts.limit ?? filtered.length;
|
|
140
218
|
return filtered.slice(offset, offset + limit).map(cloneToken);
|
|
@@ -142,4 +220,16 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
142
220
|
async close() {
|
|
143
221
|
return;
|
|
144
222
|
}
|
|
223
|
+
enforceCapacity() {
|
|
224
|
+
if (!this.maxTokens) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
while (this.tokens.size > this.maxTokens) {
|
|
228
|
+
const oldestRefresh = this.tokens.keys().next().value;
|
|
229
|
+
if (!oldestRefresh) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.removeByRefreshToken(oldestRefresh);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
145
235
|
}
|
|
@@ -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 {};
|
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
import { DataTypes, Model, Op } from 'sequelize';
|
|
2
|
+
import { normalizeStringUserId } from '../auth-api/user-id.js';
|
|
3
|
+
import { applyTablePrefix, decodeStringArray, encodeStringArray, integerIdType, tableOptions } from '../sequelize-utils.js';
|
|
2
4
|
import { TokenStore } from './base.js';
|
|
3
|
-
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
4
5
|
class TokenModel extends Model {
|
|
5
6
|
}
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
13
|
-
opts.charset = 'utf8mb4';
|
|
14
|
-
opts.collate = 'utf8mb4_unicode_ci';
|
|
15
|
-
}
|
|
16
|
-
return opts;
|
|
17
|
-
}
|
|
18
|
-
function initTokenModel(sequelize) {
|
|
7
|
+
function initTokenModel(sequelize, options = {}) {
|
|
8
|
+
const tableName = applyTablePrefix(options.tablePrefix, 'jwttokens');
|
|
9
|
+
const usePrefixedIndexNames = tableName !== 'jwttokens';
|
|
10
|
+
const accessIndexName = usePrefixedIndexNames ? `${tableName}_access_unique` : 'jwt_access_unique';
|
|
11
|
+
const refreshIndexName = usePrefixedIndexNames ? `${tableName}_refresh_unique` : 'jwt_refresh_unique';
|
|
19
12
|
TokenModel.init({
|
|
20
13
|
token_id: {
|
|
21
|
-
type:
|
|
22
|
-
? DataTypes.INTEGER.UNSIGNED
|
|
23
|
-
: DataTypes.INTEGER,
|
|
14
|
+
type: integerIdType(sequelize),
|
|
24
15
|
autoIncrement: true,
|
|
25
16
|
allowNull: false,
|
|
26
17
|
primaryKey: true
|
|
@@ -49,11 +40,11 @@ function initTokenModel(sequelize) {
|
|
|
49
40
|
defaultValue: DataTypes.NOW
|
|
50
41
|
},
|
|
51
42
|
access: {
|
|
52
|
-
type: DataTypes.STRING(
|
|
43
|
+
type: DataTypes.STRING(768),
|
|
53
44
|
allowNull: false
|
|
54
45
|
},
|
|
55
46
|
refresh: {
|
|
56
|
-
type: DataTypes.STRING(
|
|
47
|
+
type: DataTypes.STRING(768),
|
|
57
48
|
allowNull: false
|
|
58
49
|
},
|
|
59
50
|
domain: {
|
|
@@ -117,10 +108,10 @@ function initTokenModel(sequelize) {
|
|
|
117
108
|
defaultValue: '[]'
|
|
118
109
|
}
|
|
119
110
|
}, {
|
|
120
|
-
...
|
|
111
|
+
...tableOptions(sequelize, 'jwttokens', options.tablePrefix, { timestamps: false }),
|
|
121
112
|
indexes: [
|
|
122
|
-
{ name:
|
|
123
|
-
{ name:
|
|
113
|
+
{ name: accessIndexName, unique: true, fields: ['access'] },
|
|
114
|
+
{ name: refreshIndexName, unique: true, fields: ['refresh'] }
|
|
124
115
|
]
|
|
125
116
|
});
|
|
126
117
|
return TokenModel;
|
|
@@ -131,7 +122,11 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
131
122
|
if (!options?.sequelize) {
|
|
132
123
|
throw new Error('SequelizeTokenStore requires an initialised Sequelize instance');
|
|
133
124
|
}
|
|
134
|
-
this.Tokens =
|
|
125
|
+
this.Tokens =
|
|
126
|
+
options.tokenModel ??
|
|
127
|
+
(options.tokenModelFactory ?? initTokenModel)(options.sequelize, {
|
|
128
|
+
tablePrefix: options.tablePrefix
|
|
129
|
+
});
|
|
135
130
|
}
|
|
136
131
|
async save(record) {
|
|
137
132
|
const normalized = this.normalizeToken(record);
|
|
@@ -152,36 +147,50 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
152
147
|
const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
|
|
153
148
|
const sessionCookie = normalized.sessionCookie ?? false;
|
|
154
149
|
const removalWhere = { user_id: resolvedUserId };
|
|
155
|
-
if (
|
|
150
|
+
if (record.domain !== undefined) {
|
|
156
151
|
removalWhere.domain = domain;
|
|
157
152
|
}
|
|
158
|
-
if (
|
|
153
|
+
if (record.fingerprint !== undefined) {
|
|
159
154
|
removalWhere.fingerprint = fingerprint;
|
|
160
155
|
}
|
|
161
156
|
if (normalized.clientId) {
|
|
162
157
|
removalWhere.client_id = normalized.clientId;
|
|
163
158
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
159
|
+
const sequelize = this.Tokens.sequelize;
|
|
160
|
+
if (!sequelize) {
|
|
161
|
+
throw new Error('Token model is not bound to a Sequelize instance');
|
|
162
|
+
}
|
|
163
|
+
await sequelize.transaction(async (transaction) => {
|
|
164
|
+
await this.Tokens.destroy({ where: removalWhere, transaction });
|
|
165
|
+
// Access/refresh columns are unique. Remove stale collisions before insert to avoid
|
|
166
|
+
// transient uniqueness failures during retries/rotation edge-cases.
|
|
167
|
+
await this.Tokens.destroy({
|
|
168
|
+
where: {
|
|
169
|
+
[Op.or]: [{ access: normalized.accessToken ?? '' }, { refresh: normalized.refreshToken }]
|
|
170
|
+
},
|
|
171
|
+
transaction
|
|
172
|
+
});
|
|
173
|
+
await this.Tokens.create({
|
|
174
|
+
user_id: resolvedUserId,
|
|
175
|
+
real_user_id: resolvedRealUserId,
|
|
176
|
+
access: normalized.accessToken ?? '',
|
|
177
|
+
refresh: normalized.refreshToken,
|
|
178
|
+
expires: normalized.expires,
|
|
179
|
+
issued_at: issuedAt,
|
|
180
|
+
last_seen_at: lastSeenAt,
|
|
181
|
+
domain,
|
|
182
|
+
fingerprint,
|
|
183
|
+
label,
|
|
184
|
+
browser,
|
|
185
|
+
device,
|
|
186
|
+
ip,
|
|
187
|
+
os,
|
|
188
|
+
client_id: normalized.clientId ?? null,
|
|
189
|
+
scope: this.encodeScope(normalized.scope),
|
|
190
|
+
login_type: loginType,
|
|
191
|
+
refresh_ttl_seconds: refreshTtlSeconds,
|
|
192
|
+
session_cookie: sessionCookie
|
|
193
|
+
}, { transaction });
|
|
185
194
|
});
|
|
186
195
|
}
|
|
187
196
|
async get(query, opts) {
|
|
@@ -250,11 +259,11 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
250
259
|
where.client_id = params.clientId;
|
|
251
260
|
}
|
|
252
261
|
const updates = {};
|
|
253
|
-
if (params.accessToken !== undefined) {
|
|
254
|
-
updates.access = params.accessToken
|
|
262
|
+
if (params.accessToken !== undefined && params.accessToken !== null) {
|
|
263
|
+
updates.access = params.accessToken;
|
|
255
264
|
}
|
|
256
|
-
if (params.expires !== undefined) {
|
|
257
|
-
updates.expires = params.expires
|
|
265
|
+
if (params.expires !== undefined && params.expires !== null) {
|
|
266
|
+
updates.expires = params.expires;
|
|
258
267
|
}
|
|
259
268
|
if (params.scope !== undefined) {
|
|
260
269
|
updates.scope = this.encodeScope(params.scope);
|
|
@@ -292,11 +301,11 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
292
301
|
if (params.sessionCookie !== undefined) {
|
|
293
302
|
updates.session_cookie = params.sessionCookie;
|
|
294
303
|
}
|
|
295
|
-
if (params.issuedAt !== undefined) {
|
|
296
|
-
updates.issued_at = params.issuedAt
|
|
304
|
+
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
305
|
+
updates.issued_at = params.issuedAt;
|
|
297
306
|
}
|
|
298
|
-
if (params.lastSeenAt !== undefined) {
|
|
299
|
-
updates.last_seen_at = params.lastSeenAt
|
|
307
|
+
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
308
|
+
updates.last_seen_at = params.lastSeenAt;
|
|
300
309
|
}
|
|
301
310
|
if (Object.keys(updates).length === 0) {
|
|
302
311
|
return false;
|
|
@@ -321,10 +330,7 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
321
330
|
return;
|
|
322
331
|
}
|
|
323
332
|
normalizeUserId(identifier) {
|
|
324
|
-
|
|
325
|
-
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
326
|
-
}
|
|
327
|
-
return String(identifier);
|
|
333
|
+
return normalizeStringUserId(identifier);
|
|
328
334
|
}
|
|
329
335
|
resolveRealUserId(ruid) {
|
|
330
336
|
if (ruid === undefined || ruid === null) {
|
|
@@ -336,41 +342,17 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
336
342
|
}
|
|
337
343
|
return value;
|
|
338
344
|
}
|
|
339
|
-
encodeStringArray(values) {
|
|
340
|
-
return JSON.stringify(values ?? []);
|
|
341
|
-
}
|
|
342
|
-
decodeStringArray(raw) {
|
|
343
|
-
if (!raw) {
|
|
344
|
-
return [];
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
const parsed = JSON.parse(raw);
|
|
348
|
-
if (Array.isArray(parsed)) {
|
|
349
|
-
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
// ignore malformed values
|
|
354
|
-
}
|
|
355
|
-
return raw
|
|
356
|
-
.split(/\s+/)
|
|
357
|
-
.map((entry) => entry.trim())
|
|
358
|
-
.filter((entry) => entry.length > 0);
|
|
359
|
-
}
|
|
360
345
|
encodeScope(scope) {
|
|
361
346
|
if (!scope || (Array.isArray(scope) && scope.length === 0)) {
|
|
362
347
|
return '[]';
|
|
363
348
|
}
|
|
364
349
|
if (Array.isArray(scope)) {
|
|
365
|
-
return
|
|
350
|
+
return encodeStringArray(scope);
|
|
366
351
|
}
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
decodeScope(raw) {
|
|
370
|
-
return this.decodeStringArray(raw);
|
|
352
|
+
return encodeStringArray(scope.split(/\s+/).filter((entry) => entry.length > 0));
|
|
371
353
|
}
|
|
372
354
|
toTokenRecord(model) {
|
|
373
|
-
const scope =
|
|
355
|
+
const scope = decodeStringArray(model.scope);
|
|
374
356
|
const normalized = this.normalizeToken({
|
|
375
357
|
userId: model.user_id,
|
|
376
358
|
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/esm/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/esm/user/base.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
1
2
|
import bcrypt from 'bcryptjs';
|
|
2
3
|
export class UserStore {
|
|
3
4
|
constructor(opts = {}) {
|
|
@@ -9,12 +10,18 @@ export class UserStore {
|
|
|
9
10
|
this.bcryptPepper =
|
|
10
11
|
typeof opts.bcryptPepper === 'string' && opts.bcryptPepper.length > 0 ? opts.bcryptPepper : undefined;
|
|
11
12
|
}
|
|
13
|
+
applyPepper(plain) {
|
|
14
|
+
if (!this.bcryptPepper) {
|
|
15
|
+
return plain;
|
|
16
|
+
}
|
|
17
|
+
return createHmac('sha256', this.bcryptPepper).update(plain).digest('hex');
|
|
18
|
+
}
|
|
12
19
|
async hashPassword(plain) {
|
|
13
|
-
const candidate = this.
|
|
20
|
+
const candidate = this.applyPepper(plain);
|
|
14
21
|
return bcrypt.hash(candidate, this.bcryptRounds);
|
|
15
22
|
}
|
|
16
23
|
async verifyPassword(plain, hashed) {
|
|
17
|
-
const candidate = this.
|
|
24
|
+
const candidate = this.applyPepper(plain);
|
|
18
25
|
return bcrypt.compare(candidate, hashed);
|
|
19
26
|
}
|
|
20
27
|
normalizeUserInput(input) {
|
|
@@ -29,8 +36,8 @@ export class UserStore {
|
|
|
29
36
|
toPublic(user) {
|
|
30
37
|
const mapped = this.toPublicUser(user);
|
|
31
38
|
if (mapped && typeof mapped === 'object') {
|
|
32
|
-
const
|
|
33
|
-
|
|
39
|
+
const rest = { ...mapped };
|
|
40
|
+
delete rest.password;
|
|
34
41
|
return rest;
|
|
35
42
|
}
|
|
36
43
|
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/esm/user/memory.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
+
import { normalizeNumericUserId } from '../auth-api/user-id.js';
|
|
1
2
|
import { UserStore } from './base.js';
|
|
2
3
|
function cloneUser(user) {
|
|
3
4
|
return { ...user };
|
|
4
5
|
}
|
|
5
|
-
function normalizeUserId(identifier) {
|
|
6
|
-
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
7
|
-
return identifier;
|
|
8
|
-
}
|
|
9
|
-
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
10
|
-
return Number(identifier);
|
|
11
|
-
}
|
|
12
|
-
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
13
|
-
}
|
|
14
6
|
export class MemoryUserStore extends UserStore {
|
|
15
7
|
constructor(options = {}) {
|
|
16
8
|
super({
|
|
@@ -22,6 +14,10 @@ export class MemoryUserStore extends UserStore {
|
|
|
22
14
|
this.loginToId = new Map();
|
|
23
15
|
this.emailToId = new Map();
|
|
24
16
|
this.nextUserId = Number.isFinite(options.startingUserId) ? Number(options.startingUserId) : 1;
|
|
17
|
+
this.maxUsers =
|
|
18
|
+
typeof options.maxUsers === 'number' && Number.isFinite(options.maxUsers) && options.maxUsers > 0
|
|
19
|
+
? Math.floor(options.maxUsers)
|
|
20
|
+
: undefined;
|
|
25
21
|
this.userIdFactory =
|
|
26
22
|
options.userIdFactory ??
|
|
27
23
|
(() => {
|
|
@@ -56,7 +52,7 @@ export class MemoryUserStore extends UserStore {
|
|
|
56
52
|
}
|
|
57
53
|
async findById(id) {
|
|
58
54
|
try {
|
|
59
|
-
const numeric =
|
|
55
|
+
const numeric = normalizeNumericUserId(id);
|
|
60
56
|
const user = this.usersById.get(numeric);
|
|
61
57
|
return user ? cloneUser(user) : null;
|
|
62
58
|
}
|
|
@@ -74,6 +70,9 @@ export class MemoryUserStore extends UserStore {
|
|
|
74
70
|
if (this.usersById.has(userId)) {
|
|
75
71
|
throw new Error(`User ${userId} already exists`);
|
|
76
72
|
}
|
|
73
|
+
if (this.maxUsers !== undefined && this.usersById.size >= this.maxUsers) {
|
|
74
|
+
throw new Error('MemoryUserStore maxUsers limit reached');
|
|
75
|
+
}
|
|
77
76
|
if (this.loginToId.has(normalizedInput.login)) {
|
|
78
77
|
throw new Error(`User with login ${normalizedInput.login} already exists`);
|
|
79
78
|
}
|
|
@@ -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
|
}
|