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

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 (58) hide show
  1. package/dist/cjs/api-module.cjs +9 -0
  2. package/dist/cjs/api-module.d.ts +4 -2
  3. package/dist/cjs/api-server-base.cjs +59 -37
  4. package/dist/cjs/api-server-base.d.ts +5 -0
  5. package/dist/cjs/auth-api/auth-module.d.ts +12 -1
  6. package/dist/cjs/auth-api/auth-module.js +42 -34
  7. package/dist/cjs/auth-api/mem-auth-store.js +2 -24
  8. package/dist/cjs/auth-api/sql-auth-store.js +4 -32
  9. package/dist/cjs/auth-api/user-id.d.ts +4 -0
  10. package/dist/cjs/auth-api/user-id.js +31 -0
  11. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  12. package/dist/cjs/auth-cookie-options.js +57 -0
  13. package/dist/cjs/oauth/memory.js +2 -9
  14. package/dist/cjs/oauth/models.js +4 -15
  15. package/dist/cjs/oauth/sequelize.js +6 -22
  16. package/dist/cjs/passkey/config.d.ts +2 -0
  17. package/dist/cjs/passkey/config.js +26 -0
  18. package/dist/cjs/passkey/memory.js +2 -9
  19. package/dist/cjs/passkey/models.js +4 -15
  20. package/dist/cjs/passkey/sequelize.js +6 -22
  21. package/dist/cjs/sequelize-utils.d.ts +3 -0
  22. package/dist/cjs/sequelize-utils.js +17 -0
  23. package/dist/cjs/token/memory.d.ts +4 -0
  24. package/dist/cjs/token/memory.js +90 -25
  25. package/dist/cjs/token/sequelize.js +16 -22
  26. package/dist/cjs/token/types.d.ts +7 -0
  27. package/dist/cjs/user/memory.js +2 -9
  28. package/dist/cjs/user/sequelize.js +6 -22
  29. package/dist/esm/api-module.d.ts +4 -2
  30. package/dist/esm/api-module.js +9 -0
  31. package/dist/esm/api-server-base.d.ts +5 -0
  32. package/dist/esm/api-server-base.js +59 -37
  33. package/dist/esm/auth-api/auth-module.d.ts +12 -1
  34. package/dist/esm/auth-api/auth-module.js +42 -34
  35. package/dist/esm/auth-api/mem-auth-store.js +1 -23
  36. package/dist/esm/auth-api/sql-auth-store.js +2 -30
  37. package/dist/esm/auth-api/user-id.d.ts +4 -0
  38. package/dist/esm/auth-api/user-id.js +26 -0
  39. package/dist/esm/auth-cookie-options.d.ts +11 -0
  40. package/dist/esm/auth-cookie-options.js +54 -0
  41. package/dist/esm/oauth/memory.js +2 -9
  42. package/dist/esm/oauth/models.js +1 -12
  43. package/dist/esm/oauth/sequelize.js +3 -19
  44. package/dist/esm/passkey/config.d.ts +2 -0
  45. package/dist/esm/passkey/config.js +23 -0
  46. package/dist/esm/passkey/memory.js +2 -9
  47. package/dist/esm/passkey/models.js +1 -12
  48. package/dist/esm/passkey/sequelize.js +3 -19
  49. package/dist/esm/sequelize-utils.d.ts +3 -0
  50. package/dist/esm/sequelize-utils.js +12 -0
  51. package/dist/esm/token/memory.d.ts +4 -0
  52. package/dist/esm/token/memory.js +90 -25
  53. package/dist/esm/token/sequelize.js +12 -18
  54. package/dist/esm/token/types.d.ts +7 -0
  55. package/dist/esm/user/memory.js +2 -9
  56. package/dist/esm/user/sequelize.js +3 -19
  57. package/docs/swagger/openapi.json +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAuthCookieOptions = buildAuthCookieOptions;
