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