@technomoron/api-server-base 2.0.0-beta.2 → 2.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.txt +81 -28
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +7 -4
  4. package/dist/cjs/api-server-base.cjs +607 -99
  5. package/dist/cjs/api-server-base.d.ts +80 -23
  6. package/dist/cjs/auth-api/auth-module.d.ts +23 -3
  7. package/dist/cjs/auth-api/auth-module.js +320 -124
  8. package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
  9. package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
  10. package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
  11. package/dist/cjs/auth-api/mem-auth-store.js +14 -28
  12. package/dist/cjs/auth-api/module.d.ts +1 -1
  13. package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
  14. package/dist/cjs/auth-api/sql-auth-store.js +43 -30
  15. package/dist/cjs/auth-api/storage.d.ts +6 -4
  16. package/dist/cjs/auth-api/storage.js +15 -5
  17. package/dist/cjs/auth-api/types.d.ts +7 -2
  18. package/dist/cjs/auth-api/user-id.d.ts +5 -0
  19. package/dist/cjs/auth-api/user-id.js +38 -0
  20. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  21. package/dist/cjs/auth-cookie-options.js +66 -0
  22. package/dist/cjs/index.cjs +4 -14
  23. package/dist/cjs/index.d.ts +4 -9
  24. package/dist/cjs/oauth/memory.d.ts +6 -0
  25. package/dist/cjs/oauth/memory.js +44 -11
  26. package/dist/cjs/oauth/models.d.ts +7 -2
  27. package/dist/cjs/oauth/models.js +10 -21
  28. package/dist/cjs/oauth/sequelize.d.ts +10 -48
  29. package/dist/cjs/oauth/sequelize.js +44 -99
  30. package/dist/cjs/oauth/types.d.ts +1 -0
  31. package/dist/cjs/passkey/base.d.ts +2 -0
  32. package/dist/cjs/passkey/config.d.ts +2 -0
  33. package/dist/cjs/passkey/config.js +26 -0
  34. package/dist/cjs/passkey/memory.d.ts +8 -0
  35. package/dist/cjs/passkey/memory.js +57 -16
  36. package/dist/cjs/passkey/models.d.ts +13 -4
  37. package/dist/cjs/passkey/models.js +41 -14
  38. package/dist/cjs/passkey/sequelize.d.ts +13 -25
  39. package/dist/cjs/passkey/sequelize.js +68 -153
  40. package/dist/cjs/passkey/service.d.ts +6 -2
  41. package/dist/cjs/passkey/service.js +205 -27
  42. package/dist/cjs/passkey/types.d.ts +18 -9
  43. package/dist/cjs/sequelize-utils.d.ts +8 -0
  44. package/dist/cjs/sequelize-utils.js +57 -0
  45. package/dist/cjs/token/base.d.ts +2 -1
  46. package/dist/cjs/token/base.js +3 -1
  47. package/dist/cjs/token/memory.d.ts +10 -0
  48. package/dist/cjs/token/memory.js +122 -32
  49. package/dist/cjs/token/sequelize.d.ts +4 -4
  50. package/dist/cjs/token/sequelize.js +67 -85
  51. package/dist/cjs/token/types.d.ts +8 -1
  52. package/dist/cjs/user/base.d.ts +1 -0
  53. package/dist/cjs/user/base.js +11 -4
  54. package/dist/cjs/user/memory.d.ts +2 -0
  55. package/dist/cjs/user/memory.js +9 -10
  56. package/dist/cjs/user/sequelize.d.ts +7 -2
  57. package/dist/cjs/user/sequelize.js +19 -32
  58. package/dist/esm/api-module.d.ts +7 -4
  59. package/dist/esm/api-module.js +9 -0
  60. package/dist/esm/api-server-base.d.ts +80 -23
  61. package/dist/esm/api-server-base.js +608 -100
  62. package/dist/esm/auth-api/auth-module.d.ts +23 -3
  63. package/dist/esm/auth-api/auth-module.js +321 -125
  64. package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
  65. package/dist/esm/auth-api/compat-auth-storage.js +13 -1
  66. package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
  67. package/dist/esm/auth-api/mem-auth-store.js +14 -28
  68. package/dist/esm/auth-api/module.d.ts +1 -1
  69. package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
  70. package/dist/esm/auth-api/sql-auth-store.js +43 -30
  71. package/dist/esm/auth-api/storage.d.ts +6 -4
  72. package/dist/esm/auth-api/storage.js +13 -3
  73. package/dist/esm/auth-api/types.d.ts +7 -2
  74. package/dist/esm/auth-api/user-id.d.ts +5 -0
  75. package/dist/esm/auth-api/user-id.js +32 -0
  76. package/dist/esm/auth-cookie-options.d.ts +11 -0
  77. package/dist/esm/auth-cookie-options.js +63 -0
  78. package/dist/esm/index.d.ts +4 -9
  79. package/dist/esm/index.js +2 -7
  80. package/dist/esm/oauth/memory.d.ts +6 -0
  81. package/dist/esm/oauth/memory.js +44 -11
  82. package/dist/esm/oauth/models.d.ts +7 -2
  83. package/dist/esm/oauth/models.js +6 -19
  84. package/dist/esm/oauth/sequelize.d.ts +10 -48
  85. package/dist/esm/oauth/sequelize.js +32 -87
  86. package/dist/esm/oauth/types.d.ts +1 -0
  87. package/dist/esm/passkey/base.d.ts +2 -0
  88. package/dist/esm/passkey/config.d.ts +2 -0
  89. package/dist/esm/passkey/config.js +23 -0
  90. package/dist/esm/passkey/memory.d.ts +8 -0
  91. package/dist/esm/passkey/memory.js +57 -16
  92. package/dist/esm/passkey/models.d.ts +13 -4
  93. package/dist/esm/passkey/models.js +39 -12
  94. package/dist/esm/passkey/sequelize.d.ts +13 -25
  95. package/dist/esm/passkey/sequelize.js +69 -154
  96. package/dist/esm/passkey/service.d.ts +6 -2
  97. package/dist/esm/passkey/service.js +173 -28
  98. package/dist/esm/passkey/types.d.ts +18 -9
  99. package/dist/esm/sequelize-utils.d.ts +8 -0
  100. package/dist/esm/sequelize-utils.js +48 -0
  101. package/dist/esm/token/base.d.ts +2 -1
  102. package/dist/esm/token/base.js +3 -1
  103. package/dist/esm/token/memory.d.ts +10 -0
  104. package/dist/esm/token/memory.js +122 -32
  105. package/dist/esm/token/sequelize.d.ts +4 -4
  106. package/dist/esm/token/sequelize.js +67 -85
  107. package/dist/esm/token/types.d.ts +8 -1
  108. package/dist/esm/user/base.d.ts +1 -0
  109. package/dist/esm/user/base.js +11 -4
  110. package/dist/esm/user/memory.d.ts +2 -0
  111. package/dist/esm/user/memory.js +9 -10
  112. package/dist/esm/user/sequelize.d.ts +7 -2
  113. package/dist/esm/user/sequelize.js +19 -32
  114. package/docs/swagger/openapi.json +1876 -0
  115. package/package.json +81 -32
