@technomoron/api-server-base 1.1.13 → 2.0.0-beta.10

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 (116) hide show
  1. package/README.txt +25 -2
  2. package/dist/cjs/api-server-base.cjs +448 -111
  3. package/dist/cjs/api-server-base.d.ts +91 -34
  4. package/dist/cjs/auth-api/auth-module.d.ts +105 -0
  5. package/dist/cjs/auth-api/auth-module.js +1180 -0
  6. package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
  7. package/dist/cjs/auth-api/compat-auth-storage.js +128 -0
  8. package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
  9. package/dist/cjs/auth-api/mem-auth-store.js +141 -0
  10. package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
  11. package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
  12. package/dist/cjs/auth-api/sql-auth-store.d.ts +77 -0
  13. package/dist/cjs/auth-api/sql-auth-store.js +172 -0
  14. package/dist/cjs/auth-api/storage.d.ts +38 -0
  15. package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +17 -7
  16. package/dist/cjs/auth-api/types.d.ts +34 -0
  17. package/dist/cjs/auth-api/types.js +2 -0
  18. package/dist/cjs/index.cjs +41 -7
  19. package/dist/cjs/index.d.ts +29 -5
  20. package/dist/cjs/oauth/base.d.ts +10 -0
  21. package/dist/cjs/oauth/base.js +6 -0
  22. package/dist/cjs/oauth/memory.d.ts +16 -0
  23. package/dist/cjs/oauth/memory.js +99 -0
  24. package/dist/cjs/oauth/models.d.ts +45 -0
  25. package/dist/cjs/oauth/models.js +58 -0
  26. package/dist/cjs/oauth/sequelize.d.ts +68 -0
  27. package/dist/cjs/oauth/sequelize.js +210 -0
  28. package/dist/cjs/oauth/types.d.ts +50 -0
  29. package/dist/cjs/oauth/types.js +3 -0
  30. package/dist/cjs/passkey/base.d.ts +16 -0
  31. package/dist/cjs/passkey/base.js +6 -0
  32. package/dist/cjs/passkey/memory.d.ts +27 -0
  33. package/dist/cjs/passkey/memory.js +86 -0
  34. package/dist/cjs/passkey/models.d.ts +25 -0
  35. package/dist/cjs/passkey/models.js +115 -0
  36. package/dist/cjs/passkey/sequelize.d.ts +55 -0
  37. package/dist/cjs/passkey/sequelize.js +220 -0
  38. package/dist/cjs/passkey/service.d.ts +20 -0
  39. package/dist/cjs/passkey/service.js +356 -0
  40. package/dist/cjs/passkey/types.d.ts +78 -0
  41. package/dist/cjs/passkey/types.js +2 -0
  42. package/dist/cjs/token/base.d.ts +38 -0
  43. package/dist/cjs/token/base.js +114 -0
  44. package/dist/cjs/token/memory.d.ts +19 -0
  45. package/dist/cjs/token/memory.js +149 -0
  46. package/dist/cjs/token/sequelize.d.ts +58 -0
  47. package/dist/cjs/token/sequelize.js +404 -0
  48. package/dist/cjs/token/types.d.ts +27 -0
  49. package/dist/cjs/token/types.js +2 -0
  50. package/dist/cjs/user/base.d.ts +26 -0
  51. package/dist/cjs/user/base.js +45 -0
  52. package/dist/cjs/user/memory.d.ts +35 -0
  53. package/dist/cjs/user/memory.js +173 -0
  54. package/dist/cjs/user/sequelize.d.ts +41 -0
  55. package/dist/cjs/user/sequelize.js +182 -0
  56. package/dist/cjs/user/types.d.ts +11 -0
  57. package/dist/cjs/user/types.js +2 -0
  58. package/dist/esm/api-server-base.d.ts +91 -34
  59. package/dist/esm/api-server-base.js +447 -110
  60. package/dist/esm/auth-api/auth-module.d.ts +105 -0
  61. package/dist/esm/auth-api/auth-module.js +1178 -0
  62. package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
  63. package/dist/esm/auth-api/compat-auth-storage.js +124 -0
  64. package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
  65. package/dist/esm/auth-api/mem-auth-store.js +137 -0
  66. package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
  67. package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
  68. package/dist/esm/auth-api/sql-auth-store.d.ts +77 -0
  69. package/dist/esm/auth-api/sql-auth-store.js +168 -0
  70. package/dist/esm/auth-api/storage.d.ts +38 -0
  71. package/dist/esm/{auth-storage.js → auth-api/storage.js} +15 -5
  72. package/dist/esm/auth-api/types.d.ts +34 -0
  73. package/dist/esm/auth-api/types.js +1 -0
  74. package/dist/esm/index.d.ts +29 -5
  75. package/dist/esm/index.js +19 -2
  76. package/dist/esm/oauth/base.d.ts +10 -0
  77. package/dist/esm/oauth/base.js +2 -0
  78. package/dist/esm/oauth/memory.d.ts +16 -0
  79. package/dist/esm/oauth/memory.js +92 -0
  80. package/dist/esm/oauth/models.d.ts +45 -0
  81. package/dist/esm/oauth/models.js +51 -0
  82. package/dist/esm/oauth/sequelize.d.ts +68 -0
  83. package/dist/esm/oauth/sequelize.js +199 -0
  84. package/dist/esm/oauth/types.d.ts +50 -0
  85. package/dist/esm/oauth/types.js +2 -0
  86. package/dist/esm/passkey/base.d.ts +16 -0
  87. package/dist/esm/passkey/base.js +2 -0
  88. package/dist/esm/passkey/memory.d.ts +27 -0
  89. package/dist/esm/passkey/memory.js +82 -0
  90. package/dist/esm/passkey/models.d.ts +25 -0
  91. package/dist/esm/passkey/models.js +108 -0
  92. package/dist/esm/passkey/sequelize.d.ts +55 -0
  93. package/dist/esm/passkey/sequelize.js +216 -0
  94. package/dist/esm/passkey/service.d.ts +20 -0
  95. package/dist/esm/passkey/service.js +319 -0
  96. package/dist/esm/passkey/types.d.ts +78 -0
  97. package/dist/esm/passkey/types.js +1 -0
  98. package/dist/esm/token/base.d.ts +38 -0
  99. package/dist/esm/token/base.js +107 -0
  100. package/dist/esm/token/memory.d.ts +19 -0
  101. package/dist/esm/token/memory.js +145 -0
  102. package/dist/esm/token/sequelize.d.ts +58 -0
  103. package/dist/esm/token/sequelize.js +400 -0
  104. package/dist/esm/token/types.d.ts +27 -0
  105. package/dist/esm/token/types.js +1 -0
  106. package/dist/esm/user/base.d.ts +26 -0
  107. package/dist/esm/user/base.js +38 -0
  108. package/dist/esm/user/memory.d.ts +35 -0
  109. package/dist/esm/user/memory.js +169 -0
  110. package/dist/esm/user/sequelize.d.ts +41 -0
  111. package/dist/esm/user/sequelize.js +176 -0
  112. package/dist/esm/user/types.d.ts +11 -0
  113. package/dist/esm/user/types.js +1 -0
  114. package/package.json +13 -3
  115. package/dist/cjs/auth-storage.d.ts +0 -133
  116. package/dist/esm/auth-storage.d.ts +0 -133
