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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cjs/api-server-base.cjs +48 -21
  2. package/dist/cjs/auth-api/auth-module.js +16 -30
  3. package/dist/cjs/auth-api/mem-auth-store.js +5 -4
  4. package/dist/cjs/auth-api/sql-auth-store.js +6 -4
  5. package/dist/cjs/auth-api/user-id.d.ts +1 -0
  6. package/dist/cjs/auth-api/user-id.js +7 -0
  7. package/dist/cjs/auth-cookie-options.js +10 -1
  8. package/dist/cjs/oauth/memory.d.ts +6 -0
  9. package/dist/cjs/oauth/memory.js +43 -4
  10. package/dist/cjs/oauth/models.d.ts +1 -0
  11. package/dist/cjs/oauth/models.js +7 -18
  12. package/dist/cjs/oauth/sequelize.d.ts +5 -52
  13. package/dist/cjs/oauth/sequelize.js +34 -93
  14. package/dist/cjs/oauth/types.d.ts +1 -0
  15. package/dist/cjs/passkey/base.d.ts +1 -0
  16. package/dist/cjs/passkey/memory.d.ts +7 -0
  17. package/dist/cjs/passkey/memory.js +47 -5
  18. package/dist/cjs/passkey/models.js +2 -5
  19. package/dist/cjs/passkey/sequelize.d.ts +5 -29
  20. package/dist/cjs/passkey/sequelize.js +48 -191
  21. package/dist/cjs/passkey/service.d.ts +1 -0
  22. package/dist/cjs/passkey/service.js +52 -15
  23. package/dist/cjs/passkey/types.d.ts +1 -0
  24. package/dist/cjs/sequelize-utils.d.ts +5 -0
  25. package/dist/cjs/sequelize-utils.js +40 -0
  26. package/dist/cjs/token/base.js +3 -1
  27. package/dist/cjs/token/memory.d.ts +6 -0
  28. package/dist/cjs/token/memory.js +32 -7
  29. package/dist/cjs/token/sequelize.d.ts +0 -3
  30. package/dist/cjs/token/sequelize.js +50 -81
  31. package/dist/cjs/token/types.d.ts +1 -1
  32. package/dist/cjs/user/base.d.ts +1 -0
  33. package/dist/cjs/user/base.js +11 -4
  34. package/dist/cjs/user/memory.d.ts +2 -0
  35. package/dist/cjs/user/memory.js +8 -2
  36. package/dist/cjs/user/sequelize.js +12 -22
  37. package/dist/esm/api-server-base.js +48 -21
  38. package/dist/esm/auth-api/auth-module.js +16 -30
  39. package/dist/esm/auth-api/mem-auth-store.js +5 -4
  40. package/dist/esm/auth-api/sql-auth-store.js +6 -4
  41. package/dist/esm/auth-api/user-id.d.ts +1 -0
  42. package/dist/esm/auth-api/user-id.js +6 -0
  43. package/dist/esm/auth-cookie-options.js +10 -1
  44. package/dist/esm/oauth/memory.d.ts +6 -0
  45. package/dist/esm/oauth/memory.js +44 -5
  46. package/dist/esm/oauth/models.d.ts +1 -0
  47. package/dist/esm/oauth/models.js +2 -15
  48. package/dist/esm/oauth/sequelize.d.ts +5 -52
  49. package/dist/esm/oauth/sequelize.js +21 -80
  50. package/dist/esm/oauth/types.d.ts +1 -0
  51. package/dist/esm/passkey/base.d.ts +1 -0
  52. package/dist/esm/passkey/memory.d.ts +7 -0
  53. package/dist/esm/passkey/memory.js +47 -5
  54. package/dist/esm/passkey/models.js +1 -4
  55. package/dist/esm/passkey/sequelize.d.ts +5 -29
  56. package/dist/esm/passkey/sequelize.js +47 -190
  57. package/dist/esm/passkey/service.d.ts +1 -0
  58. package/dist/esm/passkey/service.js +52 -15
  59. package/dist/esm/passkey/types.d.ts +1 -0
  60. package/dist/esm/sequelize-utils.d.ts +5 -0
  61. package/dist/esm/sequelize-utils.js +36 -0
  62. package/dist/esm/token/base.js +3 -1
  63. package/dist/esm/token/memory.d.ts +6 -0
  64. package/dist/esm/token/memory.js +32 -7
  65. package/dist/esm/token/sequelize.d.ts +0 -3
  66. package/dist/esm/token/sequelize.js +51 -82
  67. package/dist/esm/token/types.d.ts +1 -1
  68. package/dist/esm/user/base.d.ts +1 -0
  69. package/dist/esm/user/base.js +11 -4
  70. package/dist/esm/user/memory.d.ts +2 -0
  71. package/dist/esm/user/memory.js +8 -2
  72. package/dist/esm/user/sequelize.js +13 -23
  73. package/package.json +5 -5
