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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.txt +81 -28
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +7 -4
  4. package/dist/cjs/api-server-base.cjs +607 -99
  5. package/dist/cjs/api-server-base.d.ts +80 -23
  6. package/dist/cjs/auth-api/auth-module.d.ts +23 -3
  7. package/dist/cjs/auth-api/auth-module.js +320 -124
  8. package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
  9. package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
  10. package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
  11. package/dist/cjs/auth-api/mem-auth-store.js +14 -28
  12. package/dist/cjs/auth-api/module.d.ts +1 -1
  13. package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
  14. package/dist/cjs/auth-api/sql-auth-store.js +43 -30
  15. package/dist/cjs/auth-api/storage.d.ts +6 -4
  16. package/dist/cjs/auth-api/storage.js +15 -5
  17. package/dist/cjs/auth-api/types.d.ts +7 -2
  18. package/dist/cjs/auth-api/user-id.d.ts +5 -0
  19. package/dist/cjs/auth-api/user-id.js +38 -0
  20. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  21. package/dist/cjs/auth-cookie-options.js +66 -0
  22. package/dist/cjs/index.cjs +4 -14
  23. package/dist/cjs/index.d.ts +4 -9
  24. package/dist/cjs/oauth/memory.d.ts +6 -0
  25. package/dist/cjs/oauth/memory.js +44 -11
  26. package/dist/cjs/oauth/models.d.ts +7 -2
  27. package/dist/cjs/oauth/models.js +10 -21
  28. package/dist/cjs/oauth/sequelize.d.ts +10 -48
  29. package/dist/cjs/oauth/sequelize.js +44 -99
  30. package/dist/cjs/oauth/types.d.ts +1 -0
  31. package/dist/cjs/passkey/base.d.ts +2 -0
  32. package/dist/cjs/passkey/config.d.ts +2 -0
  33. package/dist/cjs/passkey/config.js +26 -0
  34. package/dist/cjs/passkey/memory.d.ts +8 -0
  35. package/dist/cjs/passkey/memory.js +57 -16
  36. package/dist/cjs/passkey/models.d.ts +13 -4
  37. package/dist/cjs/passkey/models.js +41 -14
  38. package/dist/cjs/passkey/sequelize.d.ts +13 -25
  39. package/dist/cjs/passkey/sequelize.js +68 -153
  40. package/dist/cjs/passkey/service.d.ts +6 -2
  41. package/dist/cjs/passkey/service.js +205 -27
  42. package/dist/cjs/passkey/types.d.ts +18 -9
  43. package/dist/cjs/sequelize-utils.d.ts +8 -0
  44. package/dist/cjs/sequelize-utils.js +57 -0
  45. package/dist/cjs/token/base.d.ts +2 -1
  46. package/dist/cjs/token/base.js +3 -1
  47. package/dist/cjs/token/memory.d.ts +10 -0
  48. package/dist/cjs/token/memory.js +122 -32
  49. package/dist/cjs/token/sequelize.d.ts +4 -4
  50. package/dist/cjs/token/sequelize.js +67 -85
  51. package/dist/cjs/token/types.d.ts +8 -1
  52. package/dist/cjs/user/base.d.ts +1 -0
  53. package/dist/cjs/user/base.js +11 -4
  54. package/dist/cjs/user/memory.d.ts +2 -0
  55. package/dist/cjs/user/memory.js +9 -10
  56. package/dist/cjs/user/sequelize.d.ts +7 -2
  57. package/dist/cjs/user/sequelize.js +19 -32
  58. package/dist/esm/api-module.d.ts +7 -4
  59. package/dist/esm/api-module.js +9 -0
  60. package/dist/esm/api-server-base.d.ts +80 -23
  61. package/dist/esm/api-server-base.js +608 -100
  62. package/dist/esm/auth-api/auth-module.d.ts +23 -3
  63. package/dist/esm/auth-api/auth-module.js +321 -125
  64. package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
  65. package/dist/esm/auth-api/compat-auth-storage.js +13 -1
  66. package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
  67. package/dist/esm/auth-api/mem-auth-store.js +14 -28
  68. package/dist/esm/auth-api/module.d.ts +1 -1
  69. package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
  70. package/dist/esm/auth-api/sql-auth-store.js +43 -30
  71. package/dist/esm/auth-api/storage.d.ts +6 -4
  72. package/dist/esm/auth-api/storage.js +13 -3
  73. package/dist/esm/auth-api/types.d.ts +7 -2
  74. package/dist/esm/auth-api/user-id.d.ts +5 -0
  75. package/dist/esm/auth-api/user-id.js +32 -0
  76. package/dist/esm/auth-cookie-options.d.ts +11 -0
  77. package/dist/esm/auth-cookie-options.js +63 -0
  78. package/dist/esm/index.d.ts +4 -9
  79. package/dist/esm/index.js +2 -7
  80. package/dist/esm/oauth/memory.d.ts +6 -0
  81. package/dist/esm/oauth/memory.js +44 -11
  82. package/dist/esm/oauth/models.d.ts +7 -2
  83. package/dist/esm/oauth/models.js +6 -19
  84. package/dist/esm/oauth/sequelize.d.ts +10 -48
  85. package/dist/esm/oauth/sequelize.js +32 -87
  86. package/dist/esm/oauth/types.d.ts +1 -0
  87. package/dist/esm/passkey/base.d.ts +2 -0
  88. package/dist/esm/passkey/config.d.ts +2 -0
  89. package/dist/esm/passkey/config.js +23 -0
  90. package/dist/esm/passkey/memory.d.ts +8 -0
  91. package/dist/esm/passkey/memory.js +57 -16
  92. package/dist/esm/passkey/models.d.ts +13 -4
  93. package/dist/esm/passkey/models.js +39 -12
  94. package/dist/esm/passkey/sequelize.d.ts +13 -25
  95. package/dist/esm/passkey/sequelize.js +69 -154
  96. package/dist/esm/passkey/service.d.ts +6 -2
  97. package/dist/esm/passkey/service.js +173 -28
  98. package/dist/esm/passkey/types.d.ts +18 -9
  99. package/dist/esm/sequelize-utils.d.ts +8 -0
  100. package/dist/esm/sequelize-utils.js +48 -0
  101. package/dist/esm/token/base.d.ts +2 -1
  102. package/dist/esm/token/base.js +3 -1
  103. package/dist/esm/token/memory.d.ts +10 -0
  104. package/dist/esm/token/memory.js +122 -32
  105. package/dist/esm/token/sequelize.d.ts +4 -4
  106. package/dist/esm/token/sequelize.js +67 -85
  107. package/dist/esm/token/types.d.ts +8 -1
  108. package/dist/esm/user/base.d.ts +1 -0
  109. package/dist/esm/user/base.js +11 -4
  110. package/dist/esm/user/memory.d.ts +2 -0
  111. package/dist/esm/user/memory.js +9 -10
  112. package/dist/esm/user/sequelize.d.ts +7 -2
  113. package/dist/esm/user/sequelize.js +19 -32
  114. package/docs/swagger/openapi.json +1876 -0
  115. package/package.json +84 -34
