@technomoron/api-server-base 2.0.0-beta.2 → 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 (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 +81 -32
@@ -1,20 +1,13 @@
1
+ import { normalizeComparableUserId } from '../auth-api/user-id.js';
1
2
  import { PasskeyStore } from './base.js';
2
3
  function encodeCredentialId(value) {
3
4
  return Buffer.isBuffer(value) ? value.toString('base64') : value;
4
5
  }
5
- function normalizeUserId(identifier) {
6
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
7
- return identifier;
8
- }
9
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
10
- return Number(identifier);
11
- }
12
- return identifier;
13
- }
14
6
  function cloneCredential(record) {
15
7
  return {
16
8
  ...record,
17
9
  credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
10
+ publicKey: Buffer.from(record.publicKey),
18
11
  transports: record.transports ? [...record.transports] : undefined
19
12
  };
20
13
  }
@@ -24,16 +17,32 @@ export class MemoryPasskeyStore extends PasskeyStore {
24
17
  this.credentials = new Map();
25
18
  this.challenges = new Map();
26
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;
27
32
  }
28
33
  async resolveUser(params) {
29
34
  return this.resolveUserFn(params);
30
35
  }
31
36
  async listUserCredentials(userId) {
32
- const normalizedUserId = normalizeUserId(userId);
37
+ const normalizedUserId = normalizeComparableUserId(userId);
33
38
  return [...this.credentials.values()]
34
- .filter((record) => normalizeUserId(record.userId) === normalizedUserId)
39
+ .filter((record) => normalizeComparableUserId(record.userId) === normalizedUserId)
35
40
  .map((record) => cloneCredential(record));
36
41
  }
42
+ async deleteCredential(credentialId) {
43
+ const key = encodeCredentialId(credentialId);
44
+ return this.credentials.delete(key);
45
+ }
37
46
  async findCredentialById(credentialId) {
38
47
  const record = this.credentials.get(encodeCredentialId(credentialId));
39
48
  return record ? cloneCredential(record) : null;
@@ -41,10 +50,11 @@ export class MemoryPasskeyStore extends PasskeyStore {
41
50
  async saveCredential(record) {
42
51
  this.credentials.set(encodeCredentialId(record.credentialId), {
43
52
  ...record,
44
- userId: normalizeUserId(record.userId),
53
+ userId: normalizeComparableUserId(record.userId),
45
54
  credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
46
55
  transports: record.transports ? [...record.transports] : undefined
47
56
  });
57
+ this.enforceCredentialCapacity();
48
58
  }
49
59
  async updateCredentialCounter(credentialId, counter) {
50
60
  const key = encodeCredentialId(credentialId);
@@ -55,10 +65,17 @@ export class MemoryPasskeyStore extends PasskeyStore {
55
65
  }
56
66
  async saveChallenge(record) {
57
67
  this.challenges.set(record.challenge, {
58
- ...record,
59
- userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
60
- metadata: record.metadata ? { ...record.metadata } : {}
68
+ challenge: record.challenge,
69
+ action: record.action,
70
+ userId: record.userId !== undefined ? normalizeComparableUserId(record.userId) : undefined,
71
+ login: record.login ?? undefined,
72
+ expiresAt: record.expiresAt
61
73
  });
74
+ this.enforceChallengeCapacity();
75
+ }
76
+ async getChallenge(challenge) {
77
+ const record = this.challenges.get(challenge);
78
+ return record ? { ...record } : null;
62
79
  }
63
80
  async consumeChallenge(challenge) {
64
81
  const record = this.challenges.get(challenge);
@@ -66,7 +83,7 @@ export class MemoryPasskeyStore extends PasskeyStore {
66
83
  return null;
67
84
  }
68
85
  this.challenges.delete(challenge);
69
- return { ...record, metadata: record.metadata ? { ...record.metadata } : {} };
86
+ return { ...record };
70
87
  }
71
88
  async cleanupChallenges(now) {
72
89
  for (const [challenge, record] of this.challenges.entries()) {
@@ -75,4 +92,28 @@ export class MemoryPasskeyStore extends PasskeyStore {
75
92
  }
76
93
  }
77
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
+ }
78
119
  }
@@ -1,5 +1,4 @@
1
1
  import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
2
- import type { PasskeyChallengeMetadata } from './service.js';
3
2
  export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
4
3
  credentialId: Buffer;
5
4
  userId: number;
@@ -8,6 +7,13 @@ export declare class PasskeyCredentialModel extends Model<InferAttributes<Passke
8
7
  transports: string[] | null;
9
8
  backedUp: boolean;
10
9
  deviceType: string;
10
+ label: string | null;
11
+ createdDomain: string | null;
12
+ createdUserAgent: string | null;
13
+ createdBrowser: string | null;
14
+ createdOs: string | null;
15
+ createdDevice: string | null;
16
+ createdIp: string | null;
11
17
  createdAt?: Date;
12
18
  updatedAt?: Date;
13
19
  }
@@ -16,10 +22,13 @@ export declare class PasskeyChallengeModel extends Model<InferAttributes<Passkey
16
22
  action: 'register' | 'authenticate';
17
23
  userId: number | null;
18
24
  login: string | null;
19
- metadata: PasskeyChallengeMetadata | null;
20
25
  expiresAt: Date;
21
26
  createdAt?: Date;
22
27
  updatedAt?: Date;
23
28
  }
