@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.
- package/README.txt +81 -28
- package/dist/cjs/api-module.cjs +9 -0
- package/dist/cjs/api-module.d.ts +7 -4
- package/dist/cjs/api-server-base.cjs +607 -99
- package/dist/cjs/api-server-base.d.ts +80 -23
- package/dist/cjs/auth-api/auth-module.d.ts +23 -3
- package/dist/cjs/auth-api/auth-module.js +320 -124
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/cjs/auth-api/compat-auth-storage.js +15 -3
- package/dist/cjs/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/cjs/auth-api/mem-auth-store.js +14 -28
- package/dist/cjs/auth-api/module.d.ts +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/cjs/auth-api/sql-auth-store.js +43 -30
- package/dist/cjs/auth-api/storage.d.ts +6 -4
- package/dist/cjs/auth-api/storage.js +15 -5
- package/dist/cjs/auth-api/types.d.ts +7 -2
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-api/user-id.js +38 -0
- package/dist/cjs/auth-cookie-options.d.ts +11 -0
- package/dist/cjs/auth-cookie-options.js +66 -0
- package/dist/cjs/index.cjs +4 -14
- package/dist/cjs/index.d.ts +4 -9
- package/dist/cjs/oauth/memory.d.ts +6 -0
- package/dist/cjs/oauth/memory.js +44 -11
- package/dist/cjs/oauth/models.d.ts +7 -2
- package/dist/cjs/oauth/models.js +10 -21
- package/dist/cjs/oauth/sequelize.d.ts +10 -48
- package/dist/cjs/oauth/sequelize.js +44 -99
- package/dist/cjs/oauth/types.d.ts +1 -0
- package/dist/cjs/passkey/base.d.ts +2 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/config.js +26 -0
- package/dist/cjs/passkey/memory.d.ts +8 -0
- package/dist/cjs/passkey/memory.js +57 -16
- package/dist/cjs/passkey/models.d.ts +13 -4
- package/dist/cjs/passkey/models.js +41 -14
- package/dist/cjs/passkey/sequelize.d.ts +13 -25
- package/dist/cjs/passkey/sequelize.js +68 -153
- package/dist/cjs/passkey/service.d.ts +6 -2
- package/dist/cjs/passkey/service.js +205 -27
- package/dist/cjs/passkey/types.d.ts +18 -9
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/sequelize-utils.js +57 -0
- package/dist/cjs/token/base.d.ts +2 -1
- package/dist/cjs/token/base.js +3 -1
- package/dist/cjs/token/memory.d.ts +10 -0
- package/dist/cjs/token/memory.js +122 -32
- package/dist/cjs/token/sequelize.d.ts +4 -4
- package/dist/cjs/token/sequelize.js +67 -85
- package/dist/cjs/token/types.d.ts +8 -1
- package/dist/cjs/user/base.d.ts +1 -0
- package/dist/cjs/user/base.js +11 -4
- package/dist/cjs/user/memory.d.ts +2 -0
- package/dist/cjs/user/memory.js +9 -10
- package/dist/cjs/user/sequelize.d.ts +7 -2
- package/dist/cjs/user/sequelize.js +19 -32
- package/dist/esm/api-module.d.ts +7 -4
- package/dist/esm/api-module.js +9 -0
- package/dist/esm/api-server-base.d.ts +80 -23
- package/dist/esm/api-server-base.js +608 -100
- package/dist/esm/auth-api/auth-module.d.ts +23 -3
- package/dist/esm/auth-api/auth-module.js +321 -125
- package/dist/esm/auth-api/compat-auth-storage.d.ts +7 -5
- package/dist/esm/auth-api/compat-auth-storage.js +13 -1
- package/dist/esm/auth-api/mem-auth-store.d.ts +5 -3
- package/dist/esm/auth-api/mem-auth-store.js +14 -28
- package/dist/esm/auth-api/module.d.ts +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +16 -4
- package/dist/esm/auth-api/sql-auth-store.js +43 -30
- package/dist/esm/auth-api/storage.d.ts +6 -4
- package/dist/esm/auth-api/storage.js +13 -3
- package/dist/esm/auth-api/types.d.ts +7 -2
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +32 -0
- package/dist/esm/auth-cookie-options.d.ts +11 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/index.d.ts +4 -9
- package/dist/esm/index.js +2 -7
- package/dist/esm/oauth/memory.d.ts +6 -0
- package/dist/esm/oauth/memory.js +44 -11
- package/dist/esm/oauth/models.d.ts +7 -2
- package/dist/esm/oauth/models.js +6 -19
- package/dist/esm/oauth/sequelize.d.ts +10 -48
- package/dist/esm/oauth/sequelize.js +32 -87
- package/dist/esm/oauth/types.d.ts +1 -0
- package/dist/esm/passkey/base.d.ts +2 -0
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.d.ts +8 -0
- package/dist/esm/passkey/memory.js +57 -16
- package/dist/esm/passkey/models.d.ts +13 -4
- package/dist/esm/passkey/models.js +39 -12
- package/dist/esm/passkey/sequelize.d.ts +13 -25
- package/dist/esm/passkey/sequelize.js +69 -154
- package/dist/esm/passkey/service.d.ts +6 -2
- package/dist/esm/passkey/service.js +173 -28
- package/dist/esm/passkey/types.d.ts +18 -9
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +48 -0
- package/dist/esm/token/base.d.ts +2 -1
- package/dist/esm/token/base.js +3 -1
- package/dist/esm/token/memory.d.ts +10 -0
- package/dist/esm/token/memory.js +122 -32
- package/dist/esm/token/sequelize.d.ts +4 -4
- package/dist/esm/token/sequelize.js +67 -85
- package/dist/esm/token/types.d.ts +8 -1
- package/dist/esm/user/base.d.ts +1 -0
- package/dist/esm/user/base.js +11 -4
- package/dist/esm/user/memory.d.ts +2 -0
- package/dist/esm/user/memory.js +9 -10
- package/dist/esm/user/sequelize.d.ts +7 -2
- package/dist/esm/user/sequelize.js +19 -32
- package/docs/swagger/openapi.json +1876 -0
- 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 {
|
|
4
|
+
export { nullAuthAdapter, BaseAuthAdapter } from './auth-api/storage.js';
|
|
5
5
|
export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
|
|
6
|
-
export {
|
|
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
|
}
|
package/dist/esm/oauth/memory.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
|
48
|
+
export declare function initOAuthCodeModel(sequelize: Sequelize, options?: {
|
|
49
|
+
tablePrefix?: string;
|
|
50
|
+
}): typeof OAuthCodeModel;
|
package/dist/esm/oauth/models.js
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import { DataTypes, Model } from 'sequelize';
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
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 {
|
|
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
|
-
|
|
5
|
-
|
|
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 =
|
|
108
|
-
|
|
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:
|
|
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
|
|
166
|
-
if (!
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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,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
|
}
|