@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
@@ -5,6 +5,7 @@ import { normalizeTablePrefix } from '../sequelize-utils.js';
5
5
  import { SequelizeTokenStore } from '../token/sequelize.js';
6
6
  import { SequelizeUserStore } from '../user/sequelize.js';
7
7
  import { CompositeAuthAdapter } from './compat-auth-storage.js';
8
+ import { toOptionalStringId } from './user-id.js';
8
9
  function resolveTablePrefix(...prefixes) {
9
10
  for (const prefix of prefixes) {
10
11
  const normalized = normalizeTablePrefix(prefix);
@@ -103,6 +104,7 @@ export class SqlAuthStore {
103
104
  }
104
105
  }
105
106
  finally {
107
+ // Prevent double-close errors when the same Sequelize instance is shared with other code.
106
108
  this.sequelize.close = async () => { };
107
109
  }
108
110
  }
@@ -127,16 +129,16 @@ export class SqlAuthStore {
127
129
  async getToken(query, opts) {
128
130
  const normalized = {
129
131
  ...query,
130
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
131
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
132
+ userId: toOptionalStringId(query.userId),
133
+ ruid: toOptionalStringId(query.ruid)
132
134
  };
133
135
  return this.adapter.getToken(normalized, opts);
134
136
  }
135
137
  async deleteToken(query) {
136
138
  const normalized = {
137
139
  ...query,
138
- userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
139
- ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
140
+ userId: toOptionalStringId(query.userId),
141
+ ruid: toOptionalStringId(query.ruid)
140
142
  };
141
143
  return this.adapter.deleteToken(normalized);
142
144
  }
@@ -2,3 +2,4 @@ import type { AuthIdentifier } from './types.js';
2
2
  export declare function normalizeComparableUserId(identifier: AuthIdentifier): string;
3
3
  export declare function normalizeNumericUserId(identifier: AuthIdentifier): number;
4
4
  export declare function normalizeStringUserId(identifier: AuthIdentifier): string;
5
+ export declare function toOptionalStringId(value: AuthIdentifier | undefined | null): string | undefined;
@@ -24,3 +24,9 @@ export function normalizeNumericUserId(identifier) {
24
24
  export function normalizeStringUserId(identifier) {
25
25
  return normalizeComparableUserId(identifier);
26
26
  }
27
+ export function toOptionalStringId(value) {
28
+ if (value === undefined || value === null) {
29
+ return undefined;
30
+ }
31
+ return String(value);
32
+ }
@@ -28,7 +28,16 @@ export function buildAuthCookieOptions(config, req) {
28
28
  const forwardedProto = firstHeaderValue(req.headers['x-forwarded-proto']).split(',')[0].trim().toLowerCase();
29
29
  const isHttps = forwardedProto === 'https' || req.protocol === 'https';
30
30
  const origin = firstHeaderValue(req.headers.origin ?? req.headers.referer);
31
- const secure = config.cookieSecure === true ? true : config.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
31
+ let secure;
32
+ if (config.cookieSecure === true) {
33
+ secure = true;
34
+ }
35
+ else if (config.cookieSecure === false) {
36
+ secure = false;
37
+ }
38
+ else {
39
+ secure = isHttps;
40
+ }
32
41
  let sameSite = config.cookieSameSite ?? 'lax';
33
42
  if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
34
43
  sameSite = 'lax';
@@ -1,11 +1,15 @@
1
1
  import { OAuthStore, type AuthCode, type OAuthClient } from './base.js';
2
2
  export interface MemoryOAuthStoreOptions {
3
3
  bcryptRounds?: number;
4
+ maxClients?: number;
5
+ maxAuthCodes?: number;
4
6
  }
5
7
  export declare class MemoryOAuthStore extends OAuthStore {
6
8
  private readonly clients;
7
9
  private readonly codes;
8
10
  private readonly bcryptRounds;
11
+ private readonly maxClients?;
12
+ private readonly maxAuthCodes?;
9
13
  constructor(options?: MemoryOAuthStoreOptions);
10
14
  getClient(clientId: string): Promise<OAuthClient | null>;
11
15
  createClient(input: OAuthClient): Promise<OAuthClient>;
@@ -13,4 +17,6 @@ export declare class MemoryOAuthStore extends OAuthStore {
13
17
  createAuthCode(code: AuthCode): Promise<void>;
14
18
  consumeAuthCode(code: string): Promise<AuthCode | null>;
15
19
  close(): Promise<void>;
20
+ private enforceClientCapacity;
21
+ private enforceCodeCapacity;
16
22
  }
@@ -1,5 +1,5 @@
1
1
  import bcrypt from 'bcryptjs';
2
- import { normalizeNumericUserId } from '../auth-api/user-id.js';
2
+ import { normalizeComparableUserId } from '../auth-api/user-id.js';
3
3
  import { OAuthStore } from './base.js';
4
4
  function cloneClient(client) {
5
5
  if (!client) {
@@ -7,8 +7,7 @@ function cloneClient(client) {
7
7
  }
8
8
  return {
9
9
  clientId: client.clientId,
10
- // clientSecret is stored hashed; do not return the hash.
11
- clientSecret: client.clientSecret ? '__stored__' : undefined,
10
+ hasSecret: Boolean(client.clientSecret),
12
11
  name: client.name,
13
12
  redirectUris: [...client.redirectUris],
14
13
  scope: client.scope ? [...client.scope] : undefined,
@@ -19,18 +18,28 @@ function cloneClient(client) {
19
18
  function cloneCode(code) {
20
19
  return {
21
20
  ...code,
21
+ userId: normalizeComparableUserId(code.userId),
22
22
  scope: code.scope ? [...code.scope] : undefined,
23
23
  expiresAt: new Date(code.expiresAt),
24
24
  metadata: code.metadata ? { ...code.metadata } : undefined
25
25
  };
26
26
  }
27
- const normalizeUserId = normalizeNumericUserId;
28
27
  export class MemoryOAuthStore extends OAuthStore {
29
28
  constructor(options = {}) {
30
29
  super();
31
30
  this.clients = new Map();
32
31
  this.codes = new Map();
33
32
  this.bcryptRounds = options.bcryptRounds ?? 12;
33
+ this.maxClients =
34
+ typeof options.maxClients === 'number' && Number.isFinite(options.maxClients) && options.maxClients > 0
35
+ ? Math.floor(options.maxClients)
36
+ : undefined;
37
+ this.maxAuthCodes =
38
+ typeof options.maxAuthCodes === 'number' &&
39
+ Number.isFinite(options.maxAuthCodes) &&
40
+ options.maxAuthCodes > 0
41
+ ? Math.floor(options.maxAuthCodes)
42
+ : undefined;
34
43
  }
35
44
  async getClient(clientId) {
36
45
  return cloneClient(this.clients.get(clientId));
@@ -47,6 +56,7 @@ export class MemoryOAuthStore extends OAuthStore {
47
56
  firstParty: input.firstParty
48
57
  };
49
58
  this.clients.set(stored.clientId, stored);
59
+ this.enforceClientCapacity();
50
60
  return cloneClient(stored);
51
61
  }
52
62
  async verifyClientSecret(clientId, secret) {
@@ -65,22 +75,51 @@ export class MemoryOAuthStore extends OAuthStore {
65
75
  async createAuthCode(code) {
66
76
  const record = {
67
77
  ...code,
68
- userId: normalizeUserId(code.userId),
78
+ userId: normalizeComparableUserId(code.userId),
69
79
  scope: code.scope ? [...code.scope] : undefined,
70
80
  expiresAt: code.expiresAt,
71
81
  metadata: code.metadata ? { ...code.metadata } : undefined
72
82
  };
73
83
  this.codes.set(record.code, record);
84
+ this.enforceCodeCapacity();
74
85
  }
75
86
  async consumeAuthCode(code) {
76
87
  const record = this.codes.get(code);
77
88
  if (!record) {
78
89
  return null;
79
90
  }
91
+ if (record.expiresAt.getTime() <= Date.now()) {
92
+ this.codes.delete(code);
93
+ return null;
94
+ }
80
95
  this.codes.delete(code);
81
96
  return cloneCode(record);
82
97
  }
83
98
  async close() {
84
99
  return;
85
100
  }
101
+ enforceClientCapacity() {
102
+ if (!this.maxClients) {
103
+ return;
104
+ }
105
+ while (this.clients.size > this.maxClients) {
106
+ const oldest = this.clients.keys().next().value;
107
+ if (!oldest) {
108
+ return;
109
+ }
110
+ this.clients.delete(oldest);
111
+ }
112
+ }
113
+ enforceCodeCapacity() {
114
+ if (!this.maxAuthCodes) {
115
+ return;
116
+ }
117
+ while (this.codes.size > this.maxAuthCodes) {
118
+ const oldest = this.codes.keys().next().value;
119
+ if (!oldest) {
120
+ return;
121
+ }
122
+ this.codes.delete(oldest);
123
+ }
124
+ }
86
125
  }
@@ -1,4 +1,5 @@
1
1
  import { Model, type Optional, type Sequelize } from 'sequelize';
2
+ export { integerIdType, tableOptions } from '../sequelize-utils.js';
2
3
  export interface OAuthClientAttributes {
3
4
  client_id: string;
4
5
  client_secret: string;
@@ -1,19 +1,6 @@
1
1
  import { DataTypes, Model } from 'sequelize';
2
- import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
3
- function integerIdType(sequelize) {
4
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
5
- }
6
- 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
+ import { integerIdType, tableOptions } from '../sequelize-utils.js';
3
+ export { integerIdType, tableOptions } from '../sequelize-utils.js';
17
4
  export class OAuthClientModel extends Model {
18
5
  }
19
6
  export function initOAuthClientModel(sequelize, options = {}) {
@@ -1,66 +1,19 @@
1
- import { Model, type Optional, type Sequelize } from 'sequelize';
2
1
  import { OAuthStore, type AuthCode, type OAuthClient } from './base.js';
3
- export interface OAuthClientAttributes {
4
- client_id: string;
5
- client_secret: string;
6
- name: string | null;
7
- redirect_uris: string;
8
- scope: string;
9
- metadata: string | null;
10
- first_party: boolean;
11
- }
12
- export type OAuthClientCreationAttributes = Optional<OAuthClientAttributes, 'client_secret' | 'name' | 'scope' | 'metadata' | 'first_party'>;
13
- export declare class OAuthClientModel extends Model<OAuthClientAttributes, OAuthClientCreationAttributes> implements OAuthClientAttributes {
14
- client_id: string;
15
- client_secret: string;
16
- name: string | null;
17
- redirect_uris: string;
18
- scope: string;
19
- metadata: string | null;
20
- first_party: boolean;
21
- }
22
- export declare function initOAuthClientModel(sequelize: Sequelize, options?: {
23
- tablePrefix?: string;
24
- }): typeof OAuthClientModel;
25
- export interface OAuthCodeAttributes {
26
- code: string;
27
- client_id: string;
28
- user_id: number;
29
- redirect_uri: string;
30
- scope: string;
31
- code_challenge: string | null;
32
- code_challenge_method: 'plain' | 'S256' | null;
33
- expires: Date;
34
- metadata: string | null;
35
- }
36
- export type OAuthCodeCreationAttributes = Optional<OAuthCodeAttributes, 'code_challenge' | 'code_challenge_method' | 'metadata'>;
37
- export declare class OAuthCodeModel extends Model<OAuthCodeAttributes, OAuthCodeCreationAttributes> implements OAuthCodeAttributes {
38
- code: string;
39
- client_id: string;
40
- user_id: number;
41
- redirect_uri: string;
42
- scope: string;
43
- code_challenge: string | null;
44
- code_challenge_method: 'plain' | 'S256' | null;
45
- expires: Date;
46
- metadata: string | null;
47
- }
48
- export declare function initOAuthCodeModel(sequelize: Sequelize, options?: {
49
- tablePrefix?: string;
50
- }): typeof OAuthCodeModel;
2
+ import { OAuthClientModel, OAuthCodeModel } from './models.js';
51
3
  export interface SequelizeOAuthStoreOptions {
52
- sequelize: Sequelize;
4
+ sequelize: import('sequelize').Sequelize;
53
5
  tablePrefix?: string;
54
6
  clientModel?: typeof OAuthClientModel;
55
7
  codeModel?: typeof OAuthCodeModel;
56
- clientModelFactory?: (sequelize: Sequelize, options?: {
8
+ clientModelFactory?: (sequelize: import('sequelize').Sequelize, options?: {
57
9
  tablePrefix?: string;
58
10
  }) => typeof OAuthClientModel;
59
- codeModelFactory?: (sequelize: Sequelize, options?: {
11
+ codeModelFactory?: (sequelize: import('sequelize').Sequelize, options?: {
60
12
  tablePrefix?: string;
61
13
  }) => typeof OAuthCodeModel;
62
14
  bcryptRounds?: number;
63
15
  }
16
+ export { OAuthClientModel, OAuthCodeModel, initOAuthClientModel, initOAuthCodeModel, type OAuthClientAttributes, type OAuthClientCreationAttributes, type OAuthCodeAttributes, type OAuthCodeCreationAttributes } from './models.js';
64
17
  export declare class SequelizeOAuthStore extends OAuthStore {
65
18
  private readonly clients;
66
19
  private readonly codes;
@@ -1,74 +1,10 @@
1
1
  import bcrypt from 'bcryptjs';
2
- import { DataTypes, Model } from 'sequelize';
2
+ import { Transaction } from 'sequelize';
3
3
  import { normalizeNumericUserId } from '../auth-api/user-id.js';
4
- import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
4
+ import { decodeStringArray, encodeStringArray } from '../sequelize-utils.js';
5
5
  import { OAuthStore } from './base.js';
6
- function integerIdType(sequelize) {
7
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
8
- }
9
- function tableOptions(sequelize, tableName, tablePrefix, extra) {
10
- const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
11
- if (extra) {
12
- Object.assign(opts, extra);
13
- }
14
- if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
15
- opts.charset = 'utf8mb4';
16
- opts.collate = 'utf8mb4_unicode_ci';
17
- }
18
- return opts;
19
- }
20
- export class OAuthClientModel extends Model {
21
- }
22
- export function initOAuthClientModel(sequelize, options = {}) {
23
- OAuthClientModel.init({
24
- client_id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
25
- client_secret: { type: DataTypes.STRING(255), allowNull: false, defaultValue: '' },
26
- name: { type: DataTypes.STRING(128), allowNull: true, defaultValue: null },
27
- redirect_uris: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
28
- scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
29
- metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null },
30
- first_party: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
31
- }, tableOptions(sequelize, 'oauth_clients', options.tablePrefix, { timestamps: false }));
32
- return OAuthClientModel;
33
- }
34
- export class OAuthCodeModel extends Model {
35
- }
36
- export function initOAuthCodeModel(sequelize, options = {}) {
37
- const idType = integerIdType(sequelize);
38
- OAuthCodeModel.init({
39
- code: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
40
- client_id: { type: DataTypes.STRING(128), allowNull: false },
41
- user_id: { type: idType, allowNull: false },
42
- redirect_uri: { type: DataTypes.TEXT, allowNull: false },
43
- scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
44
- code_challenge: { type: DataTypes.STRING(255), allowNull: true, defaultValue: null },
45
- code_challenge_method: { type: DataTypes.STRING(10), allowNull: true, defaultValue: null },
46
- expires: { type: DataTypes.DATE, allowNull: false },
47
- metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null }
48
- }, tableOptions(sequelize, 'oauth_codes', options.tablePrefix, { timestamps: false }));
49
- return OAuthCodeModel;
50
- }
51
- function encodeStringArray(values) {
52
- return JSON.stringify(values ?? []);
53
- }
54
- function decodeStringArray(raw) {
55
- if (!raw) {
56
- return [];
57
- }
58
- try {
59
- const parsed = JSON.parse(raw);
60
- if (Array.isArray(parsed)) {
61
- return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
62
- }
63
- }
64
- catch {
65
- // ignore malformed values
66
- }
67
- return raw
68
- .split(/\s+/)
69
- .map((entry) => entry.trim())
70
- .filter((entry) => entry.length > 0);
71
- }
6
+ import { initOAuthClientModel, initOAuthCodeModel } from './models.js';
7
+ export { OAuthClientModel, OAuthCodeModel, initOAuthClientModel, initOAuthCodeModel } from './models.js';
72
8
  function serializeMetadata(metadata) {
73
9
  if (!metadata) {
74
10
  return null;
@@ -90,9 +26,6 @@ function parseMetadata(raw) {
90
26
  }
91
27
  return undefined;
92
28
  }
93
- function normalizeUserId(identifier) {
94
- return normalizeNumericUserId(identifier);
95
- }
96
29
  export class SequelizeOAuthStore extends OAuthStore {
97
30
  constructor(options) {
98
31
  super();
@@ -155,7 +88,7 @@ export class SequelizeOAuthStore extends OAuthStore {
155
88
  await this.codes.create({
156
89
  code: code.code,
157
90
  client_id: code.clientId,
158
- user_id: normalizeUserId(code.userId),
91
+ user_id: normalizeNumericUserId(code.userId),
159
92
  redirect_uri: code.redirectUri ?? '',
160
93
  scope: encodeStringArray(code.scope),
161
94
  code_challenge: code.codeChallenge ?? null,
@@ -165,12 +98,21 @@ export class SequelizeOAuthStore extends OAuthStore {
165
98
  });
166
99
  }
167
100
  async consumeAuthCode(code) {
168
- const model = await this.codes.findByPk(code);
169
- if (!model) {
170
- return null;
101
+ const sequelize = this.codes.sequelize;
102
+ if (!sequelize) {
103
+ throw new Error('Code model is not bound to a Sequelize instance');
171
104
  }
172
- await model.destroy();
173
- return this.toAuthCode(model);
105
+ return sequelize.transaction({ isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }, async (transaction) => {
106
+ const model = await this.codes.findByPk(code, { transaction, lock: true });
107
+ if (!model) {
108
+ return null;
109
+ }
110
+ await model.destroy({ transaction });
111
+ if (model.expires.getTime() <= Date.now()) {
112
+ return null;
113
+ }
114
+ return this.toAuthCode(model);
115
+ });
174
116
  }
175
117
  async close() {
176
118
  return;
@@ -178,8 +120,7 @@ export class SequelizeOAuthStore extends OAuthStore {
178
120
  toOAuthClient(model) {
179
121
  return {
180
122
  clientId: model.client_id,
181
- // client_secret is stored hashed; do not return the hash.
182
- clientSecret: model.client_secret ? '__stored__' : undefined,
123
+ hasSecret: Boolean(model.client_secret),
183
124
  name: model.name ?? undefined,
184
125
  redirectUris: decodeStringArray(model.redirect_uris),
185
126
  scope: decodeStringArray(model.scope),
@@ -191,7 +132,7 @@ export class SequelizeOAuthStore extends OAuthStore {
191
132
  return {
192
133
  code: model.code,
193
134
  clientId: model.client_id,
194
- userId: model.user_id,
135
+ userId: String(model.user_id),
195
136
  redirectUri: model.redirect_uri,
196
137
  scope: decodeStringArray(model.scope),
197
138
  codeChallenge: model.code_challenge ?? undefined,
@@ -2,6 +2,7 @@ import type { AuthIdentifier } from '../auth-api/types.js';
2
2
  export interface OAuthClient {
3
3
  clientId: string;
4
4
  clientSecret?: string;
5
+ hasSecret?: boolean;
5
6
  firstParty?: boolean;
6
7
  metadata?: Record<string, unknown>;
7
8
  name?: string;
@@ -11,6 +11,7 @@ export declare abstract class PasskeyStore implements PasskeyStorageAdapter {
11
11
  abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
12
12
  abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
13
13
  abstract saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
14
+ abstract getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
14
15
  abstract consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
15
16
  abstract cleanupChallenges(now: Date): Promise<void>;
16
17
  }
@@ -6,11 +6,15 @@ export interface MemoryPasskeyStoreOptions {
6
6
  userId?: AuthIdentifier;
7
7
  login?: string;
8
8
  }) => Promise<PasskeyUserDescriptor | null>;
9
+ maxCredentials?: number;
10
+ maxChallenges?: number;
9
11
  }
10
12
  export declare class MemoryPasskeyStore extends PasskeyStore {
11
13
  private readonly resolveUserFn;
12
14
  private readonly credentials;
13
15
  private readonly challenges;
16
+ private readonly maxCredentials?;
17
+ private readonly maxChallenges?;
14
18
  constructor(options: MemoryPasskeyStoreOptions);
15
19
  resolveUser(params: {
16
20
  userId?: AuthIdentifier;
@@ -22,6 +26,9 @@ export declare class MemoryPasskeyStore extends PasskeyStore {
22
26
  saveCredential(record: StoredPasskeyCredential): Promise<void>;
23
27
  updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
24
28
  saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
29
+ getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
25
30
  consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
26
31
  cleanupChallenges(now: Date): Promise<void>;
32
+ private enforceCredentialCapacity;
33
+ private enforceChallengeCapacity;
27
34
  }
@@ -3,11 +3,11 @@ import { PasskeyStore } from './base.js';
3
3
  function encodeCredentialId(value) {
4
4
  return Buffer.isBuffer(value) ? value.toString('base64') : value;
5
5
  }
6
- const normalizeUserId = normalizeComparableUserId;
7
6
  function cloneCredential(record) {
8
7
  return {
9
8
  ...record,
10
9
  credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
10
+ publicKey: Buffer.from(record.publicKey),
11
11
  transports: record.transports ? [...record.transports] : undefined
12
12
  };
13
13
  }
@@ -17,14 +17,26 @@ export class MemoryPasskeyStore extends PasskeyStore {
17
17
  this.credentials = new Map();
18
18
  this.challenges = new Map();
19
19
  this.resolveUserFn = options.resolveUser;
20
+ this.maxCredentials =
21
+ typeof options.maxCredentials === 'number' &&
22
+ Number.isFinite(options.maxCredentials) &&
23
+ options.maxCredentials > 0
24
+ ? Math.floor(options.maxCredentials)
25
+ : undefined;
26
+ this.maxChallenges =
27
+ typeof options.maxChallenges === 'number' &&
28
+ Number.isFinite(options.maxChallenges) &&
29
+ options.maxChallenges > 0
30
+ ? Math.floor(options.maxChallenges)
31
+ : undefined;
20
32
  }
21
33
  async resolveUser(params) {
22
34
  return this.resolveUserFn(params);
23
35
  }
24
36
  async listUserCredentials(userId) {
25
- const normalizedUserId = normalizeUserId(userId);
37
+ const normalizedUserId = normalizeComparableUserId(userId);
26
38
  return [...this.credentials.values()]
27
- .filter((record) => normalizeUserId(record.userId) === normalizedUserId)
39
+ .filter((record) => normalizeComparableUserId(record.userId) === normalizedUserId)
28
40
  .map((record) => cloneCredential(record));
29
41
  }
30
42
  async deleteCredential(credentialId) {
@@ -38,10 +50,11 @@ export class MemoryPasskeyStore extends PasskeyStore {
38
50
  async saveCredential(record) {
39
51
  this.credentials.set(encodeCredentialId(record.credentialId), {
40
52
  ...record,
41
- userId: normalizeUserId(record.userId),
53
+ userId: normalizeComparableUserId(record.userId),
42
54
  credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
43
55
  transports: record.transports ? [...record.transports] : undefined
44
56
  });
57
+ this.enforceCredentialCapacity();
45
58
  }
46
59
  async updateCredentialCounter(credentialId, counter) {
47
60
  const key = encodeCredentialId(credentialId);
@@ -54,10 +67,15 @@ export class MemoryPasskeyStore extends PasskeyStore {
54
67
  this.challenges.set(record.challenge, {
55
68
  challenge: record.challenge,
56
69
  action: record.action,
57
- userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
70
+ userId: record.userId !== undefined ? normalizeComparableUserId(record.userId) : undefined,
58
71
  login: record.login ?? undefined,
59
72
  expiresAt: record.expiresAt
60
73
  });
74
+ this.enforceChallengeCapacity();
75
+ }
76
+ async getChallenge(challenge) {
77
+ const record = this.challenges.get(challenge);
78
+ return record ? { ...record } : null;
61
79
  }
62
80
  async consumeChallenge(challenge) {
63
81
  const record = this.challenges.get(challenge);
@@ -74,4 +92,28 @@ export class MemoryPasskeyStore extends PasskeyStore {
74
92
  }
75
93
  }
76
94
  }
95
+ enforceCredentialCapacity() {
96
+ if (!this.maxCredentials) {
97
+ return;
98
+ }
99
+ while (this.credentials.size > this.maxCredentials) {
100
+ const oldest = this.credentials.keys().next().value;
101
+ if (!oldest) {
102
+ return;
103
+ }
104
+ this.credentials.delete(oldest);
105
+ }
106
+ }
107
+ enforceChallengeCapacity() {
108
+ if (!this.maxChallenges) {
109
+ return;
110
+ }
111
+ while (this.challenges.size > this.maxChallenges) {
112
+ const oldest = this.challenges.keys().next().value;
113
+ if (!oldest) {
114
+ return;
115
+ }
116
+ this.challenges.delete(oldest);
117
+ }
118
+ }
77
119
  }
@@ -1,8 +1,5 @@
1
1
  import { DataTypes, Model } from 'sequelize';
2
- import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
3
- function integerIdType(sequelize) {
4
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
5
- }
2
+ import { applyTablePrefix, integerIdType } from '../sequelize-utils.js';
6
3
  export class PasskeyCredentialModel extends Model {
7
4
  }
8
5
  export class PasskeyChallengeModel extends Model {
@@ -1,34 +1,8 @@
1
- import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
1
+ import { type ModelStatic, type Sequelize } from 'sequelize';
2
2
  import { PasskeyStore } from './base.js';
3
+ import { PasskeyChallengeModel, PasskeyCredentialModel } from './models.js';
3
4
  import type { PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
4
5
  import type { AuthIdentifier } from '../auth-api/types.js';
5
- declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
6
- credentialId: Buffer;
7
- userId: number;
8
- publicKey: Buffer;
9
- counter: number;
10
- transports: string[] | null;
11
- backedUp: boolean;
12
- deviceType: string;
13
- label: string | null;
14
- createdDomain: string | null;
15
- createdUserAgent: string | null;
16
- createdBrowser: string | null;
17
- createdOs: string | null;
18
- createdDevice: string | null;
19
- createdIp: string | null;
20
- createdAt?: Date;
21
- updatedAt?: Date;
22
- }
23
- declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallengeModel>, InferCreationAttributes<PasskeyChallengeModel>> {
24
- challenge: string;
25
- action: 'register' | 'authenticate';
26
- userId: number | null;
27
- login: string | null;
28
- expiresAt: Date;
29
- createdAt?: Date;
30
- updatedAt?: Date;
31
- }
32
6
  export interface SequelizePasskeyStoreOptions {
33
7
  sequelize: Sequelize;
34
8
  tablePrefix?: string;
@@ -60,7 +34,9 @@ export declare class SequelizePasskeyStore extends PasskeyStore {
60
34
  saveCredential(record: StoredPasskeyCredential): Promise<void>;
61
35
  updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
62
36
  saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
37
+ getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
63
38
  consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
64
39
  cleanupChallenges(now: Date): Promise<void>;
40
+ private toChallengeRecord;
41
+ private toStoredCredential;
65
42
  }
66
- export {};