package/dist/esm/index.js CHANGED
@@ -1,22 +1,17 @@
1
1
  export { default as ApiServer } from './api-server-base.js';
2
2
  export { ApiError } from './api-server-base.js';
3
3
  export { ApiModule } from './api-module.js';
4
- export { nullAuthStorage, BaseAuthStorage } from './auth-api/storage.js';
4
+ export { nullAuthAdapter, BaseAuthAdapter } from './auth-api/storage.js';
5
5
  export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
6
- export { AuthStorageAdapter } from './auth-api/compat-auth-storage.js';
6
+ export { CompositeAuthAdapter } from './auth-api/compat-auth-storage.js';
7
7
  export { MemAuthStore } from './auth-api/mem-auth-store.js';
8
- export { SqlAuthStore } from './auth-api/sql-auth-store.js';
9
8
  export { default as AuthModule } from './auth-api/auth-module.js';
10
9
  export { UserStore } from './user/base.js';
11
10
  export { MemoryUserStore } from './user/memory.js';
12
- export { SequelizeUserStore } from './user/sequelize.js';
13
11
  export { TokenStore } from './token/base.js';
14
12
  export { MemoryTokenStore } from './token/memory.js';
15
- export { SequelizeTokenStore } from './token/sequelize.js';
16
13
  export { PasskeyService } from './passkey/service.js';
17
14
  export { PasskeyStore } from './passkey/base.js';
18
15
  export { MemoryPasskeyStore } from './passkey/memory.js';
19
- export { SequelizePasskeyStore } from './passkey/sequelize.js';
20
16
  export { OAuthStore } from './oauth/base.js';