4
+ function firstHeaderValue(value) {
5
+ if (typeof value === 'string') {
6
+ return value;
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value[0] ?? '';
10
+ }
11
+ return '';
12
+ }
13
+ function resolveOriginHostname(origin) {
14
+ try {
15
+ const url = new URL(origin);
16
+ const hostname = url.hostname.trim().toLowerCase();
17
+ return hostname.length > 0 ? hostname : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function isLocalhostOrigin(origin) {
24
+ const hostname = resolveOriginHostname(origin);
25
+ if (!hostname) {
26
+ return false;
27
+ }
28
+ return hostname === 'localhost' || hostname.endsWith('.localhost');
29
+ }
30
+ function buildAuthCookieOptions(config, req) {
31
+ const forwardedProto = firstHeaderValue(req.headers['x-forwarded-proto']).split(',')[0].trim().toLowerCase();
32
+ const isHttps = forwardedProto === 'https' || req.protocol === 'https';
33
+ const origin = firstHeaderValue(req.headers.origin ?? req.headers.referer);
34
+ const secure = config.cookieSecure === true ? true : config.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
35
+ let sameSite = config.cookieSameSite ?? 'lax';
36
+ if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
37
+ sameSite = 'lax';
38
+ }
39
+ let resolvedSecure = secure;
40
+ if (sameSite === 'none' && resolvedSecure !== true) {
41
+ // Modern browsers reject SameSite=None cookies unless Secure is set.
42
+ resolvedSecure = true;
43
+ }
44
+ const options = {
45
+ httpOnly: config.cookieHttpOnly ?? true,
46
+ secure: resolvedSecure,
47
+ sameSite,
48
+ domain: config.cookieDomain || undefined,
49
+ path: config.cookiePath || '/',
50
+ maxAge: undefined
51
+ };
52
+ if (config.devMode && isLocalhostOrigin(origin)) {
53
+ // Domain cookies do not work on localhost; avoid breaking local development when cookieDomain is set.
54
+ options.domain = undefined;
55
+ }
56
+ return options;
57
+ }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MemoryOAuthStore = void 0;
7
7
  const bcryptjs_1 = __importDefault(require("bcryptjs"));
8
+ const user_id_js_1 = require("../auth-api/user-id.js");
8
9
  const base_js_1 = require("./base.js");
9
10
  function cloneClient(client) {
10
11
  if (!client) {
@@ -29,15 +30,7 @@ function cloneCode(code) {
29
30
  metadata: code.metadata ? { ...code.metadata } : undefined
30
31
  };
31
32
  }
32
- function normalizeUserId(identifier) {
33
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
34
- return identifier;
35
- }
36
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
37
- return Number(identifier);
38
- }
39
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
40
- }
33
+ const normalizeUserId = user_id_js_1.normalizeNumericUserId;
41
34
  class MemoryOAuthStore extends base_js_1.OAuthStore {
42
35
  constructor(options = {}) {
43
36
  super();
@@ -4,27 +4,16 @@ exports.OAuthCodeModel = exports.OAuthClientModel = void 0;
4
4
  exports.initOAuthClientModel = initOAuthClientModel;
5
5
  exports.initOAuthCodeModel = initOAuthCodeModel;
6
6
  const sequelize_1 = require("sequelize");
7
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
8
- function normalizeTablePrefix(prefix) {
9
- if (!prefix) {
10
- return undefined;
11
- }
12
- const trimmed = prefix.trim();
13
- return trimmed.length > 0 ? trimmed : undefined;
14
- }
15
- function applyTablePrefix(prefix, tableName) {
16
- const normalized = normalizeTablePrefix(prefix);
17
- return normalized ? `${normalized}${tableName}` : tableName;
18
- }
7
+ const sequelize_utils_js_1 = require("../sequelize-utils.js");
19
8
  function integerIdType(sequelize) {
20
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
9
+ return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
21
10
  }
22
11
  function tableOptions(sequelize, tableName, tablePrefix, extra) {
23
- const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
12
+ const opts = { sequelize, tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, tableName) };
24
13
  if (extra) {
25
14
  Object.assign(opts, extra);
26
15
  }
27
- if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
16
+ if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
28
17
  opts.charset = 'utf8mb4';
29
18
  opts.collate = 'utf8mb4_unicode_ci';
30
19
  }
@@ -8,28 +8,18 @@ exports.initOAuthClientModel = initOAuthClientModel;
8
8
  exports.initOAuthCodeModel = initOAuthCodeModel;
9
9
  const bcryptjs_1 = __importDefault(require("bcryptjs"));
10
10
  const sequelize_1 = require("sequelize");
11
+ const user_id_js_1 = require("../auth-api/user-id.js");
12
+ const sequelize_utils_js_1 = require("../sequelize-utils.js");
11
13
  const base_js_1 = require("./base.js");
12
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
13
- function normalizeTablePrefix(prefix) {
14
- if (!prefix) {
15
- return undefined;
16
- }
17
- const trimmed = prefix.trim();
18
- return trimmed.length > 0 ? trimmed : undefined;
19
- }
20
- function applyTablePrefix(prefix, tableName) {
21
- const normalized = normalizeTablePrefix(prefix);
22
- return normalized ? `${normalized}${tableName}` : tableName;
23
- }
24
14
  function integerIdType(sequelize) {
25
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
15
+ return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
26
16
  }
27
17
  function tableOptions(sequelize, tableName, tablePrefix, extra) {
28
- const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
18
+ const opts = { sequelize, tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, tableName) };
29
19
  if (extra) {
30
20
  Object.assign(opts, extra);
31
21
  }
32
- if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
22
+ if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
33
23
  opts.charset = 'utf8mb4';
34
24
  opts.collate = 'utf8mb4_unicode_ci';
35
25
  }