24
- export declare function initPasskeyCredentialModel(sequelize: Sequelize): ModelStatic<PasskeyCredentialModel>;
25
- export declare function initPasskeyChallengeModel(sequelize: Sequelize): ModelStatic<PasskeyChallengeModel>;
29
+ export declare function initPasskeyCredentialModel(sequelize: Sequelize, options?: {
30
+ tablePrefix?: string;
31
+ }): ModelStatic<PasskeyCredentialModel>;
32
+ export declare function initPasskeyChallengeModel(sequelize: Sequelize, options?: {
33
+ tablePrefix?: string;
34
+ }): ModelStatic<PasskeyChallengeModel>;
@@ -1,13 +1,10 @@
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
- }
2
+ import { applyTablePrefix, integerIdType } from '../sequelize-utils.js';
6
3
  export class PasskeyCredentialModel extends Model {
7
4
  }
8
5
  export class PasskeyChallengeModel extends Model {
9
6
  }
10
- export function initPasskeyCredentialModel(sequelize) {
7
+ export function initPasskeyCredentialModel(sequelize, options = {}) {
11
8
  const idType = integerIdType(sequelize);
12
9
  return PasskeyCredentialModel.init({
13
10
  credentialId: {
@@ -60,15 +57,49 @@ export function initPasskeyCredentialModel(sequelize) {
60
57
  type: DataTypes.STRING(32),
61
58
  allowNull: false,
62
59
  defaultValue: 'multiDevice'
60
+ },
61
+ label: {
62
+ type: DataTypes.STRING(120),
63
+ allowNull: true
64
+ },
65
+ createdDomain: {
66
+ field: 'created_domain',
67
+ type: DataTypes.STRING(255),
68
+ allowNull: true
69
+ },
70
+ createdUserAgent: {
71
+ field: 'created_user_agent',
72
+ type: DataTypes.TEXT,
73
+ allowNull: true
74
+ },
75
+ createdBrowser: {
76
+ field: 'created_browser',
77
+ type: DataTypes.STRING(120),
78
+ allowNull: true
79
+ },
80
+ createdOs: {
81
+ field: 'created_os',
82
+ type: DataTypes.STRING(120),
83
+ allowNull: true
84
+ },
85
+ createdDevice: {
86
+ field: 'created_device',
87
+ type: DataTypes.STRING(120),
88
+ allowNull: true
89
+ },
90
+ createdIp: {
91
+ field: 'created_ip',
92
+ type: DataTypes.STRING(45),
93
+ allowNull: true
63
94
  }
64
95
  }, {
65
96
  sequelize,
66
- tableName: 'passkey_credentials',
97
+ tableName: applyTablePrefix(options.tablePrefix, 'passkey_credentials'),
67
98
  timestamps: true,
68
99
  underscored: true
69
100
  });
70
101
  }
71
- export function initPasskeyChallengeModel(sequelize) {
102
+ export function initPasskeyChallengeModel(sequelize, options = {}) {
72
103
  const idType = integerIdType(sequelize);
73
104
  return PasskeyChallengeModel.init({
74
105
  challenge: {
@@ -89,10 +120,6 @@ export function initPasskeyChallengeModel(sequelize) {
89
120
  type: DataTypes.STRING(128),
90
121
  allowNull: true
91
122
  },
92
- metadata: {
93
- type: DataTypes.JSON,
94
- allowNull: true
95
- },
96
123
  expiresAt: {
97
124
  field: 'expires_at',
98
125
  type: DataTypes.DATE,
@@ -100,7 +127,7 @@ export function initPasskeyChallengeModel(sequelize) {
100
127
  }
101
128
  }, {
102
129
  sequelize,
103
- tableName: 'passkey_challenges',
130
+ tableName: applyTablePrefix(options.tablePrefix, 'passkey_challenges'),
104
131
  timestamps: true,
105
132
  underscored: true,
106
133
  indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
@@ -1,34 +1,19 @@
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
- createdAt?: Date;
14
- updatedAt?: Date;
15
- }
16
- declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallengeModel>, InferCreationAttributes<PasskeyChallengeModel>> {
17
- challenge: string;
18
- action: 'register' | 'authenticate';
19
- userId: number | null;
20
- login: string | null;
21
- metadata: Record<string, unknown> | null;
22
- expiresAt: Date;
23
- createdAt?: Date;
24
- updatedAt?: Date;
25
- }
26
6
  export interface SequelizePasskeyStoreOptions {
27
7
  sequelize: Sequelize;
8
+ tablePrefix?: string;
28
9
  credentialModel?: ModelStatic<PasskeyCredentialModel>;
29
10
  challengeModel?: ModelStatic<PasskeyChallengeModel>;
30
- credentialModelFactory?: (sequelize: Sequelize) => ModelStatic<PasskeyCredentialModel>;
31
- challengeModelFactory?: (sequelize: Sequelize) => ModelStatic<PasskeyChallengeModel>;
11
+ credentialModelFactory?: (sequelize: Sequelize, options?: {
12
+ tablePrefix?: string;
13
+ }) => ModelStatic<PasskeyCredentialModel>;
14
+ challengeModelFactory?: (sequelize: Sequelize, options?: {
15
+ tablePrefix?: string;
16
+ }) => ModelStatic<PasskeyChallengeModel>;
32
17
  resolveUser: (params: {
33
18
  userId?: AuthIdentifier;
34
19
  login?: string;
@@ -44,11 +29,14 @@ export declare class SequelizePasskeyStore extends PasskeyStore {
44
29
  login?: string;
45
30
  }): Promise<PasskeyUserDescriptor | null>;
46
31
  listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
32
+ deleteCredential(credentialId: Buffer | string): Promise<boolean>;
47
33
  findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
48
34
  saveCredential(record: StoredPasskeyCredential): Promise<void>;
49
35
  updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
50
36
  saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
37
+ getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
51
38
  consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
52
39
  cleanupChallenges(now: Date): Promise<void>;
40
+ private toChallengeRecord;
41
+ private toStoredCredential;
53
42
  }
54
- export {};
@@ -1,124 +1,10 @@
1
- import { DataTypes, Model, Op } from 'sequelize';
1
+ import { Op, Transaction } from 'sequelize';
2
+ import { normalizeNumericUserId } from '../auth-api/user-id.js';
2
3
  import { PasskeyStore } from './base.js';
3
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
4
- function integerIdType(sequelize) {
5
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
6
- }
4
+ import { initPasskeyChallengeModel, initPasskeyCredentialModel } from './models.js';
7
5
  function encodeCredentialId(value) {
8
6
  return Buffer.isBuffer(value) ? value.toString('base64') : value;
9
7
  }
10
- function normalizeUserId(identifier) {
11
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
12
- return identifier;
13
- }
14
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
15
- return Number(identifier);
16
- }
17
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
18
- }
19
- class PasskeyCredentialModel extends Model {
20
- }
21
- class PasskeyChallengeModel extends Model {
22
- }
23
- function initPasskeyCredentialModel(sequelize) {
24
- const idType = integerIdType(sequelize);
25
- return PasskeyCredentialModel.init({
26
- credentialId: {
27
- field: 'credential_id',
28
- type: DataTypes.STRING(768),
29
- primaryKey: true,
30
- allowNull: false,
31
- get() {
32
- const raw = this.getDataValue('credentialId');
33
- if (!raw) {
34
- return raw;
35
- }
36
- if (Buffer.isBuffer(raw)) {
37
- return raw;
38
- }
39
- return Buffer.from(raw, 'base64');
40
- },
41
- set(value) {
42
- const encoded = typeof value === 'string' ? value : value.toString('base64');
43
- this.setDataValue('credentialId', encoded);
44
- }
45
- },
46
- userId: {
47
- field: 'user_id',
48
- type: idType,
49
- allowNull: false
50
- },
51
- publicKey: {
52
- field: 'public_key',
53
- type: DataTypes.BLOB,
54
- allowNull: false
55
- },
56
- counter: {
57
- type: DataTypes.INTEGER,
58
- allowNull: false,
59
- defaultValue: 0
60
- },
61
- transports: {
62
- type: DataTypes.JSON,
63
- allowNull: true
64
- },
65
- backedUp: {
66
- field: 'backed_up',
67
- type: DataTypes.BOOLEAN,
68
- allowNull: false,
69
- defaultValue: false
70
- },
71
- deviceType: {
72
- field: 'device_type',
73
- type: DataTypes.STRING(32),
74
- allowNull: false,
75
- defaultValue: 'multiDevice'
76
- }
77
- }, {
78
- sequelize,
79
- tableName: 'passkey_credentials',
80
- timestamps: true,
81
- underscored: true
82
- });
83
- }
84
- function initPasskeyChallengeModel(sequelize) {
85
- const idType = integerIdType(sequelize);
86
- return PasskeyChallengeModel.init({
87
- challenge: {
88
- type: DataTypes.STRING(255),
89
- primaryKey: true,
90
- allowNull: false
91
- },
92
- action: {
93
- type: DataTypes.STRING(16),
94
- allowNull: false
95
- },
96
- userId: {
97
- field: 'user_id',
98
- type: idType,
99
- allowNull: true
100
- },
101
- login: {
102
- type: DataTypes.STRING(128),
103
- allowNull: true
104
- },
105
- metadata: {
106
- type: DataTypes.JSON,
107
- allowNull: true
108
- },
109
- expiresAt: {
110
- field: 'expires_at',
111
- type: DataTypes.DATE,
112
- allowNull: false
113
- }
114
- }, {
115
- sequelize,
116
- tableName: 'passkey_challenges',
117
- timestamps: true,
118
- underscored: true,
119
- indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
120
- });
121
- }
122
8
  export class SequelizePasskeyStore extends PasskeyStore {
123
9
  constructor(options) {
124
10
  super();
@@ -128,49 +14,47 @@ export class SequelizePasskeyStore extends PasskeyStore {
128
14
  this.resolveUserFn = options.resolveUser;
129
15
  this.credentials =
130
16
  options.credentialModel ??
131
- (options.credentialModelFactory ?? initPasskeyCredentialModel)(options.sequelize);
17
+ (options.credentialModelFactory ?? initPasskeyCredentialModel)(options.sequelize, {
18
+ tablePrefix: options.tablePrefix
19
+ });
132
20
  this.challenges =
133
- options.challengeModel ?? (options.challengeModelFactory ?? initPasskeyChallengeModel)(options.sequelize);
21
+ options.challengeModel ??
22
+ (options.challengeModelFactory ?? initPasskeyChallengeModel)(options.sequelize, {
23
+ tablePrefix: options.tablePrefix
24
+ });
134
25
  }
135
26
  async resolveUser(params) {
136
27
  return this.resolveUserFn(params);
137
28
  }
138
29
  async listUserCredentials(userId) {
139
- const models = await this.credentials.findAll({ where: { userId: normalizeUserId(userId) } });
140
- return models.map((model) => ({
141
- userId: model.userId,
142
- credentialId: model.credentialId,
143
- publicKey: model.publicKey,
144
- counter: model.counter,
145
- transports: (model.transports ?? undefined),
146
- backedUp: model.backedUp,
147
- deviceType: model.deviceType
148
- }));
30
+ const models = await this.credentials.findAll({ where: { userId: normalizeNumericUserId(userId) } });
31
+ return models.map((model) => this.toStoredCredential(model));
32
+ }
33
+ async deleteCredential(credentialId) {
34
+ const encoded = Buffer.isBuffer(credentialId) ? credentialId.toString('base64') : credentialId;
35
+ const deleted = await this.credentials.destroy({ where: { credentialId: encoded } });
36
+ return deleted > 0;
149
37
  }
150
38
  async findCredentialById(credentialId) {
151
39
  const model = await this.credentials.findByPk(encodeCredentialId(credentialId));
152
- if (!model) {
153
- return null;
154
- }
155
- return {
156
- userId: model.userId,
157
- credentialId: model.credentialId,
158
- publicKey: model.publicKey,
159
- counter: model.counter,
160
- transports: (model.transports ?? undefined),
161
- backedUp: model.backedUp,
162
- deviceType: model.deviceType
163
- };
40
+ return model ? this.toStoredCredential(model) : null;
164
41
  }
165
42
  async saveCredential(record) {
166
43
  await this.credentials.upsert({
167
44
  credentialId: record.credentialId,
168
- userId: normalizeUserId(record.userId),
45
+ userId: normalizeNumericUserId(record.userId),
169
46
  publicKey: record.publicKey,
170
47
  counter: record.counter,
171
48
  transports: record.transports ?? null,
172
49
  backedUp: record.backedUp,
173
- deviceType: record.deviceType
50
+ deviceType: record.deviceType,
51
+ label: record.label ?? null,
52
+ createdDomain: record.createdDomain ?? null,
53
+ createdUserAgent: record.createdUserAgent ?? null,
54
+ createdBrowser: record.createdBrowser ?? null,
55
+ createdOs: record.createdOs ?? null,
56
+ createdDevice: record.createdDevice ?? null,
57
+ createdIp: record.createdIp ?? null
174
58
  });
175
59
  }
176
60
  async updateCredentialCounter(credentialId, counter) {
@@ -180,28 +64,59 @@ export class SequelizePasskeyStore extends PasskeyStore {
180
64
  await this.challenges.upsert({
181
65
  challenge: record.challenge,
182
66
  action: record.action,
183
- userId: record.userId !== undefined ? normalizeUserId(record.userId) : null,
67
+ userId: record.userId !== undefined ? normalizeNumericUserId(record.userId) : null,
184
68
  login: record.login ?? null,
185
- metadata: (record.metadata ?? {}),
186
69
  expiresAt: record.expiresAt
187
70
  });
188
71
  }
189
- async consumeChallenge(challenge) {
72
+ async getChallenge(challenge) {
190
73
  const model = await this.challenges.findByPk(challenge);
191
- if (!model) {
192
- return null;
74
+ return model ? this.toChallengeRecord(model) : null;
75
+ }
76
+ async consumeChallenge(challenge) {
77
+ const sequelize = this.challenges.sequelize;
78
+ if (!sequelize) {
79
+ throw new Error('Challenge model is not bound to a Sequelize instance');
193
80
  }
194
- await model.destroy();
81
+ return sequelize.transaction({ isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }, async (transaction) => {
82
+ const model = await this.challenges.findByPk(challenge, { transaction, lock: true });
83
+ if (!model) {
84
+ return null;
85
+ }
86
+ await model.destroy({ transaction });
87
+ return this.toChallengeRecord(model);
88
+ });
89
+ }
90
+ async cleanupChallenges(now) {
91
+ await this.challenges.destroy({ where: { expiresAt: { [Op.lte]: now } } });
92
+ }
93
+ toChallengeRecord(model) {
195
94
  return {
196
95
  challenge: model.challenge,
197
96
  action: model.action,
198
- userId: model.userId ?? undefined,
97
+ userId: model.userId !== null ? String(model.userId) : undefined,
199
98
  login: model.login ?? undefined,
200
- expiresAt: model.expiresAt,
201
- metadata: model.metadata ?? {}
99
+ expiresAt: model.expiresAt
202
100
  };
203
101
  }
204
- async cleanupChallenges(now) {
205
- await this.challenges.destroy({ where: { expiresAt: { [Op.lte]: now } } });
102
+ toStoredCredential(model) {
103
+ return {
104
+ userId: String(model.userId),
105
+ credentialId: model.credentialId,
106
+ publicKey: model.publicKey,
107
+ counter: model.counter,
108
+ transports: (model.transports ?? undefined),
109
+ backedUp: model.backedUp,
110
+ deviceType: model.deviceType,
111
+ label: model.label ?? undefined,
112
+ createdDomain: model.createdDomain ?? undefined,
113
+ createdUserAgent: model.createdUserAgent ?? undefined,
114
+ createdBrowser: model.createdBrowser ?? undefined,
115
+ createdOs: model.createdOs ?? undefined,
116
+ createdDevice: model.createdDevice ?? undefined,
117
+ createdIp: model.createdIp ?? undefined,
118
+ createdAt: model.createdAt ?? undefined,
119
+ updatedAt: model.updatedAt ?? undefined
120
+ };
206
121
  }
207
122
  }
@@ -1,11 +1,14 @@
1
- import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig } from './types.js';
2
- export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyChallengeMetadata, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter } from './types.js';
1
+ import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, StoredPasskeyCredential } from './types.js';
2
+ import type { AuthIdentifier } from '../auth-api/types.js';
3
+ export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter, StoredPasskeyCredential } from './types.js';
3
4
  type Logger = Pick<typeof console, 'error' | 'warn'>;
4
5
  export declare class PasskeyService {
5
6
  private readonly config;
6
7
  private readonly adapter;
7
8
  private readonly logger;
8
9
  constructor(config: PasskeyServiceConfig, adapter: PasskeyStorageAdapter, logger?: Logger);
10
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
11
+ deleteCredential(credentialId: Buffer | string): Promise<boolean>;
9
12
  createChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
10
13
  verifyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
11
14
  private createRegistrationChallenge;
@@ -14,4 +17,5 @@ export declare class PasskeyService {
14
17
  private verifyAuthentication;
15
18
  private requireUser;
16
19
  private createExpiry;
20
+ private requireUserVerification;
17
21
  }