21
17
  export { MemoryOAuthStore } from './oauth/memory.js';
22
- export { SequelizeOAuthStore } from './oauth/sequelize.js';
@@ -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,4 +1,5 @@
1
1
  import bcrypt from 'bcryptjs';
2
+ import { normalizeComparableUserId } from '../auth-api/user-id.js';
2
3
  import { OAuthStore } from './base.js';
3
4
  function cloneClient(client) {
4
5
  if (!client) {
@@ -6,7 +7,7 @@ function cloneClient(client) {
6
7
  }
7
8
  return {
8
9
  clientId: client.clientId,
9
- clientSecret: client.clientSecret,
10
+ hasSecret: Boolean(client.clientSecret),
10
11
  name: client.name,
11
12
  redirectUris: [...client.redirectUris],
12
13
  scope: client.scope ? [...client.scope] : undefined,
@@ -17,26 +18,28 @@ function cloneClient(client) {
17
18
  function cloneCode(code) {
18
19
  return {
19
20
  ...code,
21
+ userId: normalizeComparableUserId(code.userId),
20
22
  scope: code.scope ? [...code.scope] : undefined,
21
23
  expiresAt: new Date(code.expiresAt),
22
24
  metadata: code.metadata ? { ...code.metadata } : undefined
23
25
  };
24
26
  }
25
- function normalizeUserId(identifier) {
26
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
27
- return identifier;
28
- }
29
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
30
- return Number(identifier);
31
- }
32
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
33
- }
34
27
  export class MemoryOAuthStore extends OAuthStore {
35
28
  constructor(options = {}) {
36
29
  super();
37
30
  this.clients = new Map();
38
31
  this.codes = new Map();
39
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;
40
43
  }
41
44
  async getClient(clientId) {
42
45
  return cloneClient(this.clients.get(clientId));
@@ -53,6 +56,7 @@ export class MemoryOAuthStore extends OAuthStore {
53
56
  firstParty: input.firstParty
54
57
  };
55
58
  this.clients.set(stored.clientId, stored);
59
+ this.enforceClientCapacity();
56
60
  return cloneClient(stored);
57
61
  }
58
62
  async verifyClientSecret(clientId, secret) {
@@ -71,22 +75,51 @@ export class MemoryOAuthStore extends OAuthStore {
71
75
  async createAuthCode(code) {
72
76
  const record = {
73
77
  ...code,
74
- userId: normalizeUserId(code.userId),
78
+ userId: normalizeComparableUserId(code.userId),
75
79
  scope: code.scope ? [...code.scope] : undefined,
76
80
  expiresAt: code.expiresAt,
77
81
  metadata: code.metadata ? { ...code.metadata } : undefined
78
82
  };
79
83
  this.codes.set(record.code, record);
84
+ this.enforceCodeCapacity();
80
85
  }
81
86
  async consumeAuthCode(code) {
82
87
  const record = this.codes.get(code);
83
88
  if (!record) {
84
89
  return null;
85
90
  }
91
+ if (record.expiresAt.getTime() <= Date.now()) {
92
+ this.codes.delete(code);
93
+ return null;
94
+ }
86
95
  this.codes.delete(code);
87
96
  return cloneCode(record);
88
97
  }
89
98
  async close() {
90
99
  return;
91
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
+ }
92
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;
@@ -18,7 +19,9 @@ export declare class OAuthClientModel extends Model<OAuthClientAttributes, OAuth
18
19
  metadata: string | null;
19
20
  first_party: boolean;
20
21
  }
21
- export declare function initOAuthClientModel(sequelize: Sequelize): typeof OAuthClientModel;
22
+ export declare function initOAuthClientModel(sequelize: Sequelize, options?: {
23
+ tablePrefix?: string;
24
+ }): typeof OAuthClientModel;
22
25
  export interface OAuthCodeAttributes {
23
26
  code: string;
24
27
  client_id: string;
@@ -42,4 +45,6 @@ export declare class OAuthCodeModel extends Model<OAuthCodeAttributes, OAuthCode
42
45
  expires: Date;
43
46
  metadata: string | null;
44
47
  }
45
- export declare function initOAuthCodeModel(sequelize: Sequelize): typeof OAuthCodeModel;
48
+ export declare function initOAuthCodeModel(sequelize: Sequelize, options?: {
49
+ tablePrefix?: string;
50
+ }): typeof OAuthCodeModel;
@@ -1,22 +1,9 @@
1
1
  import { DataTypes, Model } from 'sequelize';
2
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
3
- function integerIdType(sequelize) {
4
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
5
- }
6
- function tableOptions(sequelize, tableName, extra) {
7
- const opts = { sequelize, 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
- export function initOAuthClientModel(sequelize) {
6
+ export function initOAuthClientModel(sequelize, options = {}) {
20
7
  OAuthClientModel.init({
21
8
  client_id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
22
9
  client_secret: { type: DataTypes.STRING(255), allowNull: false, defaultValue: '' },
@@ -26,13 +13,13 @@ export function initOAuthClientModel(sequelize) {
26
13
  metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null },
27
14
  first_party: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
28
15
  }, {
29
- ...tableOptions(sequelize, 'oauth_clients', { timestamps: false })
16
+ ...tableOptions(sequelize, 'oauth_clients', options.tablePrefix, { timestamps: false })
30
17
  });
31
18
  return OAuthClientModel;
32
19
  }
33
20
  export class OAuthCodeModel extends Model {
34
21
  }
35
- export function initOAuthCodeModel(sequelize) {
22
+ export function initOAuthCodeModel(sequelize, options = {}) {
36
23
  const idType = integerIdType(sequelize);
37
24
  OAuthCodeModel.init({
38
25
  code: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
@@ -45,7 +32,7 @@ export function initOAuthCodeModel(sequelize) {
45
32
  expires: { type: DataTypes.DATE, allowNull: false },
46
33
  metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null }
47
34
  }, {
48
- ...tableOptions(sequelize, 'oauth_codes', { timestamps: false })
35
+ ...tableOptions(sequelize, 'oauth_codes', options.tablePrefix, { timestamps: false })
49
36
  });
50
37
  return OAuthCodeModel;
51
38
  }
@@ -1,57 +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): typeof OAuthClientModel;
23
- export interface OAuthCodeAttributes {
24
- code: string;
25
- client_id: string;
26
- user_id: number;
27
- redirect_uri: string;
28
- scope: string;
29
- code_challenge: string | null;
30
- code_challenge_method: 'plain' | 'S256' | null;
31
- expires: Date;
32
- metadata: string | null;
33
- }
34
- export type OAuthCodeCreationAttributes = Optional<OAuthCodeAttributes, 'code_challenge' | 'code_challenge_method' | 'metadata'>;
35
- export declare class OAuthCodeModel extends Model<OAuthCodeAttributes, OAuthCodeCreationAttributes> implements OAuthCodeAttributes {
36
- code: string;
37
- client_id: string;
38
- user_id: number;
39
- redirect_uri: string;
40
- scope: string;
41
- code_challenge: string | null;
42
- code_challenge_method: 'plain' | 'S256' | null;
43
- expires: Date;
44
- metadata: string | null;
45
- }
46
- export declare function initOAuthCodeModel(sequelize: Sequelize): typeof OAuthCodeModel;
2
+ import { OAuthClientModel, OAuthCodeModel } from './models.js';
47
3
  export interface SequelizeOAuthStoreOptions {
48
- sequelize: Sequelize;
4
+ sequelize: import('sequelize').Sequelize;
5
+ tablePrefix?: string;
49
6
  clientModel?: typeof OAuthClientModel;
50
7
  codeModel?: typeof OAuthCodeModel;
51
- clientModelFactory?: (sequelize: Sequelize) => typeof OAuthClientModel;
52
- codeModelFactory?: (sequelize: Sequelize) => typeof OAuthCodeModel;
8
+ clientModelFactory?: (sequelize: import('sequelize').Sequelize, options?: {
9
+ tablePrefix?: string;
10
+ }) => typeof OAuthClientModel;
11
+ codeModelFactory?: (sequelize: import('sequelize').Sequelize, options?: {
12
+ tablePrefix?: string;
13
+ }) => typeof OAuthCodeModel;
53
14
  bcryptRounds?: number;
54
15
  }
16
+ export { OAuthClientModel, OAuthCodeModel, initOAuthClientModel, initOAuthCodeModel, type OAuthClientAttributes, type OAuthClientCreationAttributes, type OAuthCodeAttributes, type OAuthCodeCreationAttributes } from './models.js';
55
17
  export declare class SequelizeOAuthStore extends OAuthStore {
56
18
  private readonly clients;
57
19
  private readonly codes;
@@ -1,73 +1,10 @@
1
1
  import bcrypt from 'bcryptjs';
2
- import { DataTypes, Model } from 'sequelize';
2
+ import { Transaction } from 'sequelize';
3
+ import { normalizeNumericUserId } from '../auth-api/user-id.js';
4
+ import { decodeStringArray, encodeStringArray } from '../sequelize-utils.js';
3
5
  import { OAuthStore } from './base.js';
4
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
5
- function integerIdType(sequelize) {
6
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
7
- }
8
- function tableOptions(sequelize, tableName, extra) {
9
- const opts = { sequelize, tableName };
10
- if (extra) {
11
- Object.assign(opts, extra);
12
- }
13
- if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
14
- opts.charset = 'utf8mb4';
15
- opts.collate = 'utf8mb4_unicode_ci';
16
- }
17
- return opts;
18
- }
19
- export class OAuthClientModel extends Model {
20
- }
21
- export function initOAuthClientModel(sequelize) {
22
- OAuthClientModel.init({
23
- client_id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
24
- client_secret: { type: DataTypes.STRING(255), allowNull: false, defaultValue: '' },
25
- name: { type: DataTypes.STRING(128), allowNull: true, defaultValue: null },
26
- redirect_uris: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
27
- scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
28
- metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null },
29
- first_party: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
30
- }, tableOptions(sequelize, 'oauth_clients', { timestamps: false }));
31
- return OAuthClientModel;
32
- }
33
- export class OAuthCodeModel extends Model {
34
- }
35
- export function initOAuthCodeModel(sequelize) {
36
- const idType = integerIdType(sequelize);
37
- OAuthCodeModel.init({
38
- code: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
39
- client_id: { type: DataTypes.STRING(128), allowNull: false },
40
- user_id: { type: idType, allowNull: false },
41
- redirect_uri: { type: DataTypes.TEXT, allowNull: false },
42
- scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
43
- code_challenge: { type: DataTypes.STRING(255), allowNull: true, defaultValue: null },
44
- code_challenge_method: { type: DataTypes.STRING(10), allowNull: true, defaultValue: null },
45
- expires: { type: DataTypes.DATE, allowNull: false },
46
- metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null }
47
- }, tableOptions(sequelize, 'oauth_codes', { timestamps: false }));
48
- return OAuthCodeModel;
49
- }
50
- function encodeStringArray(values) {
51
- return JSON.stringify(values ?? []);
52
- }
53
- function decodeStringArray(raw) {
54
- if (!raw) {
55
- return [];
56
- }
57
- try {
58
- const parsed = JSON.parse(raw);
59
- if (Array.isArray(parsed)) {
60
- return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
61
- }
62
- }
63
- catch {
64
- // ignore malformed values
65
- }
66
- return raw
67
- .split(/\s+/)
68
- .map((entry) => entry.trim())
69
- .filter((entry) => entry.length > 0);
70
- }
6
+ import { initOAuthClientModel, initOAuthCodeModel } from './models.js';
7
+ export { OAuthClientModel, OAuthCodeModel, initOAuthClientModel, initOAuthCodeModel } from './models.js';
71
8
  function serializeMetadata(metadata) {
72
9
  if (!metadata) {
73
10
  return null;
@@ -89,23 +26,22 @@ function parseMetadata(raw) {
89
26
  }
90
27
  return undefined;
91
28
  }
92
- function normalizeUserId(identifier) {
93
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
94
- return identifier;
95
- }
96
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
97
- return Number(identifier);
98
- }
99
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
100
- }
101
29
  export class SequelizeOAuthStore extends OAuthStore {
102
30
  constructor(options) {
103
31
  super();
104
32
  if (!options?.sequelize) {
105
33
  throw new Error('SequelizeOAuthStore requires an initialised Sequelize instance');
106
34
  }
107
- this.clients = options.clientModel ?? (options.clientModelFactory ?? initOAuthClientModel)(options.sequelize);
108
- this.codes = options.codeModel ?? (options.codeModelFactory ?? initOAuthCodeModel)(options.sequelize);
35
+ this.clients =
36
+ options.clientModel ??
37
+ (options.clientModelFactory ?? initOAuthClientModel)(options.sequelize, {
38
+ tablePrefix: options.tablePrefix
39
+ });
40
+ this.codes =
41
+ options.codeModel ??
42
+ (options.codeModelFactory ?? initOAuthCodeModel)(options.sequelize, {
43
+ tablePrefix: options.tablePrefix
44
+ });
109
45
  this.bcryptRounds = options.bcryptRounds ?? 12;
110
46
  }
111
47
  async getClient(clientId) {
@@ -152,7 +88,7 @@ export class SequelizeOAuthStore extends OAuthStore {
152
88
  await this.codes.create({
153
89
  code: code.code,
154
90
  client_id: code.clientId,
155
- user_id: normalizeUserId(code.userId),
91
+ user_id: normalizeNumericUserId(code.userId),
156
92
  redirect_uri: code.redirectUri ?? '',
157
93
  scope: encodeStringArray(code.scope),
158
94
  code_challenge: code.codeChallenge ?? null,
@@ -162,12 +98,21 @@ export class SequelizeOAuthStore extends OAuthStore {
162
98
  });
163
99
  }
164
100
  async consumeAuthCode(code) {
165
- const model = await this.codes.findByPk(code);
166
- if (!model) {
167
- return null;
101
+ const sequelize = this.codes.sequelize;
102
+ if (!sequelize) {
103
+ throw new Error('Code model is not bound to a Sequelize instance');
168
104
  }
169
- await model.destroy();
170
- 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
+ });
171
116
  }
172
117
  async close() {
173
118
  return;
@@ -175,7 +120,7 @@ export class SequelizeOAuthStore extends OAuthStore {
175
120
  toOAuthClient(model) {
176
121
  return {
177
122
  clientId: model.client_id,
178
- clientSecret: model.client_secret,
123
+ hasSecret: Boolean(model.client_secret),
179
124
  name: model.name ?? undefined,
180
125
  redirectUris: decodeStringArray(model.redirect_uris),
181
126
  scope: decodeStringArray(model.scope),
@@ -187,7 +132,7 @@ export class SequelizeOAuthStore extends OAuthStore {
187
132
  return {
188
133
  code: model.code,
189
134
  clientId: model.client_id,
190
- userId: model.user_id,
135
+ userId: String(model.user_id),
191
136
  redirectUri: model.redirect_uri,
192
137
  scope: decodeStringArray(model.scope),
193
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;
@@ -6,10 +6,12 @@ export declare abstract class PasskeyStore implements PasskeyStorageAdapter {
6
6
  login?: string;
7
7
  }): Promise<PasskeyUserDescriptor | null>;
8
8
  abstract listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
9
+ abstract deleteCredential(credentialId: Buffer | string): Promise<boolean>;
9
10
  abstract findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
10
11
  abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
11
12
  abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
12
13
  abstract saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
14
+ abstract getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
13
15
  abstract consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
14
16
  abstract cleanupChallenges(now: Date): Promise<void>;
15
17
  }
@@ -0,0 +1,2 @@
1
+ import type { PasskeyServiceConfig } from './types.js';
2
+ export declare function normalizePasskeyConfig(config?: Partial<PasskeyServiceConfig>): PasskeyServiceConfig;
@@ -0,0 +1,23 @@
1
+ const DEFAULT_PASSKEY_CONFIG = {
2
+ rpId: 'localhost',
3
+ rpName: 'API Server',
4
+ origins: ['http://localhost:5173'],
5
+ timeoutMs: 5 * 60 * 1000,
6
+ userVerification: 'preferred'
7
+ };
8
+ function isOriginString(origin) {
9
+ return typeof origin === 'string' && origin.trim().length > 0;
10
+ }
11
+ export function normalizePasskeyConfig(config = {}) {
12
+ const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
13
+ return {
14
+ rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
15
+ rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
16
+ origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
17
+ timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
18
+ ? config.timeoutMs
19
+ : DEFAULT_PASSKEY_CONFIG.timeoutMs,
20
+ userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification,
21
+ debug: Boolean(config.debug)
22
+ };
23
+ }
@@ -6,21 +6,29 @@ 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;
17
21
  login?: string;
18
22
  }): Promise<PasskeyUserDescriptor | null>;
19
23
  listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
24
+ deleteCredential(credentialId: Buffer | string): Promise<boolean>;
20
25
  findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
21
26
  saveCredential(record: StoredPasskeyCredential): Promise<void>;
22
27
  updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
23
28
  saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
29
+ getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
24
30
  consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
25
31
  cleanupChallenges(now: Date): Promise<void>;
32
+ private enforceCredentialCapacity;
33
+ private enforceChallengeCapacity;
26
34
  }