@@ -111,13 +101,7 @@ function parseMetadata(raw) {
111
101
  return undefined;
112
102
  }
113
103
  function normalizeUserId(identifier) {
114
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
115
- return identifier;
116
- }
117
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
118
- return Number(identifier);
119
- }
120
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
104
+ return (0, user_id_js_1.normalizeNumericUserId)(identifier);
121
105
  }
122
106
  class SequelizeOAuthStore extends base_js_1.OAuthStore {
123
107
  constructor(options) {
@@ -0,0 +1,2 @@
1
+ import type { PasskeyServiceConfig } from './types.js';
2
+ export declare function normalizePasskeyConfig(config?: Partial<PasskeyServiceConfig>): PasskeyServiceConfig;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePasskeyConfig = normalizePasskeyConfig;
4
+ const DEFAULT_PASSKEY_CONFIG = {
5
+ rpId: 'localhost',
6
+ rpName: 'API Server',
7
+ origins: ['http://localhost:5173'],
8
+ timeoutMs: 5 * 60 * 1000,
9
+ userVerification: 'preferred'
10
+ };
11
+ function isOriginString(origin) {
12
+ return typeof origin === 'string' && origin.trim().length > 0;
13
+ }
14
+ function normalizePasskeyConfig(config = {}) {
15
+ const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
16
+ return {
17
+ rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
18
+ rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
19
+ origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
20
+ timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
21
+ ? config.timeoutMs
22
+ : DEFAULT_PASSKEY_CONFIG.timeoutMs,
23
+ userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification,
24
+ debug: Boolean(config.debug)
25
+ };
26
+ }
@@ -1,19 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MemoryPasskeyStore = 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 encodeCredentialId(value) {
6
7
  return Buffer.isBuffer(value) ? value.toString('base64') : value;
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
- return identifier;
16
- }
9
+ const normalizeUserId = user_id_js_1.normalizeComparableUserId;
17
10
  function cloneCredential(record) {
18
11
  return {
19
12
  ...record,
@@ -4,20 +4,9 @@ exports.PasskeyChallengeModel = exports.PasskeyCredentialModel = void 0;
4
4
  exports.initPasskeyCredentialModel = initPasskeyCredentialModel;
5
5
  exports.initPasskeyChallengeModel = initPasskeyChallengeModel;
6
6
  const sequelize_1 = require("sequelize");
7
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
8
- function normalizeTablePrefix(prefix) {
9
- if (!prefix) {
10
- return undefined;
11
- }
12
- const trimmed = prefix.trim();
13
- return trimmed.length > 0 ? trimmed : undefined;
14
- }
15
- function applyTablePrefix(prefix, tableName) {
16
- const normalized = normalizeTablePrefix(prefix);
17
- return normalized ? `${normalized}${tableName}` : tableName;
18
- }
7
+ const sequelize_utils_js_1 = require("../sequelize-utils.js");
19
8
  function integerIdType(sequelize) {
20
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
9
+ return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
21
10
  }
22
11
  class PasskeyCredentialModel extends sequelize_1.Model {
23
12
  }
@@ -115,7 +104,7 @@ function initPasskeyCredentialModel(sequelize, options = {}) {
115
104
  }
116
105
  }, {
117
106
  sequelize,
118
- tableName: applyTablePrefix(options.tablePrefix, 'passkey_credentials'),
107
+ tableName: (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'passkey_credentials'),
119
108
  timestamps: true,
120
109
  underscored: true
121
110
  });
@@ -148,7 +137,7 @@ function initPasskeyChallengeModel(sequelize, options = {}) {
148
137
  }
149
138
  }, {
150
139
  sequelize,
151
- tableName: applyTablePrefix(options.tablePrefix, 'passkey_challenges'),
140
+ tableName: (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'passkey_challenges'),
152
141
  timestamps: true,
153
142
  underscored: true,
154
143
  indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
@@ -2,33 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SequelizePasskeyStore = 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
- function normalizeTablePrefix(prefix) {
8
- if (!prefix) {
9
- return undefined;
10
- }
11
- const trimmed = prefix.trim();
12
- return trimmed.length > 0 ? trimmed : undefined;
13
- }
14
- function applyTablePrefix(prefix, tableName) {
15
- const normalized = normalizeTablePrefix(prefix);
16
- return normalized ? `${normalized}${tableName}` : tableName;
17
- }
18
8
  function integerIdType(sequelize) {
19
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
9
+ return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
20
10
  }
21
11
  function encodeCredentialId(value) {
22
12
  return Buffer.isBuffer(value) ? value.toString('base64') : value;
23
13
  }
24
14
  function normalizeUserId(identifier) {
25
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
26
- return identifier;
27
- }
28
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
29
- return Number(identifier);
30
- }
31
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
15
+ return (0, user_id_js_1.normalizeNumericUserId)(identifier);
32
16
  }
