@technomoron/api-server-base 2.0.0-beta.21 → 2.0.0-beta.23
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/common/types.cjs +10 -0
- package/dist/cjs/common/types.d.ts +137 -0
- package/dist/cjs/{api-module.cjs → server/src/api-module.cjs} +8 -0
- package/dist/{esm → cjs/server/src}/api-module.d.ts +15 -0
- package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +669 -627
- package/dist/{esm → cjs/server/src}/api-server-base.d.ts +105 -78
- package/dist/cjs/{auth-api/auth-module.js → server/src/auth-api/auth-module.cjs} +96 -76
- package/dist/cjs/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
- package/dist/cjs/{auth-api/compat-auth-storage.js → server/src/auth-api/compat-auth-storage.cjs} +4 -4
- package/dist/cjs/{auth-api/mem-auth-store.js → server/src/auth-api/mem-auth-store.cjs} +7 -7
- package/dist/cjs/{auth-api/module.js → server/src/auth-api/module.cjs} +1 -1
- package/dist/cjs/server/src/auth-api/schemas.cjs +171 -0
- package/dist/cjs/server/src/auth-api/schemas.d.ts +21 -0
- package/dist/cjs/{auth-api/sql-auth-store.js → server/src/auth-api/sql-auth-store.cjs} +8 -8
- package/dist/cjs/{auth-api/user-id.js → server/src/auth-api/user-id.cjs} +12 -3
- package/dist/{esm → cjs/server/src}/auth-cookie-options.d.ts +5 -3
- package/dist/cjs/server/src/base/client-info.cjs +285 -0
- package/dist/cjs/server/src/base/client-info.d.ts +27 -0
- package/dist/cjs/server/src/base/error-utils.cjs +50 -0
- package/dist/cjs/server/src/base/error-utils.d.ts +16 -0
- package/dist/cjs/server/src/base/request-utils.cjs +27 -0
- package/dist/cjs/server/src/base/request-utils.d.ts +8 -0
- package/dist/cjs/{index.cjs → server/src/index.cjs} +24 -15
- package/dist/{esm → cjs/server/src}/index.d.ts +7 -0
- package/dist/cjs/server/src/limiter/auth-rate-limiter.cjs +35 -0
- package/dist/cjs/server/src/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/cjs/server/src/limiter/fixed-window.cjs +41 -0
- package/dist/cjs/server/src/limiter/fixed-window.d.ts +11 -0
- package/dist/cjs/{oauth/base.js → server/src/oauth/base.cjs} +1 -0
- package/dist/cjs/{oauth → server/src/oauth}/base.d.ts +8 -1
- package/dist/cjs/{oauth/memory.js → server/src/oauth/memory.cjs} +7 -4
- package/dist/{esm → cjs/server/src}/oauth/memory.d.ts +1 -1
- package/dist/cjs/{oauth/models.js → server/src/oauth/models.cjs} +2 -2
- package/dist/cjs/{oauth/sequelize.js → server/src/oauth/sequelize.cjs} +11 -7
- package/dist/{esm → cjs/server/src}/oauth/sequelize.d.ts +1 -1
- package/dist/cjs/{passkey/base.js → server/src/passkey/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/passkey/base.d.ts +11 -0
- package/dist/cjs/{passkey/memory.js → server/src/passkey/memory.cjs} +2 -2
- package/dist/cjs/{passkey/models.js → server/src/passkey/models.cjs} +1 -1
- package/dist/cjs/{passkey/sequelize.js → server/src/passkey/sequelize.cjs} +3 -3
- package/dist/cjs/{passkey/service.js → server/src/passkey/service.cjs} +17 -3
- package/dist/{esm → cjs/server/src}/passkey/service.d.ts +1 -1
- package/dist/cjs/{sequelize-utils.js → server/src/sequelize-utils.cjs} +4 -5
- package/dist/cjs/{token/base.js → server/src/token/base.cjs} +4 -0
- package/dist/{esm → cjs/server/src}/token/base.d.ts +7 -0
- package/dist/cjs/{token/memory.js → server/src/token/memory.cjs} +15 -20
- package/dist/cjs/{token/sequelize.js → server/src/token/sequelize.cjs} +25 -11
- package/dist/cjs/server/src/upload/memory.cjs +92 -0
- package/dist/cjs/server/src/upload/memory.d.ts +17 -0
- package/dist/cjs/server/src/upload/tus-module.cjs +270 -0
- package/dist/cjs/server/src/upload/tus-module.d.ts +38 -0
- package/dist/cjs/server/src/upload/types.d.ts +8 -0
- package/dist/cjs/{user/base.js → server/src/user/base.cjs} +1 -0
- package/dist/cjs/{user → server/src/user}/base.d.ts +9 -0
- package/dist/cjs/{user/memory.js → server/src/user/memory.cjs} +29 -7
- package/dist/cjs/{user/sequelize.js → server/src/user/sequelize.cjs} +33 -8
- package/dist/cjs/server/src/user/types.cjs +2 -0
- package/dist/esm/common/types.d.ts +137 -0
- package/dist/esm/common/types.js +9 -0
- package/dist/{cjs → esm/server/src}/api-module.d.ts +15 -0
- package/dist/esm/{api-module.js → server/src/api-module.js} +8 -0
- package/dist/{cjs → esm/server/src}/api-server-base.d.ts +105 -78
- package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +658 -616
- package/dist/esm/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
- package/dist/esm/{auth-api → server/src/auth-api}/auth-module.js +92 -72
- package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.js +3 -3
- package/dist/esm/server/src/auth-api/schemas.d.ts +21 -0
- package/dist/esm/server/src/auth-api/schemas.js +168 -0
- package/dist/esm/{auth-api → server/src/auth-api}/user-id.js +12 -3
- package/dist/{cjs → esm/server/src}/auth-cookie-options.d.ts +5 -3
- package/dist/esm/server/src/base/client-info.d.ts +27 -0
- package/dist/esm/server/src/base/client-info.js +282 -0
- package/dist/esm/server/src/base/error-utils.d.ts +16 -0
- package/dist/esm/server/src/base/error-utils.js +44 -0
- package/dist/esm/server/src/base/request-utils.d.ts +8 -0
- package/dist/esm/server/src/base/request-utils.js +23 -0
- package/dist/{cjs → esm/server/src}/index.d.ts +7 -0
- package/dist/esm/{index.js → server/src/index.js} +4 -0
- package/dist/esm/server/src/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/esm/server/src/limiter/auth-rate-limiter.js +32 -0
- package/dist/esm/server/src/limiter/fixed-window.d.ts +11 -0
- package/dist/esm/server/src/limiter/fixed-window.js +37 -0
- package/dist/esm/{oauth → server/src/oauth}/base.d.ts +8 -1
- package/dist/esm/server/src/oauth/base.js +3 -0
- package/dist/{cjs → esm/server/src}/oauth/memory.d.ts +1 -1
- package/dist/esm/{oauth → server/src/oauth}/memory.js +5 -2
- package/dist/{cjs → esm/server/src}/oauth/sequelize.d.ts +1 -1
- package/dist/esm/{oauth → server/src/oauth}/sequelize.js +6 -2
- package/dist/{cjs → esm/server/src}/passkey/base.d.ts +11 -0
- package/dist/esm/server/src/passkey/base.js +3 -0
- package/dist/{cjs → esm/server/src}/passkey/service.d.ts +1 -1
- package/dist/esm/{passkey → server/src/passkey}/service.js +17 -3
- package/dist/esm/{sequelize-utils.js → server/src/sequelize-utils.js} +4 -5
- package/dist/{cjs → esm/server/src}/token/base.d.ts +7 -0
- package/dist/esm/{token → server/src/token}/base.js +4 -0
- package/dist/esm/{token → server/src/token}/memory.js +14 -19
- package/dist/esm/{token → server/src/token}/sequelize.js +22 -8
- package/dist/esm/server/src/upload/memory.d.ts +17 -0
- package/dist/esm/server/src/upload/memory.js +86 -0
- package/dist/esm/server/src/upload/tus-module.d.ts +38 -0
- package/dist/esm/server/src/upload/tus-module.js +266 -0
- package/dist/esm/server/src/upload/types.d.ts +8 -0
- package/dist/esm/{user → server/src/user}/base.d.ts +9 -0
- package/dist/esm/{user → server/src/user}/base.js +1 -0
- package/dist/esm/{user → server/src/user}/memory.js +27 -5
- package/dist/esm/{user → server/src/user}/sequelize.js +30 -5
- package/dist/esm/server/src/user/types.js +1 -0
- package/docs/swagger/openapi.json +411 -125
- package/package.json +129 -134
- package/README.txt +0 -213
- package/dist/esm/oauth/base.js +0 -2
- package/dist/esm/passkey/base.js +0 -2
- /package/dist/cjs/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/module.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
- /package/dist/cjs/{auth-api/storage.js → server/src/auth-api/storage.cjs} +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
- /package/dist/cjs/{auth-api/types.js → server/src/auth-api/types.cjs} +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/types.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
- /package/dist/cjs/{auth-cookie-options.js → server/src/auth-cookie-options.cjs} +0 -0
- /package/dist/cjs/{oauth → server/src/oauth}/models.d.ts +0 -0
- /package/dist/cjs/{oauth/types.js → server/src/oauth/types.cjs} +0 -0
- /package/dist/cjs/{oauth → server/src/oauth}/types.d.ts +0 -0
- /package/dist/cjs/{passkey/config.js → server/src/passkey/config.cjs} +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/config.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/memory.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/models.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
- /package/dist/cjs/{passkey/types.js → server/src/passkey/types.cjs} +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/types.d.ts +0 -0
- /package/dist/cjs/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
- /package/dist/cjs/{token → server/src/token}/memory.d.ts +0 -0
- /package/dist/cjs/{token → server/src/token}/sequelize.d.ts +0 -0
- /package/dist/cjs/{token/types.js → server/src/token/types.cjs} +0 -0
- /package/dist/cjs/{token → server/src/token}/types.d.ts +0 -0
- /package/dist/cjs/{user/types.js → server/src/upload/types.cjs} +0 -0
- /package/dist/cjs/{user → server/src/user}/memory.d.ts +0 -0
- /package/dist/cjs/{user → server/src/user}/sequelize.d.ts +0 -0
- /package/dist/cjs/{user → server/src/user}/types.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/module.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/module.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/storage.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/types.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/types.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
- /package/dist/esm/{auth-cookie-options.js → server/src/auth-cookie-options.js} +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/models.d.ts +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/models.js +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/types.d.ts +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/types.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/config.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/config.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/memory.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/memory.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/models.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/models.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/sequelize.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/types.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/types.js +0 -0
- /package/dist/esm/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
- /package/dist/esm/{token → server/src/token}/memory.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/sequelize.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/types.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/types.js +0 -0
- /package/dist/esm/{user → server/src/upload}/types.js +0 -0
- /package/dist/esm/{user → server/src/user}/memory.d.ts +0 -0
- /package/dist/esm/{user → server/src/user}/sequelize.d.ts +0 -0
- /package/dist/esm/{user → server/src/user}/types.d.ts +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { CookieOptions } from 'express-serve-static-core';
|
|
1
|
+
import type { ApiCookieOptions } from './api-server-base.js';
|
|
3
2
|
export interface AuthCookieConfig {
|
|
4
3
|
cookieSecure?: boolean | 'auto';
|
|
5
4
|
cookieSameSite?: 'lax' | 'strict' | 'none';
|
|
@@ -8,4 +7,7 @@ export interface AuthCookieConfig {
|
|
|
8
7
|
cookiePath?: string;
|
|
9
8
|
devMode?: boolean;
|
|
10
9
|
}
|
|
11
|
-
export declare function buildAuthCookieOptions(config: AuthCookieConfig, req:
|
|
10
|
+
export declare function buildAuthCookieOptions(config: AuthCookieConfig, req: {
|
|
11
|
+
headers: Record<string, string | string[] | undefined>;
|
|
12
|
+
protocol?: string;
|
|
13
|
+
}): ApiCookieOptions;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface ClientInfoRequest {
|
|
2
|
+
headers: Record<string, string | string[] | undefined>;
|
|
3
|
+
ips?: string[];
|
|
4
|
+
ip?: string;
|
|
5
|
+
socket?: {
|
|
6
|
+
remoteAddress?: string;
|
|
7
|
+
};
|
|
8
|
+
connection?: {
|
|
9
|
+
remoteAddress?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface ClientAgentProfile {
|
|
13
|
+
ua: string;
|
|
14
|
+
browser: string;
|
|
15
|
+
os: string;
|
|
16
|
+
device: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ClientInfo extends ClientAgentProfile {
|
|
19
|
+
ip: string | null;
|
|
20
|
+
ipchain: string[];
|
|
21
|
+
}
|
|
22
|
+
interface ApiRequestWithClientInfo {
|
|
23
|
+
req: ClientInfoRequest;
|
|
24
|
+
clientInfo?: ClientInfo;
|
|
25
|
+
}
|
|
26
|
+
export declare function ensureClientInfo(apiReq: ApiRequestWithClientInfo, trustProxy?: boolean | number): ClientInfo;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
function normalizeIpAddress(candidate) {
|
|
2
|
+
let value = candidate.trim();
|
|
3
|
+
if (!value) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
|
|
7
|
+
if (value.startsWith('::ffff:')) {
|
|
8
|
+
value = value.slice(7);
|
|
9
|
+
}
|
|
10
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
11
|
+
value = value.slice(1, -1);
|
|
12
|
+
}
|
|
13
|
+
const firstColon = value.indexOf(':');
|
|
14
|
+
const lastColon = value.lastIndexOf(':');
|
|
15
|
+
if (firstColon !== -1 && firstColon === lastColon) {
|
|
16
|
+
const maybePort = value.slice(lastColon + 1);
|
|
17
|
+
if (/^\d+$/.test(maybePort)) {
|
|
18
|
+
value = value.slice(0, lastColon);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
value = value.trim();
|
|
22
|
+
return value || null;
|
|
23
|
+
}
|
|
24
|
+
function extractForwardedFor(header) {
|
|
25
|
+
if (!header) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const values = Array.isArray(header) ? header : [header];
|
|
29
|
+
const ips = [];
|
|
30
|
+
for (const entry of values) {
|
|
31
|
+
for (const part of entry.split(',')) {
|
|
32
|
+
const normalized = normalizeIpAddress(part);
|
|
33
|
+
if (normalized) {
|
|
34
|
+
ips.push(normalized);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return ips;
|
|
39
|
+
}
|
|
40
|
+
function extractForwardedHeader(header) {
|
|
41
|
+
if (!header) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const values = Array.isArray(header) ? header : [header];
|
|
45
|
+
const ips = [];
|
|
46
|
+
for (const entry of values) {
|
|
47
|
+
for (const part of entry.split(',')) {
|
|
48
|
+
const match = part.match(/for=([^;]+)/i);
|
|
49
|
+
if (match) {
|
|
50
|
+
const normalized = normalizeIpAddress(match[1]);
|
|
51
|
+
if (normalized) {
|
|
52
|
+
ips.push(normalized);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return ips;
|
|
58
|
+
}
|
|
59
|
+
function detectBrowser(userAgent) {
|
|
60
|
+
const browserMatchers = [
|
|
61
|
+
{ label: 'Edge', pattern: /(Edg|Edge|EdgiOS|EdgA)\/([\d.]+)/i, versionGroup: 2 },
|
|
62
|
+
{ label: 'Chrome', pattern: /(Chrome|CriOS)\/([\d.]+)/i, versionGroup: 2 },
|
|
63
|
+
{ label: 'Firefox', pattern: /(Firefox|FxiOS)\/([\d.]+)/i, versionGroup: 2 },
|
|
64
|
+
{ label: 'Safari', pattern: /Version\/([\d.]+).*Safari/i, versionGroup: 1 },
|
|
65
|
+
{ label: 'Opera', pattern: /(OPR|Opera)\/([\d.]+)/i, versionGroup: 2 },
|
|
66
|
+
{ label: 'Brave', pattern: /Brave\/([\d.]+)/i, versionGroup: 1 },
|
|
67
|
+
{ label: 'Vivaldi', pattern: /Vivaldi\/([\d.]+)/i, versionGroup: 1 },
|
|
68
|
+
{ label: 'Electron', pattern: /Electron\/([\d.]+)/i, versionGroup: 1 },
|
|
69
|
+
{ label: 'Node', pattern: /Node\.js\/([\d.]+)/i, versionGroup: 1 },
|
|
70
|
+
{ label: 'IE', pattern: /MSIE ([\d.]+)/i, versionGroup: 1 },
|
|
71
|
+
{ label: 'IE', pattern: /Trident\/.*rv:([\d.]+)/i, versionGroup: 1 }
|
|
72
|
+
];
|
|
73
|
+
for (const matcher of browserMatchers) {
|
|
74
|
+
const m = userAgent.match(matcher.pattern);
|
|
75
|
+
if (m) {
|
|
76
|
+
const version = matcher.versionGroup ? m[matcher.versionGroup] : '';
|
|
77
|
+
return version ? `${matcher.label} ${version}` : matcher.label;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
function detectOs(userAgent) {
|
|
83
|
+
const osMatchers = [
|
|
84
|
+
{
|
|
85
|
+
label: 'Windows',
|
|
86
|
+
pattern: /Windows NT ([\d.]+)/i,
|
|
87
|
+
transform: (match) => `Windows ${match[1]}`
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: 'iOS',
|
|
91
|
+
pattern: /iPhone OS ([\d_]+)/i,
|
|
92
|
+
transform: (match) => `iOS ${match[1].replace(/_/g, '.')}`
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'iPadOS',
|
|
96
|
+
pattern: /iPad; CPU OS ([\d_]+)/i,
|
|
97
|
+
transform: (match) => `iPadOS ${match[1].replace(/_/g, '.')}`
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: 'macOS',
|
|
101
|
+
pattern: /Mac OS X ([\d_]+)/i,
|
|
102
|
+
transform: (match) => `macOS ${match[1].replace(/_/g, '.')}`
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: 'Android',
|
|
106
|
+
pattern: /Android ([\d.]+)/i,
|
|
107
|
+
transform: (match) => `Android ${match[1]}`
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
label: 'ChromeOS',
|
|
111
|
+
pattern: /CrOS [^ ]+ ([\d.]+)/i,
|
|
112
|
+
transform: (match) => `ChromeOS ${match[1]}`
|
|
113
|
+
},
|
|
114
|
+
{ label: 'Linux', pattern: /Linux/i },
|
|
115
|
+
{ label: 'Unix', pattern: /X11/i }
|
|
116
|
+
];
|
|
117
|
+
for (const matcher of osMatchers) {
|
|
118
|
+
const m = userAgent.match(matcher.pattern);
|
|
119
|
+
if (m) {
|
|
120
|
+
return matcher.transform ? matcher.transform(m) : matcher.label;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
function detectDevice(userAgent, osLabel) {
|
|
126
|
+
if (/iPhone/i.test(userAgent)) {
|
|
127
|
+
return 'iPhone';
|
|
128
|
+
}
|
|
129
|
+
if (/iPad/i.test(userAgent)) {
|
|
130
|
+
return 'iPad';
|
|
131
|
+
}
|
|
132
|
+
if (/iPod/i.test(userAgent)) {
|
|
133
|
+
return 'iPod';
|
|
134
|
+
}
|
|
135
|
+
if (/Android/i.test(userAgent)) {
|
|
136
|
+
const match = userAgent.match(/;\s*([^;]+)\s+Build/i);
|
|
137
|
+
if (match) {
|
|
138
|
+
return match[1];
|
|
139
|
+
}
|
|
140
|
+
return 'Android Device';
|
|
141
|
+
}
|
|
142
|
+
if (/Macintosh/i.test(userAgent)) {
|
|
143
|
+
return 'Mac';
|
|
144
|
+
}
|
|
145
|
+
if (/Windows/i.test(userAgent)) {
|
|
146
|
+
return 'PC';
|
|
147
|
+
}
|
|
148
|
+
if (/CrOS/i.test(userAgent)) {
|
|
149
|
+
return 'Chromebook';
|
|
150
|
+
}
|
|
151
|
+
return osLabel;
|
|
152
|
+
}
|
|
153
|
+
function parseClientAgent(userAgentHeader) {
|
|
154
|
+
const raw = Array.isArray(userAgentHeader) ? userAgentHeader[0] : userAgentHeader;
|
|
155
|
+
const ua = typeof raw === 'string' ? raw.trim() : '';
|
|
156
|
+
if (!ua) {
|
|
157
|
+
return { ua: '', browser: '', os: '', device: '' };
|
|
158
|
+
}
|
|
159
|
+
const os = detectOs(ua);
|
|
160
|
+
const browser = detectBrowser(ua);
|
|
161
|
+
const device = detectDevice(ua, os);
|
|
162
|
+
return { ua, browser, os, device };
|
|
163
|
+
}
|
|
164
|
+
function isLoopbackAddress(ip) {
|
|
165
|
+
if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (ip === '0.0.0.0' || ip === '127.0.0.1') {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (ip.startsWith('127.')) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function collectSocketAddresses(req) {
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
const result = [];
|
|
179
|
+
const pushNormalized = (ip) => {
|
|
180
|
+
if (!ip || seen.has(ip))
|
|
181
|
+
return;
|
|
182
|
+
seen.add(ip);
|
|
183
|
+
result.push(ip);
|
|
184
|
+
};
|
|
185
|
+
if (Array.isArray(req.ips)) {
|
|
186
|
+
for (const ip of req.ips) {
|
|
187
|
+
pushNormalized(normalizeIpAddress(ip));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (typeof req.ip === 'string') {
|
|
191
|
+
pushNormalized(normalizeIpAddress(req.ip));
|
|
192
|
+
}
|
|
193
|
+
const socketAddress = req.socket?.remoteAddress;
|
|
194
|
+
if (typeof socketAddress === 'string') {
|
|
195
|
+
pushNormalized(normalizeIpAddress(socketAddress));
|
|
196
|
+
}
|
|
197
|
+
const connectionAddress = req.connection?.remoteAddress;
|
|
198
|
+
if (typeof connectionAddress === 'string') {
|
|
199
|
+
pushNormalized(normalizeIpAddress(connectionAddress));
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
function collectForwardedIps(req) {
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
const result = [];
|
|
206
|
+
const pushNormalized = (ip) => {
|
|
207
|
+
if (!ip || seen.has(ip))
|
|
208
|
+
return;
|
|
209
|
+
seen.add(ip);
|
|
210
|
+
result.push(ip);
|
|
211
|
+
};
|
|
212
|
+
for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
|
|
213
|
+
pushNormalized(ip);
|
|
214
|
+
}
|
|
215
|
+
for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
|
|
216
|
+
pushNormalized(ip);
|
|
217
|
+
}
|
|
218
|
+
const realIp = req.headers['x-real-ip'];
|
|
219
|
+
if (Array.isArray(realIp)) {
|
|
220
|
+
for (const value of realIp) {
|
|
221
|
+
pushNormalized(normalizeIpAddress(value));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (typeof realIp === 'string') {
|
|
225
|
+
pushNormalized(normalizeIpAddress(realIp));
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
function collectClientIpChain(req, trustProxy) {
|
|
230
|
+
const socketIps = collectSocketAddresses(req);
|
|
231
|
+
// trustProxy=false: ignore forwarded headers entirely
|
|
232
|
+
if (trustProxy === false) {
|
|
233
|
+
return socketIps;
|
|
234
|
+
}
|
|
235
|
+
const forwardedIps = collectForwardedIps(req);
|
|
236
|
+
// trustProxy=number: only trust that many rightmost forwarded entries
|
|
237
|
+
if (typeof trustProxy === 'number' && trustProxy >= 0) {
|
|
238
|
+
const trusted = trustProxy > 0 ? forwardedIps.slice(-trustProxy) : [];
|
|
239
|
+
const seen = new Set(trusted);
|
|
240
|
+
for (const ip of socketIps) {
|
|
241
|
+
if (!seen.has(ip)) {
|
|
242
|
+
seen.add(ip);
|
|
243
|
+
trusted.push(ip);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return trusted;
|
|
247
|
+
}
|
|
248
|
+
// trustProxy=true or undefined: trust all (backwards-compatible default)
|
|
249
|
+
const seen = new Set(forwardedIps);
|
|
250
|
+
const result = [...forwardedIps];
|
|
251
|
+
for (const ip of socketIps) {
|
|
252
|
+
if (!seen.has(ip)) {
|
|
253
|
+
seen.add(ip);
|
|
254
|
+
result.push(ip);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
function selectClientIp(chain) {
|
|
260
|
+
for (const ip of chain) {
|
|
261
|
+
if (!isLoopbackAddress(ip)) {
|
|
262
|
+
return ip;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return chain[0] ?? null;
|
|
266
|
+
}
|
|
267
|
+
function buildClientInfo(req, trustProxy) {
|
|
268
|
+
const agent = parseClientAgent(req.headers['user-agent']);
|
|
269
|
+
const ipchain = collectClientIpChain(req, trustProxy);
|
|
270
|
+
const ip = selectClientIp(ipchain);
|
|
271
|
+
return {
|
|
272
|
+
...agent,
|
|
273
|
+
ip,
|
|
274
|
+
ipchain
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export function ensureClientInfo(apiReq, trustProxy) {
|
|
278
|
+
if (!apiReq.clientInfo) {
|
|
279
|
+
apiReq.clientInfo = buildClientInfo(apiReq.req, trustProxy);
|
|
280
|
+
}
|
|
281
|
+
return apiReq.clientInfo;
|
|
282
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ApiErrorParams } from '../api-server-base.js';
|
|
2
|
+
export interface ApiErrorLike {
|
|
3
|
+
code: number;
|
|
4
|
+
message: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
errors?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function guessExceptionText(error: unknown, defMsg?: string): string;
|
|
9
|
+
export declare function normalizeApiErrorParams({ code, message, data, errors }: ApiErrorParams): {
|
|
10
|
+
code: number;
|
|
11
|
+
message: string;
|
|
12
|
+
data: unknown;
|
|
13
|
+
errors: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
export declare function isApiErrorLike(candidate: unknown): candidate is ApiErrorLike;
|
|
16
|
+
export declare function asHttpStatus(error: unknown): number | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function guessExceptionText(error, defMsg = 'Unknown Error') {
|
|
2
|
+
const msg = [];
|
|
3
|
+
if (typeof error === 'string' && error.trim() !== '') {
|
|
4
|
+
msg.push(error);
|
|
5
|
+
}
|
|
6
|
+
else if (error && typeof error === 'object') {
|
|
7
|
+
const errorDetails = error;
|
|
8
|
+
if (typeof errorDetails.message === 'string' && errorDetails.message.trim() !== '') {
|
|
9
|
+
msg.push(errorDetails.message);
|
|
10
|
+
}
|
|
11
|
+
if (errorDetails.parent &&
|
|
12
|
+
typeof errorDetails.parent.message === 'string' &&
|
|
13
|
+
errorDetails.parent.message.trim() !== '') {
|
|
14
|
+
msg.push(errorDetails.parent.message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return msg.length > 0 ? msg.join(' / ') : defMsg;
|
|
18
|
+
}
|
|
19
|
+
export function normalizeApiErrorParams({ code, message, data, errors }) {
|
|
20
|
+
return {
|
|
21
|
+
code: typeof code === 'number' ? code : 500,
|
|
22
|
+
message: guessExceptionText(message, '[Unknown error (null/undefined)]'),
|
|
23
|
+
data: data !== undefined ? data : null,
|
|
24
|
+
errors: errors !== undefined ? errors : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function isApiErrorLike(candidate) {
|
|
28
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const maybeError = candidate;
|
|
32
|
+
return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
|
|
33
|
+
}
|
|
34
|
+
export function asHttpStatus(error) {
|
|
35
|
+
if (!error || typeof error !== 'object') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const maybe = error;
|
|
39
|
+
const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
|
|
40
|
+
if (typeof status === 'number' && status >= 400 && status <= 599) {
|
|
41
|
+
return status;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isPlainObject(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const proto = Object.getPrototypeOf(value);
|
|
6
|
+
return proto === Object.prototype || proto === null;
|
|
7
|
+
}
|
|
8
|
+
export function hydrateGetBody(req) {
|
|
9
|
+
if ((req.method ?? '').toUpperCase() !== 'GET') {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const query = isPlainObject(req.query) ? req.query : null;
|
|
13
|
+
if (!query || Object.keys(query).length === 0) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const body = isPlainObject(req.body) ? req.body : null;
|
|
17
|
+
if (!body || Object.keys(body).length === 0) {
|
|
18
|
+
req.body = { ...query };
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Keep explicit body fields authoritative when both query and body provide the same key.
|
|
22
|
+
req.body = { ...query, ...body };
|
|
23
|
+
}
|
|
@@ -25,3 +25,10 @@ export { MemoryPasskeyStore } from './passkey/memory.js';
|
|
|
25
25
|
export type { PasskeyServiceConfig, PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential, PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
26
26
|
export { OAuthStore } from './oauth/base.js';
|
|
27
27
|
export { MemoryOAuthStore } from './oauth/memory.js';
|
|
28
|
+
export { FixedWindowRateLimiter } from './limiter/fixed-window.js';
|
|
29
|
+
export type { RateLimitDecision } from './limiter/fixed-window.js';
|
|
30
|
+
export { createAuthRateLimitHook } from './limiter/auth-rate-limiter.js';
|
|
31
|
+
export type { AuthRateLimitConfig } from './limiter/auth-rate-limiter.js';
|
|
32
|
+
export { TusUploadModule } from './upload/tus-module.js';
|
|
33
|
+
export { MemoryTusUploadStore, TusUploadOffsetError } from './upload/memory.js';
|
|
34
|
+
export type { TusMetadata, TusUploadRecord, TusCreateUploadInput, TusAppendInput, TusUploadStore } from './upload/types.js';
|
|
@@ -15,3 +15,7 @@ export { PasskeyStore } from './passkey/base.js';
|
|
|
15
15
|
export { MemoryPasskeyStore } from './passkey/memory.js';
|
|
16
16
|
export { OAuthStore } from './oauth/base.js';
|
|
17
17
|
export { MemoryOAuthStore } from './oauth/memory.js';
|
|
18
|
+
export { FixedWindowRateLimiter } from './limiter/fixed-window.js';
|
|
19
|
+
export { createAuthRateLimitHook } from './limiter/auth-rate-limiter.js';
|
|
20
|
+
export { TusUploadModule } from './upload/tus-module.js';
|
|
21
|
+
export { MemoryTusUploadStore, TusUploadOffsetError } from './upload/memory.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ApiRequest } from '../api-server-base.js';
|
|
2
|
+
import { FixedWindowRateLimiter } from './fixed-window.js';
|
|
3
|
+
type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token' | 'oauth-authorize';
|
|
4
|
+
export interface AuthRateLimitConfig {
|
|
5
|
+
windowSec?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createAuthRateLimitHook(config: Partial<Record<AuthRateLimitEndpoint, AuthRateLimitConfig>>, limiter?: FixedWindowRateLimiter): (ctx: {
|
|
9
|
+
apiReq: ApiRequest;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
}) => void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ApiError } from '../api-server-base.js';
|
|
2
|
+
import { FixedWindowRateLimiter } from './fixed-window.js';
|
|
3
|
+
export function createAuthRateLimitHook(config, limiter) {
|
|
4
|
+
if (!config || Object.keys(config).length === 0) {
|
|
5
|
+
return () => { };
|
|
6
|
+
}
|
|
7
|
+
const instance = limiter ?? new FixedWindowRateLimiter();
|
|
8
|
+
return (ctx) => {
|
|
9
|
+
const endpointConfig = config[ctx.endpoint];
|
|
10
|
+
if (!endpointConfig) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const windowSec = endpointConfig.windowSec ?? 0;
|
|
14
|
+
const max = endpointConfig.max ?? 0;
|
|
15
|
+
if (windowSec <= 0 || max <= 0) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const clientIp = ctx.apiReq.getClientIp() ?? '';
|
|
19
|
+
if (!clientIp) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const key = `${ctx.endpoint}:${clientIp}`;
|
|
23
|
+
const decision = instance.check(key, max, windowSec * 1000);
|
|
24
|
+
if (!decision.allowed) {
|
|
25
|
+
throw new ApiError({
|
|
26
|
+
code: 429,
|
|
27
|
+
message: 'Too many requests; try again later',
|
|
28
|
+
data: { retryAfterSec: decision.retryAfterSec }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RateLimitDecision = {
|
|
2
|
+
allowed: boolean;
|
|
3
|
+
retryAfterSec: number;
|
|
4
|
+
};
|
|
5
|
+
export declare class FixedWindowRateLimiter {
|
|
6
|
+
private readonly maxKeys;
|
|
7
|
+
private readonly buckets;
|
|
8
|
+
constructor(maxKeys?: number);
|
|
9
|
+
check(key: string, max: number, windowMs: number): RateLimitDecision;
|
|
10
|
+
private prune;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class FixedWindowRateLimiter {
|
|
2
|
+
constructor(maxKeys = 10000) {
|
|
3
|
+
this.maxKeys = maxKeys;
|
|
4
|
+
this.buckets = new Map();
|
|
5
|
+
}
|
|
6
|
+
check(key, max, windowMs) {
|
|
7
|
+
if (!key || max <= 0 || windowMs <= 0) {
|
|
8
|
+
return { allowed: true, retryAfterSec: 0 };
|
|
9
|
+
}
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const bucket = this.buckets.get(key);
|
|
12
|
+
if (!bucket || now - bucket.windowStartMs >= windowMs) {
|
|
13
|
+
this.buckets.delete(key);
|
|
14
|
+
this.buckets.set(key, { windowStartMs: now, count: 1 });
|
|
15
|
+
this.prune();
|
|
16
|
+
return { allowed: true, retryAfterSec: 0 };
|
|
17
|
+
}
|
|
18
|
+
bucket.count += 1;
|
|
19
|
+
// Refresh insertion order to keep active entries at the end for pruning.
|
|
20
|
+
this.buckets.delete(key);
|
|
21
|
+
this.buckets.set(key, bucket);
|
|
22
|
+
if (bucket.count <= max) {
|
|
23
|
+
return { allowed: true, retryAfterSec: 0 };
|
|
24
|
+
}
|
|
25
|
+
const retryAfterSec = Math.max(1, Math.ceil((bucket.windowStartMs + windowMs - now) / 1000));
|
|
26
|
+
return { allowed: false, retryAfterSec };
|
|
27
|
+
}
|
|
28
|
+
prune() {
|
|
29
|
+
while (this.buckets.size > this.maxKeys) {
|
|
30
|
+
const oldest = this.buckets.keys().next().value;
|
|
31
|
+
if (!oldest) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
this.buckets.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { AuthCode, OAuthClient } from './types.js';
|
|
2
|
+
/** Base contract for OAuth client/auth-code persistence backends. */
|
|
2
3
|
export declare abstract class OAuthStore {
|
|
4
|
+
/** Fetch an OAuth client by id. */
|
|
3
5
|
abstract getClient(clientId: string): Promise<OAuthClient | null>;
|
|
6
|
+
/** Create an OAuth client. */
|
|
4
7
|
abstract createClient(input: OAuthClient): Promise<OAuthClient>;
|
|
8
|
+
/** Verify an OAuth client secret. */
|
|
5
9
|
abstract verifyClientSecret(clientId: string, secret: string | null): Promise<boolean>;
|
|
10
|
+
/** Persist an authorization code. */
|
|
6
11
|
abstract createAuthCode(code: AuthCode): Promise<void>;
|
|
7
|
-
|
|
12
|
+
/** Consume an authorization code once. When clientId is provided, only consume if it matches. */
|
|
13
|
+
abstract consumeAuthCode(code: string, clientId?: string): Promise<AuthCode | null>;
|
|
14
|
+
/** Close underlying resources. */
|
|
8
15
|
abstract close(): Promise<void>;
|
|
9
16
|
}
|
|
10
17
|
export type { OAuthClient, AuthCode } from './types.js';
|
|
@@ -15,7 +15,7 @@ export declare class MemoryOAuthStore extends OAuthStore {
|
|
|
15
15
|
createClient(input: OAuthClient): Promise<OAuthClient>;
|
|
16
16
|
verifyClientSecret(clientId: string, secret: string | null): Promise<boolean>;
|
|
17
17
|
createAuthCode(code: AuthCode): Promise<void>;
|
|
18
|
-
consumeAuthCode(code: string): Promise<AuthCode | null>;
|
|
18
|
+
consumeAuthCode(code: string, clientId?: string): Promise<AuthCode | null>;
|
|
19
19
|
close(): Promise<void>;
|
|
20
20
|
private enforceClientCapacity;
|
|
21
21
|
private enforceCodeCapacity;
|
|
@@ -11,7 +11,7 @@ function cloneClient(client) {
|
|
|
11
11
|
name: client.name,
|
|
12
12
|
redirectUris: [...client.redirectUris],
|
|
13
13
|
scope: client.scope ? [...client.scope] : undefined,
|
|
14
|
-
metadata: client.metadata ?
|
|
14
|
+
metadata: client.metadata ? JSON.parse(JSON.stringify(client.metadata)) : undefined,
|
|
15
15
|
firstParty: client.firstParty
|
|
16
16
|
};
|
|
17
17
|
}
|
|
@@ -83,11 +83,14 @@ export class MemoryOAuthStore extends OAuthStore {
|
|
|
83
83
|
this.codes.set(record.code, record);
|
|
84
84
|
this.enforceCodeCapacity();
|
|
85
85
|
}
|
|
86
|
-
async consumeAuthCode(code) {
|
|
86
|
+
async consumeAuthCode(code, clientId) {
|
|
87
87
|
const record = this.codes.get(code);
|
|
88
88
|
if (!record) {
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
|
+
if (clientId && record.clientId !== clientId) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
91
94
|
if (record.expiresAt.getTime() <= Date.now()) {
|
|
92
95
|
this.codes.delete(code);
|
|
93
96
|
return null;
|
|
@@ -23,7 +23,7 @@ export declare class SequelizeOAuthStore extends OAuthStore {
|
|
|
23
23
|
createClient(input: OAuthClient): Promise<OAuthClient>;
|
|
24
24
|
verifyClientSecret(clientId: string, clientSecret: string | null): Promise<boolean>;
|
|
25
25
|
createAuthCode(code: AuthCode): Promise<void>;
|
|
26
|
-
consumeAuthCode(code: string): Promise<AuthCode | null>;
|
|
26
|
+
consumeAuthCode(code: string, clientId?: string): Promise<AuthCode | null>;
|
|
27
27
|
close(): Promise<void>;
|
|
28
28
|
private toOAuthClient;
|
|
29
29
|
private toAuthCode;
|
|
@@ -97,13 +97,17 @@ export class SequelizeOAuthStore extends OAuthStore {
|
|
|
97
97
|
metadata: serializeMetadata(code.metadata)
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
-
async consumeAuthCode(code) {
|
|
100
|
+
async consumeAuthCode(code, clientId) {
|
|
101
101
|
const sequelize = this.codes.sequelize;
|
|
102
102
|
if (!sequelize) {
|
|
103
103
|
throw new Error('Code model is not bound to a Sequelize instance');
|
|
104
104
|
}
|
|
105
105
|
return sequelize.transaction({ isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }, async (transaction) => {
|
|
106
|
-
const
|
|
106
|
+
const where = { code };
|
|
107
|
+
if (clientId) {
|
|
108
|
+
where.client_id = clientId;
|
|
109
|
+
}
|
|
110
|
+
const model = await this.codes.findOne({ where, transaction, lock: true });
|
|
107
111
|
if (!model) {
|
|
108
112
|
return null;
|
|
109
113
|
}
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import type { PasskeyChallengeRecord, PasskeyStorageAdapter, PasskeyUserDescriptor, StoredPasskeyCredential } from './types.js';
|
|
2
2
|
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
/** Base contract for passkey credential/challenge persistence backends. */
|
|
3
4
|
export declare abstract class PasskeyStore implements PasskeyStorageAdapter {
|
|
5
|
+
/** Resolve a passkey user descriptor by id/login. */
|
|
4
6
|
abstract resolveUser(params: {
|
|
5
7
|
userId?: AuthIdentifier;
|
|
6
8
|
login?: string;
|
|
7
9
|
}): Promise<PasskeyUserDescriptor | null>;
|
|
10
|
+
/** List passkey credentials for a user. */
|
|
8
11
|
abstract listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
12
|
+
/** Delete a credential by binary/base64url id. */
|
|
9
13
|
abstract deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
14
|
+
/** Find a credential by binary id. */
|
|
10
15
|
abstract findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
16
|
+
/** Save a credential record. */
|
|
11
17
|
abstract saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
18
|
+
/** Update signature counter for a credential. */
|
|
12
19
|
abstract updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
20
|
+
/** Save a challenge record. */
|
|
13
21
|
abstract saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
22
|
+
/** Read a challenge without consuming it. */
|
|
14
23
|
abstract getChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
24
|
+
/** Consume and return a challenge. */
|
|
15
25
|
abstract consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
26
|
+
/** Cleanup expired challenges. */
|
|
16
27
|
abstract cleanupChallenges(now: Date): Promise<void>;
|
|
17
28
|
}
|