@@ -0,0 +1,199 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import { DataTypes, Model } from 'sequelize';
3
+ 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
+ }
71
+ function serializeMetadata(metadata) {
72
+ if (!metadata) {
73
+ return null;
74
+ }
75
+ return JSON.stringify(metadata);
76
+ }
77
+ function parseMetadata(raw) {
78
+ if (!raw) {
79
+ return undefined;
80
+ }
81
+ try {
82
+ const parsed = JSON.parse(raw);
83
+ if (parsed && typeof parsed === 'object') {
84
+ return parsed;
85
+ }
86
+ }
87
+ catch {
88
+ // ignore
89
+ }
90
+ return undefined;
91
+ }
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
+ export class SequelizeOAuthStore extends OAuthStore {
102
+ constructor(options) {
103
+ super();
104
+ if (!options?.sequelize) {
105
+ throw new Error('SequelizeOAuthStore requires an initialised Sequelize instance');
106
+ }
107
+ this.clients = options.clientModel ?? (options.clientModelFactory ?? initOAuthClientModel)(options.sequelize);
108
+ this.codes = options.codeModel ?? (options.codeModelFactory ?? initOAuthCodeModel)(options.sequelize);
109
+ this.bcryptRounds = options.bcryptRounds ?? 12;
110
+ }
111
+ async getClient(clientId) {
112
+ const model = await this.clients.findByPk(clientId);
113
+ return model ? this.toOAuthClient(model) : null;
114
+ }
115
+ async createClient(input) {
116
+ const existing = await this.clients.findByPk(input.clientId);
117
+ const hashedSecret = input.clientSecret !== undefined && input.clientSecret !== null
118
+ ? await bcrypt.hash(input.clientSecret, this.bcryptRounds)
119
+ : (existing?.client_secret ?? '');
120
+ const redirectUris = input.redirectUris ?? (existing ? decodeStringArray(existing.redirect_uris) : undefined);
121
+ const scope = input.scope ?? (existing ? decodeStringArray(existing.scope) : undefined);
122
+ const metadata = input.metadata ?? (existing ? parseMetadata(existing.metadata) : undefined);
123
+ await this.clients.upsert({
124
+ client_id: input.clientId,
125
+ client_secret: hashedSecret,
126
+ name: input.name ?? existing?.name ?? null,
127
+ redirect_uris: encodeStringArray(redirectUris),
128
+ scope: encodeStringArray(scope),
129
+ metadata: serializeMetadata(metadata),
130
+ first_party: input.firstParty ?? existing?.first_party ?? false
131
+ });
132
+ const model = await this.clients.findByPk(input.clientId);
133
+ if (!model) {
134
+ throw new Error(`Unable to persist OAuth client ${input.clientId}`);
135
+ }
136
+ return this.toOAuthClient(model);
137
+ }
138
+ async verifyClientSecret(clientId, clientSecret) {
139
+ const model = await this.clients.findByPk(clientId);
140
+ if (!model) {
141
+ return false;
142
+ }
143
+ if (!model.client_secret) {
144
+ return !clientSecret || clientSecret.length === 0;
145
+ }
146
+ if (!clientSecret) {
147
+ return false;
148
+ }
149
+ return bcrypt.compare(clientSecret, model.client_secret);
150
+ }
151
+ async createAuthCode(code) {
152
+ await this.codes.create({
153
+ code: code.code,
154
+ client_id: code.clientId,
155
+ user_id: normalizeUserId(code.userId),
156
+ redirect_uri: code.redirectUri ?? '',
157
+ scope: encodeStringArray(code.scope),
158
+ code_challenge: code.codeChallenge ?? null,
159
+ code_challenge_method: code.codeChallengeMethod ?? null,
160
+ expires: code.expiresAt,
161
+ metadata: serializeMetadata(code.metadata)
162
+ });
163
+ }
164
+ async consumeAuthCode(code) {
165
+ const model = await this.codes.findByPk(code);
166
+ if (!model) {
167
+ return null;
168
+ }
169
+ await model.destroy();
170
+ return this.toAuthCode(model);
171
+ }
172
+ async close() {
173
+ return;
174
+ }
175
+ toOAuthClient(model) {
176
+ return {
177
+ clientId: model.client_id,
178
+ clientSecret: model.client_secret,
179
+ name: model.name ?? undefined,
180
+ redirectUris: decodeStringArray(model.redirect_uris),
181
+ scope: decodeStringArray(model.scope),
182
+ metadata: parseMetadata(model.metadata),
183
+ firstParty: model.first_party ?? false
184
+ };
185
+ }
186
+ toAuthCode(model) {
187
+ return {
188
+ code: model.code,
189
+ clientId: model.client_id,
190
+ userId: model.user_id,
191
+ redirectUri: model.redirect_uri,
192
+ scope: decodeStringArray(model.scope),
193
+ codeChallenge: model.code_challenge ?? undefined,
194
+ codeChallengeMethod: model.code_challenge_method ?? undefined,
195
+ expiresAt: model.expires,
196
+ metadata: parseMetadata(model.metadata)
197
+ };
198
+ }
199
+ }
@@ -0,0 +1,50 @@
1
+ import type { AuthIdentifier } from '../auth-api/types.js';
2
+ export interface OAuthClient {
3
+ clientId: string;
4
+ clientSecret?: string;
5
+ firstParty?: boolean;
6
+ metadata?: Record<string, unknown>;
7
+ name?: string;
8
+ redirectUris: string[];
9
+ scope?: string[];
10
+ }
11
+ export interface AuthCodeData {
12
+ code: string;
13
+ clientId: string;
14
+ codeChallenge?: string;
15
+ codeChallengeMethod?: 'plain' | 'S256';
16
+ expiresAt: Date;
17
+ metadata?: Record<string, unknown>;
18
+ redirectUri?: string;
19
+ scope?: string[];
20
+ userId: AuthIdentifier;
21
+ }
22
+ export type AuthCode = AuthCodeData;
23
+ export type AuthCodeRequest = Omit<AuthCodeData, 'code' | 'expiresAt'> & {
24
+ code?: string;
25
+ expiresInSeconds?: number;
26
+ };
27
+ export interface OAuthStartParams {
28
+ provider: string;
29
+ redirectUri?: string;
30
+ scope?: string | string[];
31
+ state?: string;
32
+ extras?: Record<string, unknown>;
33
+ }
34
+ export interface OAuthStartResult extends Record<string, unknown> {
35
+ url: string;
36
+ state?: string;
37
+ codeVerifier?: string;
38
+ }
39
+ export interface OAuthCallbackParams {
40
+ provider: string;
41
+ query: Record<string, string | string[]>;
42
+ body: Record<string, unknown>;
43
+ }
44
+ export interface OAuthCallbackResult<PublicUser> extends Record<string, unknown> {
45
+ user: PublicUser;
46
+ tokens?: {
47
+ accessToken: string;
48
+ refreshToken: string;
49
+ };
50
+ }
@@ -0,0 +1,2 @@
1
+ // OAuth-specific contracts kept alongside the OAuth module.
2
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { PasskeyChallengeRecord, PasskeyStorageAdapter, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
2
+ import type { AuthIdentifier } from '../auth-api/types.js';
3
+ export declare abstract class PasskeyStore implements PasskeyStorageAdapter {
4
+ abstract resolveUser(params: {
5
+ userId?: AuthIdentifier;
6
+ login?: string;
7
+ }): Promise<PasskeyUserDescriptor | null>;
8
+ abstract listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
9
+ abstract deleteCredential(credentialId: Buffer | string): Promise<boolean>;
10
+ abstract findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
11
+ abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
12
+ abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
13
+ abstract saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
14
+ abstract consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
15
+ abstract cleanupChallenges(now: Date): Promise<void>;
16
+ }
@@ -0,0 +1,2 @@
1
+ export class PasskeyStore {
2
+ }
@@ -0,0 +1,27 @@
1
+ import { PasskeyStore } from './base.js';
2
+ import type { PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
3
+ import type { AuthIdentifier } from '../auth-api/types.js';
4
+ export interface MemoryPasskeyStoreOptions {
5
+ resolveUser: (params: {
6
+ userId?: AuthIdentifier;
7
+ login?: string;
8
+ }) => Promise<PasskeyUserDescriptor | null>;
9
+ }
10
+ export declare class MemoryPasskeyStore extends PasskeyStore {
11
+ private readonly resolveUserFn;
12
+ private readonly credentials;
13
+ private readonly challenges;
14
+ constructor(options: MemoryPasskeyStoreOptions);
15
+ resolveUser(params: {
16
+ userId?: AuthIdentifier;
17
+ login?: string;
18
+ }): Promise<PasskeyUserDescriptor | null>;
19
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
20
+ deleteCredential(credentialId: Buffer | string): Promise<boolean>;
21
+ findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
22
+ saveCredential(record: StoredPasskeyCredential): Promise<void>;
23
+ updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
24
+ saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
25
+ consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
26
+ cleanupChallenges(now: Date): Promise<void>;
27
+ }
@@ -0,0 +1,82 @@
1
+ import { PasskeyStore } from './base.js';
2
+ function encodeCredentialId(value) {
3
+ return Buffer.isBuffer(value) ? value.toString('base64') : value;
4
+ }
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
+ function cloneCredential(record) {
15
+ return {
16
+ ...record,
17
+ credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
18
+ transports: record.transports ? [...record.transports] : undefined
19
+ };
20
+ }
21
+ export class MemoryPasskeyStore extends PasskeyStore {
22
+ constructor(options) {
23
+ super();
24
+ this.credentials = new Map();
25
+ this.challenges = new Map();
26
+ this.resolveUserFn = options.resolveUser;
27
+ }
28
+ async resolveUser(params) {
29
+ return this.resolveUserFn(params);
30
+ }
31
+ async listUserCredentials(userId) {
32
+ const normalizedUserId = normalizeUserId(userId);
33
+ return [...this.credentials.values()]
34
+ .filter((record) => normalizeUserId(record.userId) === normalizedUserId)
35
+ .map((record) => cloneCredential(record));
36
+ }
37
+ async deleteCredential(credentialId) {
38
+ const key = encodeCredentialId(credentialId);
39
+ return this.credentials.delete(key);
40
+ }
41
+ async findCredentialById(credentialId) {
42
+ const record = this.credentials.get(encodeCredentialId(credentialId));
43
+ return record ? cloneCredential(record) : null;
44
+ }
45
+ async saveCredential(record) {
46
+ this.credentials.set(encodeCredentialId(record.credentialId), {
47
+ ...record,
48
+ userId: normalizeUserId(record.userId),
49
+ credentialId: Buffer.isBuffer(record.credentialId) ? Buffer.from(record.credentialId) : record.credentialId,
50
+ transports: record.transports ? [...record.transports] : undefined
51
+ });
52
+ }
53
+ async updateCredentialCounter(credentialId, counter) {
54
+ const key = encodeCredentialId(credentialId);
55
+ const existing = this.credentials.get(key);
56
+ if (existing) {
57
+ existing.counter = counter;
58
+ }
59
+ }
60
+ async saveChallenge(record) {
61
+ this.challenges.set(record.challenge, {
62
+ ...record,
63
+ userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
64
+ metadata: record.metadata ? { ...record.metadata } : {}
65
+ });
66
+ }
67
+ async consumeChallenge(challenge) {
68
+ const record = this.challenges.get(challenge);
69
+ if (!record) {
70
+ return null;
71
+ }
72
+ this.challenges.delete(challenge);
73
+ return { ...record, metadata: record.metadata ? { ...record.metadata } : {} };
74
+ }
75
+ async cleanupChallenges(now) {
76
+ for (const [challenge, record] of this.challenges.entries()) {
77
+ if (record.expiresAt && new Date(record.expiresAt) <= now) {
78
+ this.challenges.delete(challenge);
79
+ }
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,25 @@
1
+ import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
2
+ import type { PasskeyChallengeMetadata } from './service.js';
3
+ export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
4
+ credentialId: Buffer;
5
+ userId: number;
6
+ publicKey: Buffer;
7
+ counter: number;
8
+ transports: string[] | null;
9
+ backedUp: boolean;
10
+ deviceType: string;
11
+ createdAt?: Date;
12
+ updatedAt?: Date;
13
+ }
14
+ export declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallengeModel>, InferCreationAttributes<PasskeyChallengeModel>> {
15
+ challenge: string;
16
+ action: 'register' | 'authenticate';
17
+ userId: number | null;
18
+ login: string | null;
19
+ metadata: PasskeyChallengeMetadata | null;
20
+ expiresAt: Date;
21
+ createdAt?: Date;
22
+ updatedAt?: Date;
23
+ }
24
+ export declare function initPasskeyCredentialModel(sequelize: Sequelize): ModelStatic<PasskeyCredentialModel>;
25
+ export declare function initPasskeyChallengeModel(sequelize: Sequelize): ModelStatic<PasskeyChallengeModel>;
@@ -0,0 +1,108 @@
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
+ export class PasskeyCredentialModel extends Model {
7
+ }
8
+ export class PasskeyChallengeModel extends Model {
9
+ }
10
+ export function initPasskeyCredentialModel(sequelize) {
11
+ const idType = integerIdType(sequelize);
12
+ return PasskeyCredentialModel.init({
13
+ credentialId: {
14
+ field: 'credential_id',
15
+ type: DataTypes.STRING(768),
16
+ primaryKey: true,
17
+ allowNull: false,
18
+ get() {
19
+ const raw = this.getDataValue('credentialId');
20
+ if (!raw) {
21
+ return raw;
22
+ }
23
+ if (Buffer.isBuffer(raw)) {
24
+ return raw;
25
+ }
26
+ return Buffer.from(raw, 'base64');
27
+ },
28
+ set(value) {
29
+ const encoded = typeof value === 'string' ? value : value.toString('base64');
30
+ this.setDataValue('credentialId', encoded);
31
+ }
32
+ },
33
+ userId: {
34
+ field: 'user_id',
35
+ type: idType,
36
+ allowNull: false
37
+ },
38
+ publicKey: {
39
+ field: 'public_key',
40
+ type: DataTypes.BLOB,
41
+ allowNull: false
42
+ },
43
+ counter: {
44
+ type: DataTypes.INTEGER,
45
+ allowNull: false,
46
+ defaultValue: 0
47
+ },
48
+ transports: {
49
+ type: DataTypes.JSON,
50
+ allowNull: true
51
+ },
52
+ backedUp: {
53
+ field: 'backed_up',
54
+ type: DataTypes.BOOLEAN,
55
+ allowNull: false,
56
+ defaultValue: false
57
+ },
58
+ deviceType: {
59
+ field: 'device_type',
60
+ type: DataTypes.STRING(32),
61
+ allowNull: false,
62
+ defaultValue: 'multiDevice'
63
+ }
64
+ }, {
65
+ sequelize,
66
+ tableName: 'passkey_credentials',
67
+ timestamps: true,
68
+ underscored: true
69
+ });
70
+ }
71
+ export function initPasskeyChallengeModel(sequelize) {
72
+ const idType = integerIdType(sequelize);
73
+ return PasskeyChallengeModel.init({
74
+ challenge: {
75
+ type: DataTypes.STRING(255),
76
+ primaryKey: true,
77
+ allowNull: false
78
+ },
79
+ action: {
80
+ type: DataTypes.STRING(16),
81
+ allowNull: false
82
+ },
83
+ userId: {
84
+ field: 'user_id',
85
+ type: idType,
86
+ allowNull: true
87
+ },
88
+ login: {
89
+ type: DataTypes.STRING(128),
90
+ allowNull: true
91
+ },
92
+ metadata: {
93
+ type: DataTypes.JSON,
94
+ allowNull: true
95
+ },
96
+ expiresAt: {
97
+ field: 'expires_at',
98
+ type: DataTypes.DATE,
99
+ allowNull: false
100
+ }
101
+ }, {
102
+ sequelize,
103
+ tableName: 'passkey_challenges',
104
+ timestamps: true,
105
+ underscored: true,
106
+ indexes: [{ fields: ['expires_at'] }, { fields: ['user_id'] }]
107
+ });
108
+ }
@@ -0,0 +1,55 @@
1
+ import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
2
+ import { PasskeyStore } from './base.js';
3
+ import type { PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
4
+ 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
+ export interface SequelizePasskeyStoreOptions {
27
+ sequelize: Sequelize;
28
+ credentialModel?: ModelStatic<PasskeyCredentialModel>;
29
+ challengeModel?: ModelStatic<PasskeyChallengeModel>;
30
+ credentialModelFactory?: (sequelize: Sequelize) => ModelStatic<PasskeyCredentialModel>;
31
+ challengeModelFactory?: (sequelize: Sequelize) => ModelStatic<PasskeyChallengeModel>;
32
+ resolveUser: (params: {
33
+ userId?: AuthIdentifier;
34
+ login?: string;
35
+ }) => Promise<PasskeyUserDescriptor | null>;
36
+ }
37
+ export declare class SequelizePasskeyStore extends PasskeyStore {
38
+ private readonly resolveUserFn;
39
+ private readonly credentials;
40
+ private readonly challenges;
41
+ constructor(options: SequelizePasskeyStoreOptions);
42
+ resolveUser(params: {
43
+ userId?: AuthIdentifier;
44
+ login?: string;
45
+ }): Promise<PasskeyUserDescriptor | null>;
46
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
47
+ deleteCredential(credentialId: Buffer | string): Promise<boolean>;
48
+ findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
49
+ saveCredential(record: StoredPasskeyCredential): Promise<void>;
50
+ updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
51
+ saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
52
+ consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
53
+ cleanupChallenges(now: Date): Promise<void>;
54
+ }
55
+ export {};