33
17
  class PasskeyCredentialModel extends sequelize_1.Model {
34
18
  }
@@ -124,7 +108,7 @@ function initPasskeyCredentialModel(sequelize, options = {}) {
124
108
  }
125
109
  }, {
126
110
  sequelize,
127
- tableName: applyTablePrefix(options.tablePrefix, 'passkey_credentials'),
111
+ tableName: (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'passkey_credentials'),
128
112
  timestamps: true,
129
113
  underscored: true
130
114
  });
@@ -157,7 +141,7 @@ function initPasskeyChallengeModel(sequelize, options = {}) {
157
141
  }
158
142
  }, {
159
143
  sequelize,
160
- tableName: applyTablePrefix(options.tablePrefix, 'passkey_challenges'),
144
+ tableName: (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'passkey_challenges'),
161
145
  timestamps: true,
162
146
  underscored: true,
163
147
  indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
@@ -0,0 +1,3 @@
1
+ export declare const DIALECTS_SUPPORTING_UNSIGNED: Set<string>;
2
+ export declare function normalizeTablePrefix(prefix?: string): string | undefined;
3
+ export declare function applyTablePrefix(prefix: string | undefined, tableName: string): string;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DIALECTS_SUPPORTING_UNSIGNED = void 0;
4
+ exports.normalizeTablePrefix = normalizeTablePrefix;
5
+ exports.applyTablePrefix = applyTablePrefix;
6
+ exports.DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
7
+ function normalizeTablePrefix(prefix) {
8
+ if (!prefix) {
9
+ return undefined;
10
+ }
11
+ const trimmed = prefix.trim();
12
+ return trimmed.length > 0 ? trimmed : undefined;
13
+ }
14
+ function applyTablePrefix(prefix, tableName) {
15
+ const normalized = normalizeTablePrefix(prefix);
16
+ return normalized ? `${normalized}${tableName}` : tableName;
17
+ }
@@ -2,6 +2,10 @@ import { TokenStore } from './base.js';
2
2
  import type { Token } from './types.js';
3
3
  export declare class MemoryTokenStore extends TokenStore {
4
4
  private readonly tokens;
5
+ private readonly tokensByUser;
6
+ private indexToken;
7
+ private unindexToken;
8
+ private removeByRefreshToken;
5
9
  save(record: Token): Promise<void>;
6
10
  get(query: Partial<Token>, opts?: {
7
11
  includeExpired?: boolean;
@@ -9,12 +9,12 @@ function comparableUserId(value) {
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) {
@@ -50,15 +50,57 @@ function matchesQuery(record, query, includeExpired) {
50
50
  class MemoryTokenStore extends base_js_1.TokenStore {
51
51
  constructor() {
52
52
  super(...arguments);
53
- this.tokens = [];
53
+ this.tokens = new Map();
54
+ this.tokensByUser = new Map();
55
+ }
56
+ indexToken(token) {
57
+ const userId = comparableUserId(token.userId);
58
+ if (!userId) {
59
+ return;
60
+ }
61
+ let userTokens = this.tokensByUser.get(userId);
62
+ if (!userTokens) {
63
+ userTokens = new Set();
64
+ this.tokensByUser.set(userId, userTokens);
65
+ }
66
+ userTokens.add(token.refreshToken);
67
+ }
68
+ unindexToken(token) {
69
+ const userId = comparableUserId(token.userId);
70
+ if (!userId) {
71
+ return;
72
+ }
73
+ const userTokens = this.tokensByUser.get(userId);
74
+ if (!userTokens) {
75
+ return;
76
+ }
77
+ userTokens.delete(token.refreshToken);
78
+ if (userTokens.size === 0) {
79
+ this.tokensByUser.delete(userId);
80
+ }
81
+ }
82
+ removeByRefreshToken(refreshToken) {
83
+ const existing = this.tokens.get(refreshToken);
84
+ if (!existing) {
85
+ return;
86
+ }
87
+ this.unindexToken(existing);
88
+ this.tokens.delete(refreshToken);
54
89
  }
55
90
  async save(record) {
56
91
  const stored = this.normalizeToken(record);
57
92
  const normalizedUserId = comparableUserId(stored.userId);
93
+ if (!normalizedUserId) {
94
+ throw new Error('userId is required');
95
+ }
58
96
  const domainProvided = record.domain !== undefined;
59
97
  const fingerprintProvided = record.fingerprint !== undefined;
60
- for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
61
- const existing = this.tokens[index];
98
+ const userRefreshTokens = [...(this.tokensByUser.get(normalizedUserId) ?? [])];
99
+ for (const refreshToken of userRefreshTokens) {
100
+ const existing = this.tokens.get(refreshToken);
101
+ if (!existing) {
102
+ continue;
103
+ }
62
104
  if (comparableUserId(existing.userId) !== normalizedUserId) {
63
105
  continue;
64
106
  }
@@ -71,44 +113,51 @@ class MemoryTokenStore extends base_js_1.TokenStore {
71
113
  if (fingerprintProvided && existing.fingerprint !== stored.fingerprint) {
72
114
  continue;
73
115
  }
74
- this.tokens.splice(index, 1);
116
+ this.removeByRefreshToken(existing.refreshToken);
75
117
  }
76
- this.tokens.push(stored);
118
+ this.removeByRefreshToken(stored.refreshToken);
119
+ this.tokens.set(stored.refreshToken, stored);
120
+ this.indexToken(stored);
77
121
  }
78
122
  async get(query, opts) {
79
123
  if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
80
124
  throw new Error('At least one token lookup field must be provided');
81
125
  }
82
126
  const includeExpired = opts?.includeExpired ?? false;
83
- const record = this.tokens.find((token) => matchesQuery(token, query, includeExpired));
84
- return record ? cloneToken(record) : null;
127
+ if (query.refreshToken) {
128
+ const record = this.tokens.get(query.refreshToken);
129
+ return record && matchesQuery(record, query, includeExpired) ? cloneToken(record) : null;
130
+ }
131
+ for (const token of this.tokens.values()) {
132
+ if (matchesQuery(token, query, includeExpired)) {
133
+ return cloneToken(token);
134
+ }
135
+ }
136
+ return null;
85
137
  }
86
138
  async delete(query) {
87
139
  if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
88
140
  return 0;
89
141
  }
90
142
  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);
143
+ const refreshTokens = [...this.tokens.keys()];
144
+ for (const refreshToken of refreshTokens) {
145
+ const token = this.tokens.get(refreshToken);
146
+ if (token && matchesQuery(token, query, true)) {
147
+ this.removeByRefreshToken(refreshToken);
94
148
  removed += 1;
95
149
  }
96
150
  }
97
151
  return removed;
98
152
  }
99
153
  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
- });
154
+ const token = this.tokens.get(params.refreshToken);
109
155
  if (!token) {
110
156
  return false;
111
157
  }
158
+ if (params.clientId && token.clientId !== params.clientId) {
159
+ return false;
160
+ }
112
161
  const merged = { ...token };
113
162
  const maybeAssign = (key) => {
114
163
  const value = params[key];
@@ -132,12 +181,28 @@ class MemoryTokenStore extends base_js_1.TokenStore {
132
181
  maybeAssign('lastSeenAt');
133
182
  maybeAssign('sessionCookie');
134
183
  const normalized = this.normalizeToken(merged);
184
+ const previousUserId = token.userId;
185
+ const previousRefreshToken = token.refreshToken;
186
+ const userChanged = comparableUserId(previousUserId) !== comparableUserId(normalized.userId);
135
187
  Object.assign(token, normalized);
188
+ if (userChanged || previousRefreshToken !== token.refreshToken) {
189
+ this.unindexToken({ ...token, userId: previousUserId, refreshToken: previousRefreshToken });
190
+ this.indexToken(token);
191
+ if (previousRefreshToken !== token.refreshToken) {
192
+ this.tokens.delete(previousRefreshToken);
193
+ this.tokens.set(token.refreshToken, token);
194
+ }
195
+ }
136
196
  return true;
137
197
  }
138
198
  async list(userId, opts = {}) {
139
199
  const includeExpired = opts.includeExpired ?? false;
140
- const filtered = this.tokens.filter((token) => matchesQuery(token, { userId: comparableUserId(userId) }, includeExpired));
200
+ const normalizedUserId = comparableUserId(userId);
201
+ const userRefreshTokens = normalizedUserId ? [...(this.tokensByUser.get(normalizedUserId) ?? [])] : [];
202
+ const filtered = userRefreshTokens
203
+ .map((refreshToken) => this.tokens.get(refreshToken))
204
+ .filter((token) => Boolean(token))
205
+ .filter((token) => matchesQuery(token, { userId: normalizedUserId }, includeExpired));
141
206
  const offset = opts.offset ?? 0;
142
207
  const limit = opts.limit ?? filtered.length;
143
208
  return filtered.slice(offset, offset + limit).map(cloneToken);
@@ -2,41 +2,31 @@
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
- function normalizeTablePrefix(prefix) {
8
- if (!prefix) {
9
- return undefined;
10
- }
11
- const trimmed = prefix.trim();
12
- return trimmed.length > 0 ? trimmed : undefined;
13
- }
14
- function applyTablePrefix(prefix, tableName) {
15
- const normalized = normalizeTablePrefix(prefix);
16
- return normalized ? `${normalized}${tableName}` : tableName;
17
- }
18
8
  class TokenModel extends sequelize_1.Model {
19
9
  }
20
10
  function tokenTableOptions(sequelize, tablePrefix) {
21
11
  const opts = {
22
12
  sequelize,
23
- tableName: applyTablePrefix(tablePrefix, 'jwttokens'),
13
+ tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, 'jwttokens'),
24
14
  timestamps: false
25
15
  };
26
- if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
16
+ if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
27
17
  opts.charset = 'utf8mb4';
28
18
  opts.collate = 'utf8mb4_unicode_ci';
29
19
  }
30
20
  return opts;
31
21
  }
32
22
  function initTokenModel(sequelize, options = {}) {
33
- const tableName = applyTablePrefix(options.tablePrefix, 'jwttokens');
23
+ const tableName = (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'jwttokens');
34
24
  const usePrefixedIndexNames = tableName !== 'jwttokens';
35
25
  const accessIndexName = usePrefixedIndexNames ? `${tableName}_access_unique` : 'jwt_access_unique';
36
26
  const refreshIndexName = usePrefixedIndexNames ? `${tableName}_refresh_unique` : 'jwt_refresh_unique';
37
27
  TokenModel.init({
38
28
  token_id: {
39
- type: DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())
29
+ type: sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())
40
30
  ? sequelize_1.DataTypes.INTEGER.UNSIGNED
41
31
  : sequelize_1.DataTypes.INTEGER,
42
32
  autoIncrement: true,
@@ -67,11 +57,11 @@ function initTokenModel(sequelize, options = {}) {
67
57
  defaultValue: sequelize_1.DataTypes.NOW
68
58
  },
69
59
  access: {
70
- type: sequelize_1.DataTypes.STRING(512),
60
+ type: sequelize_1.DataTypes.STRING(768),
71
61
  allowNull: false
72
62
  },
73
63
  refresh: {
74
- type: sequelize_1.DataTypes.STRING(512),
64
+ type: sequelize_1.DataTypes.STRING(768),
75
65
  allowNull: false
76
66
  },
77
67
  domain: {
@@ -184,6 +174,13 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
184
174
  removalWhere.client_id = normalized.clientId;
185
175
  }
186
176
  await this.Tokens.destroy({ where: removalWhere });
177
+ // Access/refresh columns are unique. Remove stale collisions before insert to avoid
178
+ // transient uniqueness failures during retries/rotation edge-cases.
179
+ await this.Tokens.destroy({
180
+ where: {
181
+ [sequelize_1.Op.or]: [{ access: normalized.accessToken ?? '' }, { refresh: normalized.refreshToken }]
182
+ }
183
+ });
187
184
  await this.Tokens.create({
188
185
  user_id: resolvedUserId,
189
186
  real_user_id: resolvedRealUserId,
@@ -343,10 +340,7 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
343
340
  return;
344
341
  }
345
342
  normalizeUserId(identifier) {
346
- if (identifier === undefined || identifier === null) {
347
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
348
- }
349
- return String(identifier);
343
+ return (0, user_id_js_1.normalizeStringUserId)(identifier);
350
344
  }
351
345
  resolveRealUserId(ruid) {
352
346
  if (ruid === undefined || ruid === null) {
@@ -9,7 +9,14 @@ 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;
@@ -1,19 +1,12 @@
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
- }
9
+ const normalizeUserId = user_id_js_1.normalizeNumericUserId;
17
10
  class MemoryUserStore extends base_js_1.UserStore {
18
11
  constructor(options = {}) {
19
12
  super({