@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
@@ -7,18 +7,6 @@ const sequelize_utils_js_1 = require("../sequelize-utils.js");
7
7
  const base_js_1 = require("./base.js");
8
8
  class TokenModel extends sequelize_1.Model {
9
9
  }
10
- function tokenTableOptions(sequelize, tablePrefix) {
11
- const opts = {
12
- sequelize,
13
- tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, 'jwttokens'),
14
- timestamps: false
15
- };
16
- if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
17
- opts.charset = 'utf8mb4';
18
- opts.collate = 'utf8mb4_unicode_ci';
19
- }
20
- return opts;
21
- }
22
10
  function initTokenModel(sequelize, options = {}) {
23
11
  const tableName = (0, sequelize_utils_js_1.applyTablePrefix)(options.tablePrefix, 'jwttokens');
24
12
  const usePrefixedIndexNames = tableName !== 'jwttokens';
@@ -26,9 +14,7 @@ function initTokenModel(sequelize, options = {}) {
26
14
  const refreshIndexName = usePrefixedIndexNames ? `${tableName}_refresh_unique` : 'jwt_refresh_unique';
27
15
  TokenModel.init({
28
16
  token_id: {
29
- type: sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())
30
- ? sequelize_1.DataTypes.INTEGER.UNSIGNED
31
- : sequelize_1.DataTypes.INTEGER,
17
+ type: (0, sequelize_utils_js_1.integerIdType)(sequelize),
32
18
  autoIncrement: true,
33
19
  allowNull: false,
34
20
  primaryKey: true
@@ -125,7 +111,7 @@ function initTokenModel(sequelize, options = {}) {
125
111
  defaultValue: '[]'
126
112
  }
127
113
  }, {
128
- ...tokenTableOptions(sequelize, options.tablePrefix),
114
+ ...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'jwttokens', options.tablePrefix, { timestamps: false }),
129
115
  indexes: [
130
116
  { name: accessIndexName, unique: true, fields: ['access'] },
131
117
  { name: refreshIndexName, unique: true, fields: ['refresh'] }
@@ -164,43 +150,50 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
164
150
  const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
165
151
  const sessionCookie = normalized.sessionCookie ?? false;
166
152
  const removalWhere = { user_id: resolvedUserId };
167
- if (normalized.domain !== undefined && record.domain !== undefined) {
153
+ if (record.domain !== undefined) {
168
154
  removalWhere.domain = domain;
169
155
  }
170
- if (normalized.fingerprint !== undefined && record.fingerprint !== undefined) {
156
+ if (record.fingerprint !== undefined) {
171
157
  removalWhere.fingerprint = fingerprint;
172
158
  }
173
159
  if (normalized.clientId) {
174
160
  removalWhere.client_id = normalized.clientId;
175
161
  }
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
- });
184
- await this.Tokens.create({
185
- user_id: resolvedUserId,
186
- real_user_id: resolvedRealUserId,
187
- access: normalized.accessToken ?? '',
188
- refresh: normalized.refreshToken,
189
- expires: normalized.expires,
190
- issued_at: issuedAt,
191
- last_seen_at: lastSeenAt,
192
- domain,
193
- fingerprint,
194
- label,
195
- browser,
196
- device,
197
- ip,
198
- os,
199
- client_id: normalized.clientId ?? null,
200
- scope: this.encodeScope(normalized.scope),
201
- login_type: loginType,
202
- refresh_ttl_seconds: refreshTtlSeconds,
203
- 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 });
204
197
  });
205
198
  }
206
199
  async get(query, opts) {
@@ -269,11 +262,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
269
262
  where.client_id = params.clientId;
270
263
  }
271
264
  const updates = {};
272
- if (params.accessToken !== undefined) {
273
- updates.access = params.accessToken ?? null;
265
+ if (params.accessToken !== undefined && params.accessToken !== null) {
266
+ updates.access = params.accessToken;
274
267
  }
275
- if (params.expires !== undefined) {
276
- updates.expires = params.expires ?? null;
268
+ if (params.expires !== undefined && params.expires !== null) {
269
+ updates.expires = params.expires;
277
270
  }
278
271
  if (params.scope !== undefined) {
279
272
  updates.scope = this.encodeScope(params.scope);
@@ -311,11 +304,11 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
311
304
  if (params.sessionCookie !== undefined) {
312
305
  updates.session_cookie = params.sessionCookie;
313
306
  }
314
- if (params.issuedAt !== undefined) {
315
- updates.issued_at = params.issuedAt ?? null;
307
+ if (params.issuedAt !== undefined && params.issuedAt !== null) {
308
+ updates.issued_at = params.issuedAt;
316
309
  }
317
- if (params.lastSeenAt !== undefined) {
318
- updates.last_seen_at = params.lastSeenAt ?? null;
310
+ if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
311
+ updates.last_seen_at = params.lastSeenAt;
319
312
  }
320
313
  if (Object.keys(updates).length === 0) {
321
314
  return false;
@@ -352,41 +345,17 @@ class SequelizeTokenStore extends base_js_1.TokenStore {
352
345
  }
353
346
  return value;
354
347
  }
355
- encodeStringArray(values) {
356
- return JSON.stringify(values ?? []);
357
- }
358
- decodeStringArray(raw) {
359
- if (!raw) {
360
- return [];
361
- }
362
- try {
363
- const parsed = JSON.parse(raw);
364
- if (Array.isArray(parsed)) {
365
- return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
366
- }
367
- }
368
- catch {
369
- // ignore malformed values
370
- }
371
- return raw
372
- .split(/\s+/)
373
- .map((entry) => entry.trim())
374
- .filter((entry) => entry.length > 0);
375
- }
376
348
  encodeScope(scope) {
377
349
  if (!scope || (Array.isArray(scope) && scope.length === 0)) {
378
350
  return '[]';
379
351
  }
380
352
  if (Array.isArray(scope)) {
381
- return this.encodeStringArray(scope);
353
+ return (0, sequelize_utils_js_1.encodeStringArray)(scope);
382
354
  }
383
- return this.encodeStringArray(scope.split(/\s+/).filter((entry) => entry.length > 0));
384
- }
385
- decodeScope(raw) {
386
- return this.decodeStringArray(raw);
355
+ return (0, sequelize_utils_js_1.encodeStringArray)(scope.split(/\s+/).filter((entry) => entry.length > 0));
387
356
  }
388
357
  toTokenRecord(model) {
389
- const scope = this.decodeScope(model.scope);
358
+ const scope = (0, sequelize_utils_js_1.decodeStringArray)(model.scope);
390
359
  const normalized = this.normalizeToken({
391
360
  userId: model.user_id,
392
361
  refreshToken: model.refresh,
@@ -23,7 +23,7 @@ export interface Token {
23
23
  device?: string;
24
24
  ip?: string;
25
25
  os?: string;
26
- scope?: string | string[];
26
+ scope?: string[];
27
27
  loginType?: string;
28
28
  refreshTtlSeconds?: number;
29
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>;
@@ -6,7 +6,6 @@ const base_js_1 = require("./base.js");
6
6
  function cloneUser(user) {
7
7
  return { ...user };
8
8
  }
9
- const normalizeUserId = user_id_js_1.normalizeNumericUserId;
10
9
  class MemoryUserStore extends base_js_1.UserStore {
11
10
  constructor(options = {}) {
12
11
  super({
@@ -18,6 +17,10 @@ class MemoryUserStore extends base_js_1.UserStore {
18
17
  this.loginToId = new Map();
19
18
  this.emailToId = new Map();
20
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;
21
24
  this.userIdFactory =
22
25
  options.userIdFactory ??
23
26
  (() => {
@@ -52,7 +55,7 @@ class MemoryUserStore extends base_js_1.UserStore {
52
55
  }
53
56
  async findById(id) {
54
57
  try {
55
- const numeric = normalizeUserId(id);
58
+ const numeric = (0, user_id_js_1.normalizeNumericUserId)(id);
56
59
  const user = this.usersById.get(numeric);
57
60
  return user ? cloneUser(user) : null;
58
61
  }
@@ -70,6 +73,9 @@ class MemoryUserStore extends base_js_1.UserStore {
70
73
  if (this.usersById.has(userId)) {
71
74
  throw new Error(`User ${userId} already exists`);
72
75
  }
76
+ if (this.maxUsers !== undefined && this.usersById.size >= this.maxUsers) {
77
+ throw new Error('MemoryUserStore maxUsers limit reached');
78
+ }
73
79
  if (this.loginToId.has(normalizedInput.login)) {
74
80
  throw new Error(`User with login ${normalizedInput.login} already exists`);
75
81
  }
@@ -6,26 +6,11 @@ const sequelize_1 = require("sequelize");
6
6
  const user_id_js_1 = require("../auth-api/user-id.js");
7
7
  const sequelize_utils_js_1 = require("../sequelize-utils.js");
8
8
  const base_js_1 = require("./base.js");
9
- function integerIdType(sequelize) {
10
- return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
11
- }
12
- function userTableOptions(sequelize, tablePrefix) {
13
- const opts = {
14
- sequelize,
15
- tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, 'users'),
16
- timestamps: false
17
- };
18
- if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
19
- opts.charset = 'utf8mb4';
20
- opts.collate = 'utf8mb4_unicode_ci';
21
- }
22
- return opts;
23
- }
24
9
  class AuthUserModel extends sequelize_1.Model {
25
10
  }
26
11
  exports.AuthUserModel = AuthUserModel;
27
12
  function initAuthUserModel(sequelize, options = {}) {
28
- const idType = integerIdType(sequelize);
13
+ const idType = (0, sequelize_utils_js_1.integerIdType)(sequelize);
29
14
  AuthUserModel.init({
30
15
  user_id: {
31
16
  type: idType,
@@ -48,7 +33,7 @@ function initAuthUserModel(sequelize, options = {}) {
48
33
  allowNull: false
49
34
  }
50
35
  }, {
51
- ...userTableOptions(sequelize, options.tablePrefix)
36
+ ...(0, sequelize_utils_js_1.tableOptions)(sequelize, 'users', options.tablePrefix, { timestamps: false })
52
37
  });
53
38
  return AuthUserModel;
54
39
  }
@@ -115,11 +100,16 @@ class SequelizeUserStore extends base_js_1.UserStore {
115
100
  if (providedId !== undefined && providedId !== null && Number.isFinite(providedId)) {
116
101
  defaults.user_id = Number(providedId);
117
102
  }
118
- const [model] = await this.Users.findOrCreate({
119
- where: { login: rest.login },
120
- defaults: defaults
121
- });
122
- return this.toUserRecord(model);
103
+ try {
104
+ const model = await this.Users.create(defaults);
105
+ return this.toUserRecord(model);
106
+ }
107
+ catch (error) {
108
+ if (error instanceof sequelize_1.UniqueConstraintError) {
109
+ throw new Error(`User with login ${rest.login} or email ${rest.email} already exists`);
110
+ }
111
+ throw error;
112
+ }
123
113
  }
124
114
  async upsertUser(input) {
125
115
  const normalized = this.normalizeUserInput(input);
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import { randomUUID } from 'node:crypto';
8
- import fs from 'node:fs';
8
+ import { access, readFile } from 'node:fs/promises';
9
9
  import { createRequire } from 'node:module';
10
10
  import path from 'node:path';
11
11
  import cookieParser from 'cookie-parser';
@@ -14,6 +14,7 @@ import express from 'express';
14
14
  import multer from 'multer';
15
15
  import { nullAuthModule } from './auth-api/module.js';
16
16
  import { nullAuthAdapter } from './auth-api/storage.js';
17
+ import { toOptionalStringId } from './auth-api/user-id.js';
17
18
  import { buildAuthCookieOptions } from './auth-cookie-options.js';
18
19
  import { TokenStore } from './token/base.js';
19
20
  class JwtHelperStore extends TokenStore {
@@ -267,7 +268,9 @@ function collectClientIpChain(req) {
267
268
  }
268
269
  const realIp = req.headers['x-real-ip'];
269
270
  if (Array.isArray(realIp)) {
270
- realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
271
+ for (const value of realIp) {
272
+ pushNormalized(normalizeIpAddress(value));
273
+ }
271
274
  }
272
275
  else if (typeof realIp === 'string') {
273
276
  pushNormalized(normalizeIpAddress(realIp));
@@ -421,6 +424,10 @@ export class ApiServer {
421
424
  this.canImpersonateAdapter = canImpersonate ?? null;
422
425
  this.storageAdapter = this.getServerAuthAdapter();
423
426
  }
427
+ if ((this.config.authApi || this.config.authStores) &&
428
+ (!this.config.accessSecret || !this.config.refreshSecret)) {
429
+ console.warn('[api-server-base] Auth is enabled but accessSecret and/or refreshSecret are empty. JWT signing will fail at runtime.');
430
+ }
424
431
  this.app = express();
425
432
  // Mount API modules and any custom endpoints under `apiBasePath` on this router so we can keep
426
433
  // the API 404 handler ordered last without relying on Express internals.
@@ -608,8 +615,8 @@ export class ApiServer {
608
615
  async getToken(query, opts) {
609
616
  const normalized = {
610
617
  ...query,
611
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
612
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
618
+ userId: toOptionalStringId(query.userId),
619
+ ruid: toOptionalStringId(query.ruid)
613
620
  };
614
621
  if (this.tokenStoreAdapter) {
615
622
  return this.tokenStoreAdapter.get(normalized, opts);
@@ -623,8 +630,8 @@ export class ApiServer {
623
630
  async deleteToken(query) {
624
631
  const normalized = {
625
632
  ...query,
626
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
627
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
633
+ userId: toOptionalStringId(query.userId),
634
+ ruid: toOptionalStringId(query.ruid)
628
635
  };
629
636
  if (this.tokenStoreAdapter) {
630
637
  return this.tokenStoreAdapter.delete(normalized);
@@ -787,7 +794,7 @@ export class ApiServer {
787
794
  res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
788
795
  });
789
796
  }
790
- loadSwaggerSpec() {
797
+ async loadSwaggerSpec() {
791
798
  const candidates = [path.resolve(process.cwd(), 'docs/swagger/openapi.json')];
792
799
  if (typeof __dirname === 'string') {
793
800
  candidates.push(path.resolve(__dirname, '../../docs/swagger/openapi.json'));
@@ -802,11 +809,14 @@ export class ApiServer {
802
809
  // Ignore resolution failures; fall back to any existing candidate.
803
810
  }
804
811
  for (const candidate of candidates) {
805
- if (!fs.existsSync(candidate)) {
812
+ try {
813
+ await access(candidate);
814
+ }
815
+ catch {
806
816
  continue;
807
817
  }
808
818
  try {
809
- const raw = fs.readFileSync(candidate, 'utf8');
819
+ const raw = await readFile(candidate, 'utf8');
810
820
  return JSON.parse(raw);
811
821
  }
812
822
  catch {
@@ -824,12 +834,12 @@ export class ApiServer {
824
834
  const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
825
835
  const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
826
836
  const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
827
- let specCache;
828
- this.app.get(path, (_req, res) => {
829
- if (specCache === undefined) {
830
- specCache = this.loadSwaggerSpec();
837
+ let specPromise;
838
+ this.app.get(path, async (_req, res) => {
839
+ if (!specPromise) {
840
+ specPromise = this.loadSwaggerSpec();
831
841
  }
832
- const spec = specCache;
842
+ const spec = await specPromise;
833
843
  if (!spec) {
834
844
  res.status(500).json({
835
845
  success: false,
@@ -1042,7 +1052,7 @@ export class ApiServer {
1042
1052
  let tokenData;
1043
1053
  let error;
1044
1054
  let expired = false;
1045
- if (!token || token === null) {
1055
+ if (!token) {
1046
1056
  if (authType === 'maybe') {
1047
1057
  if (!this.config.refreshMaybe) {
1048
1058
  return null;
@@ -1195,9 +1205,6 @@ export class ApiServer {
1195
1205
  if (rawReal === null) {
1196
1206
  return effectiveUserId;
1197
1207
  }
1198
- if (typeof rawReal === 'number' && rawReal === 0) {
1199
- return effectiveUserId;
1200
- }
1201
1208
  return rawReal;
1202
1209
  }
1203
1210
  useExpress(pathOrHandler, ...handlers) {
@@ -1421,9 +1428,29 @@ export class ApiServer {
1421
1428
  console.log('URL:', url);
1422
1429
  console.log('Method:', req.method);
1423
1430
  console.log('Query Params:', req.query || {});
1424
- console.log('Body Params:', req.body || {});
1425
- console.log('Cookies:', req.cookies || {});
1426
- console.log('Headers:', req.headers);
1431
+ const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
1432
+ const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
1433
+ if (body && typeof body === 'object') {
1434
+ for (const key of sensitiveBodyKeys) {
1435
+ if (key in body) {
1436
+ body[key] = '[REDACTED]';
1437
+ }
1438
+ }
1439
+ }
1440
+ console.log('Body Params:', body || {});
1441
+ const cookies = req.cookies ? { ...req.cookies } : {};
1442
+ const sensitiveCookieKeys = [this.config.accessCookie, this.config.refreshCookie];
1443
+ for (const key of sensitiveCookieKeys) {
1444
+ if (key in cookies) {
1445
+ cookies[key] = '[REDACTED]';
1446
+ }
1447
+ }
1448
+ console.log('Cookies:', cookies);
1449
+ const headers = { ...req.headers };
1450
+ if (headers.authorization) {
1451
+ headers.authorization = '[REDACTED]';
1452
+ }
1453
+ console.log('Headers:', headers);
1427
1454
  console.log('------------------------');
1428
1455
  }
1429
1456
  formatDebugValue(value, maxLength = 50, seen = new WeakSet()) {
@@ -155,6 +155,9 @@ class AuthModule extends BaseAuthModule {
155
155
  return candidate ? {} : { sessionCookie: true, refreshTtlSeconds: this.sessionRefreshTtlSeconds() };
156
156
  }
157
157
  if (typeof candidate === 'number') {
158
+ if (candidate === 0) {
159
+ return { sessionCookie: true, refreshTtlSeconds: this.sessionRefreshTtlSeconds() };
160
+ }
158
161
  const ttl = this.normalizeRefreshTtlSeconds(candidate);
159
162
  return ttl ? { sessionCookie: false, refreshTtlSeconds: ttl } : {};
160
163
  }
@@ -447,20 +450,12 @@ class AuthModule extends BaseAuthModule {
447
450
  this.assertAuthReady();
448
451
  const { login, password, ...metadata } = this.parseLoginBody(apiReq);
449
452
  const user = await this.storage.getUser(login);
450
- if (!user) {
453
+ const hash = user ? this.storage.getUserPasswordHash(user) : '';
454
+ const verified = user ? await this.storage.verifyPassword(password, hash) : false;
455
+ if (!user || !verified) {
451
456
  throw new ApiError({
452
457
  code: 400,
453
- message: 'Invalid credentials',
454
- errors: { login: 'Unknown user' }
455
- });
456
- }
457
- const hash = this.storage.getUserPasswordHash(user);
458
- const verified = await this.storage.verifyPassword(password, hash);
459
- if (!verified) {
460
- throw new ApiError({
461
- code: 400,
462
- message: 'Invalid credentials',
463
- errors: { password: 'Wrong password' }
458
+ message: 'Invalid credentials'
464
459
  });
465
460
  }
466
461
  const pair = await this.issueTokens(apiReq, user, metadata);
@@ -502,6 +497,7 @@ class AuthModule extends BaseAuthModule {
502
497
  refreshTtlSeconds: sessionPrefs.refreshTtlSeconds ?? stored.refreshTtlSeconds,
503
498
  sessionCookie: sessionPrefs.sessionCookie ?? stored.sessionCookie
504
499
  };
500
+ await this.storage.deleteToken({ refreshToken: providedToken });
505
501
  const pair = await this.issueTokens(apiReq, user, metadata);
506
502
  const publicUser = this.storage.filterUser(user);
507
503
  return [200, { ...pair, user: publicUser }];
@@ -911,7 +907,7 @@ class AuthModule extends BaseAuthModule {
911
907
  }
912
908
  }
913
909
  }
914
- else if (!clientSecretProvided && client.clientSecret) {
910
+ else if (!clientSecretProvided && (client.hasSecret ?? Boolean(client.clientSecret))) {
915
911
  throw new ApiError({ code: 400, message: 'Client authentication required when no PKCE challenge present' });
916
912
  }
917
913
  const user = await this.getUserOrThrow(record.userId, 'User not found');
@@ -946,6 +942,7 @@ class AuthModule extends BaseAuthModule {
946
942
  throw new ApiError({ code: 400, message: 'Refresh token issued to another client' });
947
943
  }
948
944
  const user = await this.getUserOrThrow(stored.userId ?? verify.data.uid, 'User not found');
945
+ await this.storage.deleteToken({ refreshToken });
949
946
  const tokens = await this.issueTokens(apiReq, user, {
950
947
  clientId: client.clientId,
951
948
  scope: stored.scope,
@@ -954,11 +951,7 @@ class AuthModule extends BaseAuthModule {
954
951
  loginType: stored.loginType ?? 'oauth'
955
952
  });
956
953
  this.clearOAuthCookies(apiReq);
957
- const scope = Array.isArray(stored.scope)
958
- ? stored.scope
959
- : typeof stored.scope === 'string'
960
- ? stored.scope.split(/\s+/).filter((entry) => entry.length > 0)
961
- : [];
954
+ const scope = Array.isArray(stored.scope) ? stored.scope : [];
962
955
  return [200, this.buildTokenResponse(tokens, client, scope)];
963
956
  }
964
957
  clearOAuthCookies(apiReq) {
@@ -1021,7 +1014,7 @@ class AuthModule extends BaseAuthModule {
1021
1014
  if (!client) {
1022
1015
  throw new ApiError({ code: 400, message: 'Unknown client_id' });
1023
1016
  }
1024
- const requiresSecret = !!client.clientSecret;
1017
+ const requiresSecret = client.hasSecret ?? Boolean(client.clientSecret);
1025
1018
  if (requiresSecret) {
1026
1019
  if (!secretProvided) {
1027
1020
  throw new ApiError({ code: 400, message: 'Client authentication is required' });
@@ -1057,17 +1050,10 @@ class AuthModule extends BaseAuthModule {
1057
1050
  const password = toStringOrNull(body.password);
1058
1051
  if (login && password) {
1059
1052
  const user = await this.storage.getUser(login);
1060
- if (!user) {
1061
- throw new ApiError({ code: 400, message: 'Invalid credentials', errors: { login: 'Unknown user' } });
1062
- }
1063
- const hash = this.storage.getUserPasswordHash(user);
1064
- const verified = await this.storage.verifyPassword(password, hash);
1065
- if (!verified) {
1066
- throw new ApiError({
1067
- code: 400,
1068
- message: 'Invalid credentials',
1069
- errors: { password: 'Wrong password' }
1070
- });
1053
+ const hash = user ? this.storage.getUserPasswordHash(user) : '';
1054
+ const verified = user ? await this.storage.verifyPassword(password, hash) : false;
1055
+ if (!user || !verified) {
1056
+ throw new ApiError({ code: 400, message: 'Invalid credentials' });
1071
1057
  }
1072
1058
  return user;
1073
1059
  }
@@ -4,6 +4,7 @@ import { MemoryPasskeyStore } from '../passkey/memory.js';
4
4
  import { MemoryTokenStore } from '../token/memory.js';
5
5
  import { MemoryUserStore } from '../user/memory.js';
6
6
  import { CompositeAuthAdapter } from './compat-auth-storage.js';
7
+ import { toOptionalStringId } from './user-id.js';
7
8
  export class MemAuthStore {
8
9
  constructor(params = {}) {
9
10
  this.userStore = new MemoryUserStore({
@@ -70,16 +71,16 @@ export class MemAuthStore {
70
71
  async getToken(query, opts) {
71
72
  const normalized = {
72
73
  ...query,
73
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
74
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
74
+ userId: toOptionalStringId(query.userId),
75
+ ruid: toOptionalStringId(query.ruid)
75
76
  };
76
77
  return this.adapter.getToken(normalized, opts);
77
78
  }
78
79
  async deleteToken(query) {
79
80
  const normalized = {
80
81
  ...query,
81
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
82
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
82
+ userId: toOptionalStringId(query.userId),
83
+ ruid: toOptionalStringId(query.ruid)
83
84
  };
84
85
  return this.adapter.deleteToken(normalized);
85
86
  }