@stonyx/oauth 0.1.1-beta.40 → 0.1.1-beta.42
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/auth-request.d.ts +35 -0
- package/dist/auth-request.js +68 -0
- package/dist/main.d.ts +22 -0
- package/dist/main.js +75 -0
- package/dist/oauth-flow.d.ts +30 -0
- package/dist/oauth-flow.js +83 -0
- package/dist/providers/discord.d.ts +30 -0
- package/dist/providers/discord.js +43 -0
- package/dist/session-manager.d.ts +20 -0
- package/dist/session-manager.js +30 -0
- package/dist/token-manager.d.ts +15 -0
- package/dist/token-manager.js +24 -0
- package/package.json +32 -5
- package/src/{auth-request.js → auth-request.ts} +26 -6
- package/src/{main.js → main.ts} +27 -10
- package/src/{oauth-flow.js → oauth-flow.ts} +31 -7
- package/src/providers/{discord.js → discord.ts} +29 -3
- package/src/{session-manager.js → session-manager.ts} +19 -6
- package/src/token-manager.ts +35 -0
- package/src/types/node.d.ts +3 -0
- package/src/types/stonyx-events.d.ts +4 -0
- package/src/types/stonyx-rest-server.d.ts +11 -0
- package/src/types/stonyx.d.ts +30 -0
- package/src/token-manager.js +0 -26
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Request } from '@stonyx/rest-server';
|
|
2
|
+
interface OAuthInstance {
|
|
3
|
+
frontendCallbackUrl?: string;
|
|
4
|
+
getSession(sessionId: string): unknown;
|
|
5
|
+
getAuthorizationUrl(providerName: string): string;
|
|
6
|
+
handleCallback(providerName: string, code: string, stateToken: string): Promise<{
|
|
7
|
+
sessionId: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
}>;
|
|
10
|
+
logout(sessionId: string): void;
|
|
11
|
+
}
|
|
12
|
+
interface RouteRequest {
|
|
13
|
+
headers: Record<string, string | undefined>;
|
|
14
|
+
params: Record<string, string>;
|
|
15
|
+
query: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
interface RouteState {
|
|
18
|
+
redirect?: string;
|
|
19
|
+
}
|
|
20
|
+
export default class AuthRequest extends Request {
|
|
21
|
+
oauth: OAuthInstance;
|
|
22
|
+
constructor(oauth: OAuthInstance);
|
|
23
|
+
handlers: {
|
|
24
|
+
get: {
|
|
25
|
+
'/': ({ headers }: RouteRequest) => {};
|
|
26
|
+
'/login/:provider': (req: RouteRequest, state: RouteState) => 404 | undefined;
|
|
27
|
+
'/callback/:provider': (req: RouteRequest, state: RouteState) => Promise<{
|
|
28
|
+
sessionId: string;
|
|
29
|
+
expiresAt: number;
|
|
30
|
+
} | 400 | 500 | undefined>;
|
|
31
|
+
'/logout': ({ headers }: RouteRequest) => void;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Request } from '@stonyx/rest-server';
|
|
2
|
+
export default class AuthRequest extends Request {
|
|
3
|
+
oauth;
|
|
4
|
+
constructor(oauth) {
|
|
5
|
+
super();
|
|
6
|
+
this.oauth = oauth;
|
|
7
|
+
}
|
|
8
|
+
handlers = {
|
|
9
|
+
get: {
|
|
10
|
+
'/': ({ headers }) => {
|
|
11
|
+
const sessionId = headers['session-id'];
|
|
12
|
+
if (!sessionId)
|
|
13
|
+
return 401;
|
|
14
|
+
const user = this.oauth.getSession(sessionId);
|
|
15
|
+
if (!user)
|
|
16
|
+
return 401;
|
|
17
|
+
return user;
|
|
18
|
+
},
|
|
19
|
+
'/login/:provider': (req, state) => {
|
|
20
|
+
const { provider: providerName } = req.params;
|
|
21
|
+
try {
|
|
22
|
+
const url = this.oauth.getAuthorizationUrl(providerName);
|
|
23
|
+
state.redirect = url;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 404;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
'/callback/:provider': async (req, state) => {
|
|
30
|
+
const { provider: providerName } = req.params;
|
|
31
|
+
const { code, state: stateToken, error } = req.query;
|
|
32
|
+
if (error) {
|
|
33
|
+
if (this.oauth.frontendCallbackUrl) {
|
|
34
|
+
state.redirect = `${this.oauth.frontendCallbackUrl}?error=${encodeURIComponent(error)}`;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
return 400;
|
|
38
|
+
}
|
|
39
|
+
if (!code)
|
|
40
|
+
return 400;
|
|
41
|
+
try {
|
|
42
|
+
const session = await this.oauth.handleCallback(providerName, code, stateToken);
|
|
43
|
+
if (this.oauth.frontendCallbackUrl) {
|
|
44
|
+
const params = new URLSearchParams({
|
|
45
|
+
sessionId: session.sessionId,
|
|
46
|
+
expiresAt: String(session.expiresAt),
|
|
47
|
+
});
|
|
48
|
+
state.redirect = `${this.oauth.frontendCallbackUrl}?${params}`;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
return session;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
if (this.oauth.frontendCallbackUrl) {
|
|
55
|
+
state.redirect = `${this.oauth.frontendCallbackUrl}?error=auth_failed`;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
return 500;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
'/logout': ({ headers }) => {
|
|
62
|
+
const sessionId = headers['session-id'];
|
|
63
|
+
if (sessionId)
|
|
64
|
+
this.oauth.logout(sessionId);
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import TokenManager from './token-manager.js';
|
|
2
|
+
import SessionManager from './session-manager.js';
|
|
3
|
+
import type OAuthFlow from './oauth-flow.js';
|
|
4
|
+
interface ProviderEntry {
|
|
5
|
+
flow: OAuthFlow;
|
|
6
|
+
tokenManager: TokenManager;
|
|
7
|
+
}
|
|
8
|
+
export default class OAuth {
|
|
9
|
+
static instance: OAuth | null;
|
|
10
|
+
providers: Map<string, ProviderEntry>;
|
|
11
|
+
pendingStates: Map<string, number>;
|
|
12
|
+
sessionManager: SessionManager;
|
|
13
|
+
frontendCallbackUrl?: string;
|
|
14
|
+
constructor();
|
|
15
|
+
init(): Promise<void>;
|
|
16
|
+
getProvider(name: string): ProviderEntry;
|
|
17
|
+
getAuthorizationUrl(providerName: string): string;
|
|
18
|
+
handleCallback(providerName: string, code: string, stateToken: string): Promise<import("./session-manager.js").SessionResult>;
|
|
19
|
+
getSession(sessionId: string): unknown;
|
|
20
|
+
logout(sessionId: string): void;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import config from 'stonyx/config';
|
|
2
|
+
import log from 'stonyx/log';
|
|
3
|
+
import { waitForModule } from 'stonyx';
|
|
4
|
+
import { setup, emit } from '@stonyx/events';
|
|
5
|
+
import RestServer from '@stonyx/rest-server';
|
|
6
|
+
import TokenManager from './token-manager.js';
|
|
7
|
+
import SessionManager from './session-manager.js';
|
|
8
|
+
import AuthRequest from './auth-request.js';
|
|
9
|
+
setup(['authenticate']);
|
|
10
|
+
export default class OAuth {
|
|
11
|
+
static instance;
|
|
12
|
+
providers = new Map();
|
|
13
|
+
pendingStates = new Map();
|
|
14
|
+
sessionManager;
|
|
15
|
+
frontendCallbackUrl;
|
|
16
|
+
constructor() {
|
|
17
|
+
if (OAuth.instance)
|
|
18
|
+
return OAuth.instance;
|
|
19
|
+
OAuth.instance = this;
|
|
20
|
+
}
|
|
21
|
+
async init() {
|
|
22
|
+
const oauthConfig = config.oauth;
|
|
23
|
+
const { providers, sessionDuration, frontendCallbackUrl } = oauthConfig;
|
|
24
|
+
this.frontendCallbackUrl = frontendCallbackUrl;
|
|
25
|
+
for (const [name, providerConfig] of Object.entries(providers)) {
|
|
26
|
+
const modulePath = providerConfig.module
|
|
27
|
+
? `${config.rootPath}/${providerConfig.module}`
|
|
28
|
+
: `./providers/${name}.js`;
|
|
29
|
+
const { default: Provider } = await import(modulePath);
|
|
30
|
+
const flow = new Provider(providerConfig);
|
|
31
|
+
this.providers.set(name, { flow, tokenManager: new TokenManager(flow) });
|
|
32
|
+
}
|
|
33
|
+
this.sessionManager = new SessionManager(sessionDuration);
|
|
34
|
+
await waitForModule('rest-server');
|
|
35
|
+
RestServer.instance.mountRoute(AuthRequest, { name: 'auth', options: this });
|
|
36
|
+
log.oauth?.('OAuth module initialized');
|
|
37
|
+
}
|
|
38
|
+
getProvider(name) {
|
|
39
|
+
const provider = this.providers.get(name);
|
|
40
|
+
if (!provider)
|
|
41
|
+
throw new Error(`OAuth provider "${name}" is not configured`);
|
|
42
|
+
return provider;
|
|
43
|
+
}
|
|
44
|
+
getAuthorizationUrl(providerName) {
|
|
45
|
+
const { flow } = this.getProvider(providerName);
|
|
46
|
+
const stateToken = crypto.randomUUID();
|
|
47
|
+
this.pendingStates.set(stateToken, Date.now());
|
|
48
|
+
return flow.buildAuthorizationUrl(stateToken);
|
|
49
|
+
}
|
|
50
|
+
async handleCallback(providerName, code, stateToken) {
|
|
51
|
+
if (!stateToken || !this.pendingStates.has(stateToken)) {
|
|
52
|
+
throw new Error('Invalid or missing state token');
|
|
53
|
+
}
|
|
54
|
+
const stateCreatedAt = this.pendingStates.get(stateToken);
|
|
55
|
+
if (stateCreatedAt === undefined)
|
|
56
|
+
throw new Error('State token not found in pending states');
|
|
57
|
+
this.pendingStates.delete(stateToken);
|
|
58
|
+
const TEN_MINUTES = 10 * 60 * 1000;
|
|
59
|
+
if (Date.now() - stateCreatedAt > TEN_MINUTES) {
|
|
60
|
+
throw new Error('State token has expired');
|
|
61
|
+
}
|
|
62
|
+
const { flow, tokenManager } = this.getProvider(providerName);
|
|
63
|
+
const tokens = await tokenManager.getTokens(code);
|
|
64
|
+
const rawUser = await flow.fetchUserInfo(tokens.accessToken);
|
|
65
|
+
const user = flow.normalizeUser(rawUser);
|
|
66
|
+
await emit('authenticate', user);
|
|
67
|
+
return this.sessionManager.create(user, tokens);
|
|
68
|
+
}
|
|
69
|
+
getSession(sessionId) {
|
|
70
|
+
return this.sessionManager.validate(sessionId);
|
|
71
|
+
}
|
|
72
|
+
logout(sessionId) {
|
|
73
|
+
this.sessionManager.destroy(sessionId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface OAuthConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes?: string[];
|
|
6
|
+
authorizationUrl: string;
|
|
7
|
+
tokenUrl: string;
|
|
8
|
+
userInfoUrl: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenResult {
|
|
11
|
+
accessToken: string;
|
|
12
|
+
refreshToken: string | null;
|
|
13
|
+
expiresIn: number;
|
|
14
|
+
}
|
|
15
|
+
export default class OAuthFlow {
|
|
16
|
+
clientId: string;
|
|
17
|
+
clientSecret: string;
|
|
18
|
+
redirectUri: string;
|
|
19
|
+
scopes: string[];
|
|
20
|
+
authorizationUrl: string;
|
|
21
|
+
tokenUrl: string;
|
|
22
|
+
userInfoUrl: string;
|
|
23
|
+
constructor({ clientId, clientSecret, redirectUri, scopes, authorizationUrl, tokenUrl, userInfoUrl }: OAuthConfig);
|
|
24
|
+
buildAuthorizationUrl(stateToken: string): string;
|
|
25
|
+
exchangeCode(code: string): Promise<TokenResult>;
|
|
26
|
+
refreshAccessToken(refreshToken: string): Promise<TokenResult>;
|
|
27
|
+
fetchUserInfo(accessToken: string): Promise<unknown>;
|
|
28
|
+
normalizeUser(rawUser: unknown): unknown;
|
|
29
|
+
revokeToken(_accessToken: string): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export default class OAuthFlow {
|
|
2
|
+
clientId;
|
|
3
|
+
clientSecret;
|
|
4
|
+
redirectUri;
|
|
5
|
+
scopes;
|
|
6
|
+
authorizationUrl;
|
|
7
|
+
tokenUrl;
|
|
8
|
+
userInfoUrl;
|
|
9
|
+
constructor({ clientId, clientSecret, redirectUri, scopes, authorizationUrl, tokenUrl, userInfoUrl }) {
|
|
10
|
+
this.clientId = clientId;
|
|
11
|
+
this.clientSecret = clientSecret;
|
|
12
|
+
this.redirectUri = redirectUri;
|
|
13
|
+
this.scopes = scopes || [];
|
|
14
|
+
this.authorizationUrl = authorizationUrl;
|
|
15
|
+
this.tokenUrl = tokenUrl;
|
|
16
|
+
this.userInfoUrl = userInfoUrl;
|
|
17
|
+
}
|
|
18
|
+
buildAuthorizationUrl(stateToken) {
|
|
19
|
+
const params = new URLSearchParams({
|
|
20
|
+
client_id: this.clientId,
|
|
21
|
+
redirect_uri: this.redirectUri,
|
|
22
|
+
response_type: 'code',
|
|
23
|
+
scope: this.scopes.join(' '),
|
|
24
|
+
state: stateToken,
|
|
25
|
+
});
|
|
26
|
+
return `${this.authorizationUrl}?${params.toString()}`;
|
|
27
|
+
}
|
|
28
|
+
async exchangeCode(code) {
|
|
29
|
+
const response = await fetch(this.tokenUrl, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
client_id: this.clientId,
|
|
34
|
+
client_secret: this.clientSecret,
|
|
35
|
+
grant_type: 'authorization_code',
|
|
36
|
+
code,
|
|
37
|
+
redirect_uri: this.redirectUri,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok)
|
|
41
|
+
throw new Error(`Token exchange failed: ${response.status}`);
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
return {
|
|
44
|
+
accessToken: data.access_token,
|
|
45
|
+
refreshToken: data.refresh_token || null,
|
|
46
|
+
expiresIn: data.expires_in,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async refreshAccessToken(refreshToken) {
|
|
50
|
+
const response = await fetch(this.tokenUrl, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
client_id: this.clientId,
|
|
55
|
+
client_secret: this.clientSecret,
|
|
56
|
+
grant_type: 'refresh_token',
|
|
57
|
+
refresh_token: refreshToken,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
return {
|
|
64
|
+
accessToken: data.access_token,
|
|
65
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
66
|
+
expiresIn: data.expires_in,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async fetchUserInfo(accessToken) {
|
|
70
|
+
const response = await fetch(this.userInfoUrl, {
|
|
71
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
throw new Error(`User info fetch failed: ${response.status}`);
|
|
75
|
+
return response.json();
|
|
76
|
+
}
|
|
77
|
+
normalizeUser(rawUser) {
|
|
78
|
+
return { raw: rawUser };
|
|
79
|
+
}
|
|
80
|
+
async revokeToken(_accessToken) {
|
|
81
|
+
// Optional — providers override if supported
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import OAuthFlow from '../oauth-flow.js';
|
|
2
|
+
import type { TokenResult } from '../oauth-flow.js';
|
|
3
|
+
interface DiscordProviderConfig {
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
redirectUri: string;
|
|
7
|
+
scopes?: string[];
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
interface DiscordUser {
|
|
11
|
+
id: string;
|
|
12
|
+
username: string;
|
|
13
|
+
global_name?: string;
|
|
14
|
+
avatar: string | null;
|
|
15
|
+
email?: string | null;
|
|
16
|
+
}
|
|
17
|
+
interface NormalizedDiscordUser {
|
|
18
|
+
id: string;
|
|
19
|
+
username: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
avatar: string | null;
|
|
22
|
+
email: string | null;
|
|
23
|
+
raw: DiscordUser;
|
|
24
|
+
}
|
|
25
|
+
export default class DiscordProvider extends OAuthFlow {
|
|
26
|
+
constructor(config: DiscordProviderConfig);
|
|
27
|
+
exchangeCode(code: string): Promise<TokenResult>;
|
|
28
|
+
normalizeUser(rawUser: DiscordUser): NormalizedDiscordUser;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import OAuthFlow from '../oauth-flow.js';
|
|
2
|
+
export default class DiscordProvider extends OAuthFlow {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
super({
|
|
5
|
+
...config,
|
|
6
|
+
authorizationUrl: 'https://discord.com/oauth2/authorize',
|
|
7
|
+
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
8
|
+
userInfoUrl: 'https://discord.com/api/users/@me',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async exchangeCode(code) {
|
|
12
|
+
const response = await fetch(this.tokenUrl, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
15
|
+
body: new URLSearchParams({
|
|
16
|
+
client_id: this.clientId,
|
|
17
|
+
client_secret: this.clientSecret,
|
|
18
|
+
grant_type: 'authorization_code',
|
|
19
|
+
code,
|
|
20
|
+
redirect_uri: this.redirectUri,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new Error(`Token exchange failed: ${response.status}`);
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
return {
|
|
27
|
+
accessToken: data.access_token,
|
|
28
|
+
refreshToken: data.refresh_token || null,
|
|
29
|
+
expiresIn: data.expires_in,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
normalizeUser(rawUser) {
|
|
33
|
+
const { id, username, global_name, avatar, email } = rawUser;
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
username,
|
|
37
|
+
displayName: global_name || username,
|
|
38
|
+
avatar: avatar ? `https://cdn.discordapp.com/avatars/${id}/${avatar}.png` : null,
|
|
39
|
+
email: email || null,
|
|
40
|
+
raw: rawUser,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface SessionData {
|
|
2
|
+
user: unknown;
|
|
3
|
+
tokens: unknown;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SessionResult {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
user: unknown;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
}
|
|
11
|
+
export default class SessionManager {
|
|
12
|
+
sessions: Map<string, SessionData>;
|
|
13
|
+
duration: number;
|
|
14
|
+
constructor(duration: number);
|
|
15
|
+
create(user: unknown, tokens: unknown): SessionResult;
|
|
16
|
+
get(sessionId: string): SessionData | null;
|
|
17
|
+
destroy(sessionId: string): void;
|
|
18
|
+
validate(sessionId: string): unknown;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export default class SessionManager {
|
|
3
|
+
sessions = new Map();
|
|
4
|
+
duration;
|
|
5
|
+
constructor(duration) {
|
|
6
|
+
this.duration = duration;
|
|
7
|
+
}
|
|
8
|
+
create(user, tokens) {
|
|
9
|
+
const sessionId = randomUUID();
|
|
10
|
+
const expiresAt = Date.now() + (this.duration * 1000);
|
|
11
|
+
this.sessions.set(sessionId, { user, tokens, expiresAt });
|
|
12
|
+
return { sessionId, user, expiresAt };
|
|
13
|
+
}
|
|
14
|
+
get(sessionId) {
|
|
15
|
+
return this.sessions.get(sessionId) || null;
|
|
16
|
+
}
|
|
17
|
+
destroy(sessionId) {
|
|
18
|
+
this.sessions.delete(sessionId);
|
|
19
|
+
}
|
|
20
|
+
validate(sessionId) {
|
|
21
|
+
const session = this.get(sessionId);
|
|
22
|
+
if (!session)
|
|
23
|
+
return null;
|
|
24
|
+
if (Date.now() >= session.expiresAt) {
|
|
25
|
+
this.destroy(sessionId);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return session.user;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type OAuthFlow from './oauth-flow.js';
|
|
2
|
+
import type { TokenResult } from './oauth-flow.js';
|
|
3
|
+
export interface TokenData extends TokenResult {
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export default class TokenManager {
|
|
7
|
+
flow: OAuthFlow;
|
|
8
|
+
constructor(flow: OAuthFlow);
|
|
9
|
+
getTokens(code: string): Promise<TokenData>;
|
|
10
|
+
refresh(refreshToken: string): Promise<TokenData>;
|
|
11
|
+
revoke(accessToken: string): Promise<void>;
|
|
12
|
+
isExpired(tokenData: {
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
} | null | undefined): boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default class TokenManager {
|
|
2
|
+
flow;
|
|
3
|
+
constructor(flow) {
|
|
4
|
+
this.flow = flow;
|
|
5
|
+
}
|
|
6
|
+
async getTokens(code) {
|
|
7
|
+
const tokens = await this.flow.exchangeCode(code);
|
|
8
|
+
tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
|
|
9
|
+
return tokens;
|
|
10
|
+
}
|
|
11
|
+
async refresh(refreshToken) {
|
|
12
|
+
const tokens = await this.flow.refreshAccessToken(refreshToken);
|
|
13
|
+
tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
|
|
14
|
+
return tokens;
|
|
15
|
+
}
|
|
16
|
+
async revoke(accessToken) {
|
|
17
|
+
return this.flow.revokeToken(accessToken);
|
|
18
|
+
}
|
|
19
|
+
isExpired(tokenData) {
|
|
20
|
+
if (!tokenData?.expiresAt)
|
|
21
|
+
return true;
|
|
22
|
+
return Date.now() >= tokenData.expiresAt;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -4,16 +4,39 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.1-beta.
|
|
7
|
+
"version": "0.1.1-beta.42",
|
|
8
8
|
"description": "OAuth2 authentication module for the Stonyx framework",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/abofs/stonyx-oauth.git"
|
|
12
12
|
},
|
|
13
|
-
"main": "
|
|
13
|
+
"main": "dist/main.js",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
|
-
".":
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/main.d.ts",
|
|
18
|
+
"default": "./dist/main.js"
|
|
19
|
+
},
|
|
20
|
+
"./oauth-flow": {
|
|
21
|
+
"types": "./dist/oauth-flow.d.ts",
|
|
22
|
+
"default": "./dist/oauth-flow.js"
|
|
23
|
+
},
|
|
24
|
+
"./auth-request": {
|
|
25
|
+
"types": "./dist/auth-request.d.ts",
|
|
26
|
+
"default": "./dist/auth-request.js"
|
|
27
|
+
},
|
|
28
|
+
"./session-manager": {
|
|
29
|
+
"types": "./dist/session-manager.d.ts",
|
|
30
|
+
"default": "./dist/session-manager.js"
|
|
31
|
+
},
|
|
32
|
+
"./token-manager": {
|
|
33
|
+
"types": "./dist/token-manager.d.ts",
|
|
34
|
+
"default": "./dist/token-manager.js"
|
|
35
|
+
},
|
|
36
|
+
"./providers/discord": {
|
|
37
|
+
"types": "./dist/providers/discord.d.ts",
|
|
38
|
+
"default": "./dist/providers/discord.js"
|
|
39
|
+
}
|
|
17
40
|
},
|
|
18
41
|
"author": "Stone Costa",
|
|
19
42
|
"license": "Apache-2.0",
|
|
@@ -21,6 +44,7 @@
|
|
|
21
44
|
"Stone Costa <stone.costa@synamicd.com>"
|
|
22
45
|
],
|
|
23
46
|
"files": [
|
|
47
|
+
"dist",
|
|
24
48
|
"src",
|
|
25
49
|
"config",
|
|
26
50
|
"README.md"
|
|
@@ -40,9 +64,12 @@
|
|
|
40
64
|
"@stonyx/rest-server": "0.2.1-beta.30",
|
|
41
65
|
"@stonyx/utils": "0.2.3-beta.7",
|
|
42
66
|
"qunit": "^2.24.1",
|
|
43
|
-
"sinon": "^21.0.0"
|
|
67
|
+
"sinon": "^21.0.0",
|
|
68
|
+
"typescript": "^5.8.3"
|
|
44
69
|
},
|
|
45
70
|
"scripts": {
|
|
46
|
-
"
|
|
71
|
+
"build": "tsc",
|
|
72
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
73
|
+
"test": "npm run build && npm run build:test && stonyx test 'dist-test/test/**/*-test.js'"
|
|
47
74
|
}
|
|
48
75
|
}
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { Request } from '@stonyx/rest-server';
|
|
2
2
|
|
|
3
|
+
interface OAuthInstance {
|
|
4
|
+
frontendCallbackUrl?: string;
|
|
5
|
+
getSession(sessionId: string): unknown;
|
|
6
|
+
getAuthorizationUrl(providerName: string): string;
|
|
7
|
+
handleCallback(providerName: string, code: string, stateToken: string): Promise<{ sessionId: string; expiresAt: number }>;
|
|
8
|
+
logout(sessionId: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RouteRequest {
|
|
12
|
+
headers: Record<string, string | undefined>;
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
query: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RouteState {
|
|
18
|
+
redirect?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export default class AuthRequest extends Request {
|
|
4
|
-
|
|
22
|
+
oauth: OAuthInstance;
|
|
23
|
+
|
|
24
|
+
constructor(oauth: OAuthInstance) {
|
|
5
25
|
super();
|
|
6
26
|
this.oauth = oauth;
|
|
7
27
|
}
|
|
8
28
|
|
|
9
29
|
handlers = {
|
|
10
30
|
get: {
|
|
11
|
-
'/': ({ headers }) => {
|
|
31
|
+
'/': ({ headers }: RouteRequest) => {
|
|
12
32
|
const sessionId = headers['session-id'];
|
|
13
33
|
if (!sessionId) return 401;
|
|
14
34
|
|
|
@@ -18,7 +38,7 @@ export default class AuthRequest extends Request {
|
|
|
18
38
|
return user;
|
|
19
39
|
},
|
|
20
40
|
|
|
21
|
-
'/login/:provider': (req, state) => {
|
|
41
|
+
'/login/:provider': (req: RouteRequest, state: RouteState) => {
|
|
22
42
|
const { provider: providerName } = req.params;
|
|
23
43
|
|
|
24
44
|
try {
|
|
@@ -29,7 +49,7 @@ export default class AuthRequest extends Request {
|
|
|
29
49
|
}
|
|
30
50
|
},
|
|
31
51
|
|
|
32
|
-
'/callback/:provider': async (req, state) => {
|
|
52
|
+
'/callback/:provider': async (req: RouteRequest, state: RouteState) => {
|
|
33
53
|
const { provider: providerName } = req.params;
|
|
34
54
|
const { code, state: stateToken, error } = req.query;
|
|
35
55
|
|
|
@@ -49,7 +69,7 @@ export default class AuthRequest extends Request {
|
|
|
49
69
|
if (this.oauth.frontendCallbackUrl) {
|
|
50
70
|
const params = new URLSearchParams({
|
|
51
71
|
sessionId: session.sessionId,
|
|
52
|
-
expiresAt: session.expiresAt,
|
|
72
|
+
expiresAt: String(session.expiresAt),
|
|
53
73
|
});
|
|
54
74
|
state.redirect = `${this.oauth.frontendCallbackUrl}?${params}`;
|
|
55
75
|
return;
|
|
@@ -65,7 +85,7 @@ export default class AuthRequest extends Request {
|
|
|
65
85
|
}
|
|
66
86
|
},
|
|
67
87
|
|
|
68
|
-
'/logout': ({ headers }) => {
|
|
88
|
+
'/logout': ({ headers }: RouteRequest) => {
|
|
69
89
|
const sessionId = headers['session-id'];
|
|
70
90
|
if (sessionId) this.oauth.logout(sessionId);
|
|
71
91
|
},
|
package/src/{main.js → main.ts}
RENAMED
|
@@ -6,20 +6,36 @@ import RestServer from '@stonyx/rest-server';
|
|
|
6
6
|
import TokenManager from './token-manager.js';
|
|
7
7
|
import SessionManager from './session-manager.js';
|
|
8
8
|
import AuthRequest from './auth-request.js';
|
|
9
|
+
import type OAuthFlow from './oauth-flow.js';
|
|
9
10
|
|
|
10
11
|
setup(['authenticate']);
|
|
11
12
|
|
|
13
|
+
interface ProviderEntry {
|
|
14
|
+
flow: OAuthFlow;
|
|
15
|
+
tokenManager: TokenManager;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ProviderConfig {
|
|
19
|
+
module?: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export default class OAuth {
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
static instance: OAuth | null;
|
|
25
|
+
|
|
26
|
+
providers = new Map<string, ProviderEntry>();
|
|
27
|
+
pendingStates = new Map<string, number>();
|
|
28
|
+
sessionManager!: SessionManager;
|
|
29
|
+
frontendCallbackUrl?: string;
|
|
15
30
|
|
|
16
31
|
constructor() {
|
|
17
32
|
if (OAuth.instance) return OAuth.instance;
|
|
18
33
|
OAuth.instance = this;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
async init() {
|
|
22
|
-
const
|
|
36
|
+
async init(): Promise<void> {
|
|
37
|
+
const oauthConfig = config.oauth;
|
|
38
|
+
const { providers, sessionDuration, frontendCallbackUrl } = oauthConfig;
|
|
23
39
|
this.frontendCallbackUrl = frontendCallbackUrl;
|
|
24
40
|
|
|
25
41
|
for (const [name, providerConfig] of Object.entries(providers)) {
|
|
@@ -27,7 +43,7 @@ export default class OAuth {
|
|
|
27
43
|
? `${config.rootPath}/${providerConfig.module}`
|
|
28
44
|
: `./providers/${name}.js`;
|
|
29
45
|
const { default: Provider } = await import(modulePath);
|
|
30
|
-
const flow = new Provider(providerConfig);
|
|
46
|
+
const flow: OAuthFlow = new Provider(providerConfig);
|
|
31
47
|
this.providers.set(name, { flow, tokenManager: new TokenManager(flow) });
|
|
32
48
|
}
|
|
33
49
|
|
|
@@ -39,25 +55,26 @@ export default class OAuth {
|
|
|
39
55
|
log.oauth?.('OAuth module initialized');
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
getProvider(name) {
|
|
58
|
+
getProvider(name: string): ProviderEntry {
|
|
43
59
|
const provider = this.providers.get(name);
|
|
44
60
|
if (!provider) throw new Error(`OAuth provider "${name}" is not configured`);
|
|
45
61
|
return provider;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
getAuthorizationUrl(providerName) {
|
|
64
|
+
getAuthorizationUrl(providerName: string): string {
|
|
49
65
|
const { flow } = this.getProvider(providerName);
|
|
50
66
|
const stateToken = crypto.randomUUID();
|
|
51
67
|
this.pendingStates.set(stateToken, Date.now());
|
|
52
68
|
return flow.buildAuthorizationUrl(stateToken);
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
async handleCallback(providerName, code, stateToken) {
|
|
71
|
+
async handleCallback(providerName: string, code: string, stateToken: string) {
|
|
56
72
|
if (!stateToken || !this.pendingStates.has(stateToken)) {
|
|
57
73
|
throw new Error('Invalid or missing state token');
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
const stateCreatedAt = this.pendingStates.get(stateToken);
|
|
77
|
+
if (stateCreatedAt === undefined) throw new Error('State token not found in pending states');
|
|
61
78
|
this.pendingStates.delete(stateToken);
|
|
62
79
|
|
|
63
80
|
const TEN_MINUTES = 10 * 60 * 1000;
|
|
@@ -73,11 +90,11 @@ export default class OAuth {
|
|
|
73
90
|
return this.sessionManager.create(user, tokens);
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
getSession(sessionId) {
|
|
93
|
+
getSession(sessionId: string) {
|
|
77
94
|
return this.sessionManager.validate(sessionId);
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
logout(sessionId) {
|
|
97
|
+
logout(sessionId: string): void {
|
|
81
98
|
this.sessionManager.destroy(sessionId);
|
|
82
99
|
}
|
|
83
100
|
}
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
+
export interface OAuthConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes?: string[];
|
|
6
|
+
authorizationUrl: string;
|
|
7
|
+
tokenUrl: string;
|
|
8
|
+
userInfoUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TokenResult {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string | null;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
export default class OAuthFlow {
|
|
2
|
-
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
redirectUri: string;
|
|
21
|
+
scopes: string[];
|
|
22
|
+
authorizationUrl: string;
|
|
23
|
+
tokenUrl: string;
|
|
24
|
+
userInfoUrl: string;
|
|
25
|
+
|
|
26
|
+
constructor({ clientId, clientSecret, redirectUri, scopes, authorizationUrl, tokenUrl, userInfoUrl }: OAuthConfig) {
|
|
3
27
|
this.clientId = clientId;
|
|
4
28
|
this.clientSecret = clientSecret;
|
|
5
29
|
this.redirectUri = redirectUri;
|
|
@@ -9,7 +33,7 @@ export default class OAuthFlow {
|
|
|
9
33
|
this.userInfoUrl = userInfoUrl;
|
|
10
34
|
}
|
|
11
35
|
|
|
12
|
-
buildAuthorizationUrl(stateToken) {
|
|
36
|
+
buildAuthorizationUrl(stateToken: string): string {
|
|
13
37
|
const params = new URLSearchParams({
|
|
14
38
|
client_id: this.clientId,
|
|
15
39
|
redirect_uri: this.redirectUri,
|
|
@@ -21,7 +45,7 @@ export default class OAuthFlow {
|
|
|
21
45
|
return `${this.authorizationUrl}?${params.toString()}`;
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
async exchangeCode(code) {
|
|
48
|
+
async exchangeCode(code: string): Promise<TokenResult> {
|
|
25
49
|
const response = await fetch(this.tokenUrl, {
|
|
26
50
|
method: 'POST',
|
|
27
51
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -45,7 +69,7 @@ export default class OAuthFlow {
|
|
|
45
69
|
};
|
|
46
70
|
}
|
|
47
71
|
|
|
48
|
-
async refreshAccessToken(refreshToken) {
|
|
72
|
+
async refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
|
49
73
|
const response = await fetch(this.tokenUrl, {
|
|
50
74
|
method: 'POST',
|
|
51
75
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -68,7 +92,7 @@ export default class OAuthFlow {
|
|
|
68
92
|
};
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
async fetchUserInfo(accessToken) {
|
|
95
|
+
async fetchUserInfo(accessToken: string): Promise<unknown> {
|
|
72
96
|
const response = await fetch(this.userInfoUrl, {
|
|
73
97
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
74
98
|
});
|
|
@@ -78,11 +102,11 @@ export default class OAuthFlow {
|
|
|
78
102
|
return response.json();
|
|
79
103
|
}
|
|
80
104
|
|
|
81
|
-
normalizeUser(rawUser) {
|
|
105
|
+
normalizeUser(rawUser: unknown): unknown {
|
|
82
106
|
return { raw: rawUser };
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
async revokeToken(_accessToken) {
|
|
109
|
+
async revokeToken(_accessToken: string): Promise<void> {
|
|
86
110
|
// Optional — providers override if supported
|
|
87
111
|
}
|
|
88
112
|
}
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import OAuthFlow from '../oauth-flow.js';
|
|
2
|
+
import type { TokenResult } from '../oauth-flow.js';
|
|
3
|
+
|
|
4
|
+
interface DiscordProviderConfig {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
scopes?: string[];
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DiscordUser {
|
|
13
|
+
id: string;
|
|
14
|
+
username: string;
|
|
15
|
+
global_name?: string;
|
|
16
|
+
avatar: string | null;
|
|
17
|
+
email?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface NormalizedDiscordUser {
|
|
21
|
+
id: string;
|
|
22
|
+
username: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
avatar: string | null;
|
|
25
|
+
email: string | null;
|
|
26
|
+
raw: DiscordUser;
|
|
27
|
+
}
|
|
2
28
|
|
|
3
29
|
export default class DiscordProvider extends OAuthFlow {
|
|
4
|
-
constructor(config) {
|
|
30
|
+
constructor(config: DiscordProviderConfig) {
|
|
5
31
|
super({
|
|
6
32
|
...config,
|
|
7
33
|
authorizationUrl: 'https://discord.com/oauth2/authorize',
|
|
@@ -10,7 +36,7 @@ export default class DiscordProvider extends OAuthFlow {
|
|
|
10
36
|
});
|
|
11
37
|
}
|
|
12
38
|
|
|
13
|
-
async exchangeCode(code) {
|
|
39
|
+
async exchangeCode(code: string): Promise<TokenResult> {
|
|
14
40
|
const response = await fetch(this.tokenUrl, {
|
|
15
41
|
method: 'POST',
|
|
16
42
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
@@ -34,7 +60,7 @@ export default class DiscordProvider extends OAuthFlow {
|
|
|
34
60
|
};
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
normalizeUser(rawUser) {
|
|
63
|
+
override normalizeUser(rawUser: DiscordUser): NormalizedDiscordUser {
|
|
38
64
|
const { id, username, global_name, avatar, email } = rawUser;
|
|
39
65
|
|
|
40
66
|
return {
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
interface SessionData {
|
|
4
|
+
user: unknown;
|
|
5
|
+
tokens: unknown;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SessionResult {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
user: unknown;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export default class SessionManager {
|
|
4
|
-
sessions = new Map();
|
|
16
|
+
sessions = new Map<string, SessionData>();
|
|
17
|
+
duration: number;
|
|
5
18
|
|
|
6
|
-
constructor(duration) {
|
|
19
|
+
constructor(duration: number) {
|
|
7
20
|
this.duration = duration;
|
|
8
21
|
}
|
|
9
22
|
|
|
10
|
-
create(user, tokens) {
|
|
23
|
+
create(user: unknown, tokens: unknown): SessionResult {
|
|
11
24
|
const sessionId = randomUUID();
|
|
12
25
|
const expiresAt = Date.now() + (this.duration * 1000);
|
|
13
26
|
|
|
@@ -16,15 +29,15 @@ export default class SessionManager {
|
|
|
16
29
|
return { sessionId, user, expiresAt };
|
|
17
30
|
}
|
|
18
31
|
|
|
19
|
-
get(sessionId) {
|
|
32
|
+
get(sessionId: string): SessionData | null {
|
|
20
33
|
return this.sessions.get(sessionId) || null;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
destroy(sessionId) {
|
|
36
|
+
destroy(sessionId: string): void {
|
|
24
37
|
this.sessions.delete(sessionId);
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
validate(sessionId) {
|
|
40
|
+
validate(sessionId: string): unknown {
|
|
28
41
|
const session = this.get(sessionId);
|
|
29
42
|
if (!session) return null;
|
|
30
43
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type OAuthFlow from './oauth-flow.js';
|
|
2
|
+
import type { TokenResult } from './oauth-flow.js';
|
|
3
|
+
|
|
4
|
+
export interface TokenData extends TokenResult {
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default class TokenManager {
|
|
9
|
+
flow: OAuthFlow;
|
|
10
|
+
|
|
11
|
+
constructor(flow: OAuthFlow) {
|
|
12
|
+
this.flow = flow;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getTokens(code: string): Promise<TokenData> {
|
|
16
|
+
const tokens = await this.flow.exchangeCode(code) as TokenData;
|
|
17
|
+
tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
|
|
18
|
+
return tokens;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async refresh(refreshToken: string): Promise<TokenData> {
|
|
22
|
+
const tokens = await this.flow.refreshAccessToken(refreshToken) as TokenData;
|
|
23
|
+
tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
|
|
24
|
+
return tokens;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async revoke(accessToken: string): Promise<void> {
|
|
28
|
+
return this.flow.revokeToken(accessToken);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isExpired(tokenData: { expiresAt?: number } | null | undefined): boolean {
|
|
32
|
+
if (!tokenData?.expiresAt) return true;
|
|
33
|
+
return Date.now() >= tokenData.expiresAt;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare module '@stonyx/rest-server' {
|
|
2
|
+
export class Request {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export default class RestServer {
|
|
7
|
+
static instance: RestServer;
|
|
8
|
+
static close(): void;
|
|
9
|
+
mountRoute(RequestClass: unknown, options: { name: string; options?: unknown }): void;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
declare module 'stonyx/config' {
|
|
2
|
+
interface OAuthConfig {
|
|
3
|
+
providers: Record<string, { module?: string; [key: string]: unknown }>;
|
|
4
|
+
sessionDuration: number;
|
|
5
|
+
frontendCallbackUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Config {
|
|
8
|
+
oauth: OAuthConfig;
|
|
9
|
+
rootPath: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
const config: Config;
|
|
13
|
+
export default config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module 'stonyx/log' {
|
|
17
|
+
const log: Record<string, ((...args: unknown[]) => void) | undefined>;
|
|
18
|
+
export default log;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module 'stonyx' {
|
|
22
|
+
export function waitForModule(name: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module 'stonyx/test-helpers' {
|
|
26
|
+
export function setupIntegrationTests(hooks: {
|
|
27
|
+
before(fn: () => void | Promise<void>): void;
|
|
28
|
+
after(fn: () => void | Promise<void>): void;
|
|
29
|
+
}): void;
|
|
30
|
+
}
|
package/src/token-manager.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export default class TokenManager {
|
|
2
|
-
constructor(flow) {
|
|
3
|
-
this.flow = flow;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
async getTokens(code) {
|
|
7
|
-
const tokens = await this.flow.exchangeCode(code);
|
|
8
|
-
tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
|
|
9
|
-
return tokens;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async refresh(refreshToken) {
|
|
13
|
-
const tokens = await this.flow.refreshAccessToken(refreshToken);
|
|
14
|
-
tokens.expiresAt = Date.now() + (tokens.expiresIn * 1000);
|
|
15
|
-
return tokens;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async revoke(accessToken) {
|
|
19
|
-
return this.flow.revokeToken(accessToken);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
isExpired(tokenData) {
|
|
23
|
-
if (!tokenData?.expiresAt) return true;
|
|
24
|
-
return Date.now() >= tokenData.expiresAt;
|
|
25
|
-
}
|
|
26
|
-
}
|