@@ -1,149 +1,10 @@
1
- import { DataTypes, Model, Op } from 'sequelize';
1
+ import { Op, Transaction } from 'sequelize';
2
2
  import { normalizeNumericUserId } from '../auth-api/user-id.js';
3
- import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
4
3
  import { PasskeyStore } from './base.js';
5
- function integerIdType(sequelize) {
6
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
7
- }
4
+ import { initPasskeyChallengeModel, initPasskeyCredentialModel } from './models.js';
8
5
  function encodeCredentialId(value) {
9
6
  return Buffer.isBuffer(value) ? value.toString('base64') : value;
10
7
  }
11
- function normalizeUserId(identifier) {
12
- return normalizeNumericUserId(identifier);
13
- }
14
- class PasskeyCredentialModel extends Model {
15
- }
16
- class PasskeyChallengeModel extends Model {
17
- }
18
- function initPasskeyCredentialModel(sequelize, options = {}) {
19
- const idType = integerIdType(sequelize);
20
- return PasskeyCredentialModel.init({
21
- credentialId: {
22
- field: 'credential_id',
23
- type: DataTypes.STRING(768),
24
- primaryKey: true,
25
- allowNull: false,
26
- get() {
27
- const raw = this.getDataValue('credentialId');
28
- if (!raw) {
29
- return raw;
30
- }
31
- if (Buffer.isBuffer(raw)) {
32
- return raw;
33
- }
34
- return Buffer.from(raw, 'base64');
35
- },
36
- set(value) {
37
- const encoded = typeof value === 'string' ? value : value.toString('base64');
38
- this.setDataValue('credentialId', encoded);
39
- }
40
- },
41
- userId: {
42
- field: 'user_id',
43
- type: idType,
44
- allowNull: false
45
- },
46
- publicKey: {
47
- field: 'public_key',
48
- type: DataTypes.BLOB,
49
- allowNull: false
50
- },
51
- counter: {
52
- type: DataTypes.INTEGER,
53
- allowNull: false,
54
- defaultValue: 0
55
- },
56
- transports: {
57
- type: DataTypes.JSON,
58
- allowNull: true
59
- },
60
- backedUp: {
61
- field: 'backed_up',
62
- type: DataTypes.BOOLEAN,
63
- allowNull: false,
64
- defaultValue: false
65
- },
66
- deviceType: {
67
- field: 'device_type',
68
- type: DataTypes.STRING(32),
69
- allowNull: false,
70
- defaultValue: 'multiDevice'
71
- },
72
- label: {
73
- type: DataTypes.STRING(120),
74
- allowNull: true
75
- },
76
- createdDomain: {
77
- field: 'created_domain',
78
- type: DataTypes.STRING(255),
79
- allowNull: true
80
- },
81
- createdUserAgent: {
82
- field: 'created_user_agent',
83
- type: DataTypes.TEXT,
84
- allowNull: true
85
- },
86
- createdBrowser: {
87
- field: 'created_browser',
88
- type: DataTypes.STRING(120),
89
- allowNull: true
90
- },
91
- createdOs: {
92
- field: 'created_os',
93
- type: DataTypes.STRING(120),
94
- allowNull: true
95
- },
96
- createdDevice: {
97
- field: 'created_device',
98
- type: DataTypes.STRING(120),
99
- allowNull: true
100
- },
101
- createdIp: {
102
- field: 'created_ip',
103
- type: DataTypes.STRING(45),
104
- allowNull: true
105
- }
106
- }, {
107
- sequelize,
108
- tableName: applyTablePrefix(options.tablePrefix, 'passkey_credentials'),
109
- timestamps: true,
110
- underscored: true
111
- });
112
- }
113
- function initPasskeyChallengeModel(sequelize, options = {}) {
114
- const idType = integerIdType(sequelize);
115
- return PasskeyChallengeModel.init({
116
- challenge: {
117
- type: DataTypes.STRING(255),
118
- primaryKey: true,
119
- allowNull: false
120
- },
121
- action: {
122
- type: DataTypes.STRING(16),
123
- allowNull: false
124
- },
125
- userId: {
126
- field: 'user_id',
127
- type: idType,
128
- allowNull: true
129
- },
130
- login: {
131
- type: DataTypes.STRING(128),
132
- allowNull: true
133
- },
134
- expiresAt: {
135
- field: 'expires_at',
136
- type: DataTypes.DATE,
137
- allowNull: false
138
- }
139
- }, {
140
- sequelize,
141
- tableName: applyTablePrefix(options.tablePrefix, 'passkey_challenges'),
142
- timestamps: true,
143
- underscored: true,
144
- indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
145
- });
146
- }
147
8
  export class SequelizePasskeyStore extends PasskeyStore {
148
9
  constructor(options) {
149
10
  super();
@@ -166,25 +27,8 @@ export class SequelizePasskeyStore extends PasskeyStore {
166
27
  return this.resolveUserFn(params);
167
28
  }
168
29
  async listUserCredentials(userId) {
169
- const models = await this.credentials.findAll({ where: { userId: normalizeUserId(userId) } });
170
- return models.map((model) => ({
171
- userId: model.userId,
172
- credentialId: model.credentialId,
173
- publicKey: model.publicKey,
174
- counter: model.counter,
175
- transports: (model.transports ?? undefined),
176
- backedUp: model.backedUp,
177
- deviceType: model.deviceType,
178
- label: model.label ?? undefined,
179
- createdDomain: model.createdDomain ?? undefined,
180
- createdUserAgent: model.createdUserAgent ?? undefined,
181
- createdBrowser: model.createdBrowser ?? undefined,
182
- createdOs: model.createdOs ?? undefined,
183
- createdDevice: model.createdDevice ?? undefined,
184
- createdIp: model.createdIp ?? undefined,
185
- createdAt: model.createdAt ?? undefined,
186
- updatedAt: model.updatedAt ?? undefined
187
- }));
30
+ const models = await this.credentials.findAll({ where: { userId: normalizeNumericUserId(userId) } });
31
+ return models.map((model) => this.toStoredCredential(model));
188
32
  }
189
33
  async deleteCredential(credentialId) {
190
34
  const encoded = Buffer.isBuffer(credentialId) ? credentialId.toString('base64') : credentialId;
@@ -193,32 +37,12 @@ export class SequelizePasskeyStore extends PasskeyStore {
193
37
  }
194
38
  async findCredentialById(credentialId) {
195
39
  const model = await this.credentials.findByPk(encodeCredentialId(credentialId));
196
- if (!model) {
197
- return null;
198
- }
199
- return {
200
- userId: model.userId,
201
- credentialId: model.credentialId,
202
- publicKey: model.publicKey,
203
- counter: model.counter,
204
- transports: (model.transports ?? undefined),
205
- backedUp: model.backedUp,
206
- deviceType: model.deviceType,
207
- label: model.label ?? undefined,
208
- createdDomain: model.createdDomain ?? undefined,
209
- createdUserAgent: model.createdUserAgent ?? undefined,
210
- createdBrowser: model.createdBrowser ?? undefined,
211
- createdOs: model.createdOs ?? undefined,
212
- createdDevice: model.createdDevice ?? undefined,
213
- createdIp: model.createdIp ?? undefined,
214
- createdAt: model.createdAt ?? undefined,
215
- updatedAt: model.updatedAt ?? undefined
216
- };
40
+ return model ? this.toStoredCredential(model) : null;
217
41
  }
218
42
  async saveCredential(record) {
219
43
  await this.credentials.upsert({
220
44
  credentialId: record.credentialId,
221
- userId: normalizeUserId(record.userId),
45
+ userId: normalizeNumericUserId(record.userId),
222
46
  publicKey: record.publicKey,
223
47
  counter: record.counter,
224
48
  transports: record.transports ?? null,
@@ -240,26 +64,59 @@ export class SequelizePasskeyStore extends PasskeyStore {
240
64
  await this.challenges.upsert({
241
65
  challenge: record.challenge,
242
66
  action: record.action,
243
- userId: record.userId !== undefined ? normalizeUserId(record.userId) : null,
67
+ userId: record.userId !== undefined ? normalizeNumericUserId(record.userId) : null,
244
68
  login: record.login ?? null,
245
69
  expiresAt: record.expiresAt
246
70
  });
247
71
  }
248
- async consumeChallenge(challenge) {
72
+ async getChallenge(challenge) {
249
73
  const model = await this.challenges.findByPk(challenge);
250
- if (!model) {
251
- return null;
74
+ return model ? this.toChallengeRecord(model) : null;
75
+ }
76
+ async consumeChallenge(challenge) {
77
+ const sequelize = this.challenges.sequelize;
78
+ if (!sequelize) {
79
+ throw new Error('Challenge model is not bound to a Sequelize instance');
252
80
  }
253
- await model.destroy();
81
+ return sequelize.transaction({ isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }, async (transaction) => {
82
+ const model = await this.challenges.findByPk(challenge, { transaction, lock: true });
83
+ if (!model) {
84
+ return null;
85
+ }
86
+ await model.destroy({ transaction });
87
+ return this.toChallengeRecord(model);
88
+ });
89
+ }
90
+ async cleanupChallenges(now) {
91
+ await this.challenges.destroy({ where: { expiresAt: { [Op.lte]: now } } });
92
+ }
93
+ toChallengeRecord(model) {
254
94
  return {
255
95
  challenge: model.challenge,
256
96
  action: model.action,
257
- userId: model.userId ?? undefined,
97
+ userId: model.userId !== null ? String(model.userId) : undefined,
258
98
  login: model.login ?? undefined,
259
99
  expiresAt: model.expiresAt
260
100
  };
261
101
  }
262
- async cleanupChallenges(now) {
263
- await this.challenges.destroy({ where: { expiresAt: { [Op.lte]: now } } });
102
+ toStoredCredential(model) {
103
+ return {
104
+ userId: String(model.userId),
105
+ credentialId: model.credentialId,
106
+ publicKey: model.publicKey,
107
+ counter: model.counter,
108
+ transports: (model.transports ?? undefined),
109
+ backedUp: model.backedUp,
110
+ deviceType: model.deviceType,
111
+ label: model.label ?? undefined,
112
+ createdDomain: model.createdDomain ?? undefined,
113
+ createdUserAgent: model.createdUserAgent ?? undefined,
114
+ createdBrowser: model.createdBrowser ?? undefined,
115
+ createdOs: model.createdOs ?? undefined,
116
+ createdDevice: model.createdDevice ?? undefined,
117
+ createdIp: model.createdIp ?? undefined,
118
+ createdAt: model.createdAt ?? undefined,
119
+ updatedAt: model.updatedAt ?? undefined
120
+ };
264
121
  }
265
122
  }
@@ -17,4 +17,5 @@ export declare class PasskeyService {
17
17
  private verifyAuthentication;
18
18
  private requireUser;
19
19
  private createExpiry;
20
+ private requireUserVerification;
20
21
  }
@@ -66,21 +66,51 @@ async function spkiToCosePublicKey(spki) {
66
66
  return null;
67
67
  }
68
68
  const spkiView = new Uint8Array(spki);
69
- const key = await subtle.importKey('spki', spkiView, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
70
- const raw = Buffer.from(await subtle.exportKey('raw', key));
71
- if (raw.length !== 65 || raw[0] !== 0x04) {
69
+ const ecCurves = [
70
+ { namedCurve: 'P-256', crv: 1, alg: -7, rawLength: 65 },
71
+ { namedCurve: 'P-384', crv: 2, alg: -35, rawLength: 97 },
72
+ { namedCurve: 'P-521', crv: 3, alg: -36, rawLength: 133 }
73
+ ];
74
+ for (const curve of ecCurves) {
75
+ try {
76
+ const key = await subtle.importKey('spki', spkiView, { name: 'ECDSA', namedCurve: curve.namedCurve }, true, ['verify']);
77
+ const raw = Buffer.from(await subtle.exportKey('raw', key));
78
+ if (raw.length !== curve.rawLength || raw[0] !== 0x04) {
79
+ continue;
80
+ }
81
+ const coordLength = (raw.length - 1) / 2;
82
+ const x = raw.slice(1, 1 + coordLength);
83
+ const y = raw.slice(1 + coordLength);
84
+ const coseMap = new Map([
85
+ [1, 2], // kty: EC2
86
+ [3, curve.alg],
87
+ [-1, curve.crv],
88
+ [-2, x],
89
+ [-3, y]
90
+ ]);
91
+ return Buffer.from(isoCBOR.encode(coseMap));
92
+ }
93
+ catch {
94
+ // try next algorithm
95
+ }
96
+ }
97
+ try {
98
+ const edKey = await subtle.importKey('spki', spkiView, { name: 'Ed25519' }, true, ['verify']);
99
+ const raw = Buffer.from(await subtle.exportKey('raw', edKey));
100
+ if (raw.length !== 32) {
101
+ return null;
102
+ }
103
+ const coseMap = new Map([
104
+ [1, 1], // kty: OKP
105
+ [3, -8], // alg: EdDSA
106
+ [-1, 6], // crv: Ed25519
107
+ [-2, raw]
108
+ ]);
109
+ return Buffer.from(isoCBOR.encode(coseMap));
110
+ }
111
+ catch {
72
112
  return null;
73
113
  }
74
- const x = raw.slice(1, 33);
75
- const y = raw.slice(33, 65);
76
- const coseMap = new Map([
77
- [1, 2], // kty: EC2
78
- [3, -7], // alg: ES256
79
- [-1, 1], // crv: P-256
80
- [-2, x],
81
- [-3, y]
82
- ]);
83
- return Buffer.from(isoCBOR.encode(coseMap));
84
114
  }
85
115
  catch {
86
116
  return null;
@@ -110,6 +140,10 @@ export class PasskeyService {
110
140
  }
111
141
  async verifyResponse(params) {
112
142
  await this.adapter.cleanupChallenges?.(new Date());
143
+ const existing = this.adapter.getChallenge ? await this.adapter.getChallenge(params.expectedChallenge) : null;
144
+ if (existing && existing.expiresAt.getTime() <= Date.now()) {
145
+ return { verified: false };
146
+ }
113
147
  const record = await this.adapter.consumeChallenge(params.expectedChallenge);
114
148
  if (!record) {
115
149
  return { verified: false };
@@ -204,7 +238,7 @@ export class PasskeyService {
204
238
  expectedChallenge: record.challenge,
205
239
  expectedOrigin: this.config.origins,
206
240
  expectedRPID: this.config.rpId,
207
- requireUserVerification: true
241
+ requireUserVerification: this.requireUserVerification()
208
242
  });
209
243
  if (!result.verified || !result.registrationInfo) {
210
244
  if (!result.verified) {
@@ -298,7 +332,7 @@ export class PasskeyService {
298
332
  expectedOrigin: this.config.origins,
299
333
  expectedRPID: this.config.rpId,
300
334
  credential: storedAuthData,
301
- requireUserVerification: true
335
+ requireUserVerification: this.requireUserVerification()
302
336
  });
303
337
  if (!result.verified) {
304
338
  const err = result.error ?? result;
@@ -322,4 +356,7 @@ export class PasskeyService {
322
356
  createExpiry() {
323
357
  return new Date(Date.now() + this.config.timeoutMs);
324
358
  }
359
+ requireUserVerification() {
360
+ return this.config.userVerification !== 'discouraged';
361
+ }
325
362
  }
@@ -55,6 +55,7 @@ export interface PasskeyStorageAdapter {
55
55
  saveCredential(record: StoredPasskeyCredential): Promise<void>;
56
56
  updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
57
57
  saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
58
+ getChallenge?(challenge: string): Promise<PasskeyChallengeRecord | null>;
58
59
  consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
59
60
  cleanupChallenges?(now: Date): Promise<void>;
60
61
  }
@@ -1,3 +1,8 @@
1
+ import { DataTypes, type InitOptions, type Model, type Sequelize } from 'sequelize';
1
2
  export declare const DIALECTS_SUPPORTING_UNSIGNED: Set<string>;
3
+ export declare function integerIdType(sequelize: Sequelize): DataTypes.IntegerDataTypeConstructor;
4
+ export declare function tableOptions<ModelType extends Model>(sequelize: Sequelize, tableName: string, tablePrefix?: string, extra?: Partial<InitOptions<ModelType>>): InitOptions<ModelType>;
2
5
  export declare function normalizeTablePrefix(prefix?: string): string | undefined;
3
6
  export declare function applyTablePrefix(prefix: string | undefined, tableName: string): string;
7
+ export declare function encodeStringArray(values: string[] | undefined): string;
8
+ export declare function decodeStringArray(raw: string | null | undefined): string[];
@@ -1,4 +1,19 @@
1
+ import { DataTypes } from 'sequelize';
1
2
  export const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
3
+ export function integerIdType(sequelize) {
4
+ return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
5
+ }
6
+ export function tableOptions(sequelize, tableName, tablePrefix, extra) {
7
+ const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
8
+ if (extra) {
9
+ Object.assign(opts, extra);
10
+ }
11
+ if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
12
+ opts.charset = 'utf8mb4';
13
+ opts.collate = 'utf8mb4_unicode_ci';
14
+ }
15
+ return opts;
16
+ }
2
17
  export function normalizeTablePrefix(prefix) {
3
18
  if (!prefix) {
4
19
  return undefined;
@@ -10,3 +25,24 @@ export function applyTablePrefix(prefix, tableName) {
10
25
  const normalized = normalizeTablePrefix(prefix);
11
26
  return normalized ? `${normalized}${tableName}` : tableName;
12
27
  }
28
+ export function encodeStringArray(values) {
29
+ return JSON.stringify(values ?? []);
30
+ }
31
+ export function decodeStringArray(raw) {
32
+ if (!raw) {
33
+ return [];
34
+ }
35
+ try {
36
+ const parsed = JSON.parse(raw);
37
+ if (Array.isArray(parsed)) {
38
+ return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
39
+ }
40
+ }
41
+ catch {
42
+ // ignore malformed values
43
+ }
44
+ return raw
45
+ .split(/\s+/)
46
+ .map((entry) => entry.trim())
47
+ .filter((entry) => entry.length > 0);
48
+ }
@@ -43,10 +43,10 @@ function normalizeTokenInternal(input) {
43
43
  const status = input.status === 'revoked' ? 'revoked' : expires.getTime() < Date.now() ? 'expired' : 'active';
44
44
  const sessionCookie = typeof input.sessionCookie === 'boolean' ? input.sessionCookie : false;
45
45
  return {
46
- ...input,
47
46
  accessToken: input.accessToken,
48
47
  refreshToken: input.refreshToken,
49
48
  userId,
49
+ clientId: typeof input.clientId === 'string' && input.clientId.length > 0 ? input.clientId : undefined,
50
50
  domain: typeof input.domain === 'string' ? input.domain : '',
51
51
  fingerprint: typeof input.fingerprint === 'string' ? input.fingerprint : '',
52
52
  label: typeof input.label === 'string' ? input.label : '',
@@ -70,6 +70,8 @@ export class TokenStore {
70
70
  normalizeToken(token) {
71
71
  return normalizeTokenInternal(token);
72
72
  }
73
+ // JWT helpers live on TokenStore so every adapter automatically exposes
74
+ // signing/verification without additional wiring.
73
75
  jwtSign(payload, secret, expiresInSeconds, options) {
74
76
  const opts = { ...(options ?? {}), expiresIn: expiresInSeconds };
75
77
  try {
@@ -1,8 +1,13 @@
1
1
  import { TokenStore } from './base.js';
2
2
  import type { Token } from './types.js';
3
+ export interface MemoryTokenStoreOptions {
4
+ maxTokens?: number;
5
+ }
3
6
  export declare class MemoryTokenStore extends TokenStore {
4
7
  private readonly tokens;
5
8
  private readonly tokensByUser;
9
+ private readonly maxTokens?;
10
+ constructor(options?: MemoryTokenStoreOptions);
6
11
  private indexToken;
7
12
  private unindexToken;
8
13
  private removeByRefreshToken;
@@ -20,4 +25,5 @@ export declare class MemoryTokenStore extends TokenStore {
20
25
  includeExpired?: boolean;
21
26
  }): Promise<Token[]>;
22
27
  close(): Promise<void>;
28
+ private enforceCapacity;
23
29
  }
@@ -1,6 +1,6 @@
1
1
  import { TokenStore } from './base.js';
2
2
  function comparableUserId(value) {
3
- if (value === undefined || value === null) {
3
+ if (value === undefined) {
4
4
  return undefined;
5
5
  }
6
6
  return String(value);
@@ -45,10 +45,14 @@ function matchesQuery(record, query, includeExpired) {
45
45
  return true;
46
46
  }
47
47
  export class MemoryTokenStore extends TokenStore {
48
- constructor() {
49
- super(...arguments);
48
+ constructor(options = {}) {
49
+ super();
50
50
  this.tokens = new Map();
51
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;
52
56
  }
53
57
  indexToken(token) {
54
58
  const userId = comparableUserId(token.userId);
@@ -115,6 +119,7 @@ export class MemoryTokenStore extends TokenStore {
115
119
  this.removeByRefreshToken(stored.refreshToken);
116
120
  this.tokens.set(stored.refreshToken, stored);
117
121
  this.indexToken(stored);
122
+ this.enforceCapacity();
118
123
  }
119
124
  async get(query, opts) {
120
125
  if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
@@ -162,8 +167,12 @@ export class MemoryTokenStore extends TokenStore {
162
167
  merged[key] = value;
163
168
  }
164
169
  };
165
- maybeAssign('accessToken');
166
- maybeAssign('expires');
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
+ }
167
176
  maybeAssign('scope');
168
177
  maybeAssign('label');
169
178
  maybeAssign('domain');
@@ -174,8 +183,12 @@ export class MemoryTokenStore extends TokenStore {
174
183
  maybeAssign('os');
175
184
  maybeAssign('refreshTtlSeconds');
176
185
  maybeAssign('loginType');
177
- maybeAssign('issuedAt');
178
- maybeAssign('lastSeenAt');
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
+ }
179
192
  maybeAssign('sessionCookie');
180
193
  const normalized = this.normalizeToken(merged);
181
194
  const previousUserId = token.userId;
@@ -207,4 +220,16 @@ export class MemoryTokenStore extends TokenStore {
207
220
  async close() {
208
221
  return;
209
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
+ }
210
235
  }
@@ -52,10 +52,7 @@ export declare class SequelizeTokenStore extends TokenStore {
52
52
  close(): Promise<void>;
53
53
  private normalizeUserId;
54
54
  private resolveRealUserId;
55
- private encodeStringArray;
56
- private decodeStringArray;
57
55
  private encodeScope;
58
- private decodeScope;
59
56
  private toTokenRecord;
60
57
  }
61
58
  export {};