@@ -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 || value === null) {
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
- ...normalized,
17
- scope: normalized.scope ? [...normalized.scope] : undefined
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(...arguments);
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
- for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
61
- const existing = this.tokens[index];
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.tokens.splice(index, 1);
120
+ this.removeByRefreshToken(existing.refreshToken);
75
121
  }
76
- this.tokens.push(stored);
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
- const record = this.tokens.find((token) => matchesQuery(token, query, includeExpired));
84
- return record ? cloneToken(record) : null;
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
- for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
92
- if (matchesQuery(this.tokens[index], query, true)) {
93
- this.tokens.splice(index, 1);
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.find((record) => {
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
- maybeAssign('accessToken');
120
- maybeAssign('expires');
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
- maybeAssign('issuedAt');
132
- maybeAssign('lastSeenAt');
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 filtered = this.tokens.filter((token) => matchesQuery(token, { userId: comparableUserId(userId) }, includeExpired));
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) => ModelStatic<TokenModel>;
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 tokenTableOptions(sequelize) {
10
- const opts = {
11
- sequelize,
12
- tableName: 'jwttokens',
13
- timestamps: false
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: DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())
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(512),
46
+ type: sequelize_1.DataTypes.STRING(768),
56
47
  allowNull: false
57
48
  },
58
49
  refresh: {
59
- type: sequelize_1.DataTypes.STRING(512),
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
- ...tokenTableOptions(sequelize),
114
+ ...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'jwttokens', options.tablePrefix, { timestamps: false }),
124
115
  indexes: [
125
- { name: 'jwt_access_unique', unique: true, fields: ['access'] },
126
- { name: 'jwt_refresh_unique', unique: true, fields: ['refresh'] }
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 = options.tokenModel ?? (options.tokenModelFactory ?? initTokenModel)(options.sequelize);
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 (normalized.domain !== undefined && record.domain !== undefined) {
153
+ if (record.domain !== undefined) {
159
154
  removalWhere.domain = domain;
160
155
  }
161
- if (normalized.fingerprint !== undefined && record.fingerprint !== undefined) {
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
- await this.Tokens.destroy({ where: removalWhere });
168
- await this.Tokens.create({
169
- user_id: resolvedUserId,
170
- real_user_id: resolvedRealUserId,
171
- access: normalized.accessToken ?? '',
172
- refresh: normalized.refreshToken,
173
- expires: normalized.expires,
174
- issued_at: issuedAt,
175
- last_seen_at: lastSeenAt,
176
- domain,
177
- fingerprint,
178
- label,
179
- browser,
180
- device,
181
- ip,
182
- os,
183
- client_id: normalized.clientId ?? null,
184
- scope: this.encodeScope(normalized.scope),
185
- login_type: loginType,
186
- refresh_ttl_seconds: refreshTtlSeconds,
187
- session_cookie: sessionCookie
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 ?? null;
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 ?? null;
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 ?? null;
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 ?? null;
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
- if (identifier === undefined || identifier === null) {
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 this.encodeStringArray(scope);
353
+ return (0, sequelize_utils_js_1.encodeStringArray)(scope);
369
354
  }
370
- return this.encodeStringArray(scope.split(/\s+/).filter((entry) => entry.length > 0));
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 = this.decodeScope(model.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 | string[];
26
+ scope?: string[];
20
27
  loginType?: string;
21
28
  refreshTtlSeconds?: number;
22
29
  sessionCookie?: boolean;
@@ -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;
@@ -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.bcryptPepper ? `${plain}${this.bcryptPepper}` : plain;
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.bcryptPepper ? `${plain}${this.bcryptPepper}` : plain;
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 { password: _password, ...rest } = mapped;
39
- void _password;
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>;
@@ -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 = normalizeUserId(id);
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): typeof AuthUserModel;
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) => GenericUserModelStatic;
24
+ userModelFactory?: (sequelize: Sequelize, options?: {
25
+ tablePrefix?: string;
26
+ }) => GenericUserModelStatic;
22
27
  recordMapper?: (model: GenericUserModel) => UserAttributes;
23
28
  toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
24
29
  }