@shadowob/oauth 0.4.0 → 1.1.0
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/index.cjs +131 -0
- package/dist/index.d.cts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +104 -0
- package/package.json +24 -4
- package/src/client.ts +0 -157
- package/src/index.ts +0 -7
- package/src/types.ts +0 -28
- package/tsconfig.json +0 -8
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ShadowOAuth: () => ShadowOAuth
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/client.ts
|
|
28
|
+
var DEFAULT_BASE_URL = "https://shadowob.com";
|
|
29
|
+
var ShadowOAuth = class {
|
|
30
|
+
baseUrl;
|
|
31
|
+
clientId;
|
|
32
|
+
clientSecret;
|
|
33
|
+
redirectUri;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
36
|
+
this.clientId = config.clientId;
|
|
37
|
+
this.clientSecret = config.clientSecret;
|
|
38
|
+
this.redirectUri = config.redirectUri;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate the authorization URL to redirect users to Shadow for login.
|
|
42
|
+
*/
|
|
43
|
+
getAuthorizeUrl(options) {
|
|
44
|
+
const scope = options?.scope?.join(" ") ?? "user:read";
|
|
45
|
+
const params = new URLSearchParams({
|
|
46
|
+
response_type: "code",
|
|
47
|
+
client_id: this.clientId,
|
|
48
|
+
redirect_uri: this.redirectUri,
|
|
49
|
+
scope
|
|
50
|
+
});
|
|
51
|
+
if (options?.state) {
|
|
52
|
+
params.set("state", options.state);
|
|
53
|
+
}
|
|
54
|
+
return `${this.baseUrl}/oauth/authorize?${params.toString()}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Exchange an authorization code for access and refresh tokens.
|
|
58
|
+
*/
|
|
59
|
+
async getToken(code) {
|
|
60
|
+
const res = await fetch(`${this.baseUrl}/api/oauth/token`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
grant_type: "authorization_code",
|
|
65
|
+
code,
|
|
66
|
+
client_id: this.clientId,
|
|
67
|
+
client_secret: this.clientSecret,
|
|
68
|
+
redirect_uri: this.redirectUri
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const body = await res.text().catch(() => "");
|
|
73
|
+
throw new Error(`Shadow OAuth token exchange failed (${res.status}): ${body}`);
|
|
74
|
+
}
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
return {
|
|
77
|
+
accessToken: data.access_token,
|
|
78
|
+
refreshToken: data.refresh_token,
|
|
79
|
+
expiresIn: data.expires_in,
|
|
80
|
+
tokenType: data.token_type,
|
|
81
|
+
scope: data.scope
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Refresh an access token using a refresh token.
|
|
86
|
+
*/
|
|
87
|
+
async refreshToken(refreshToken) {
|
|
88
|
+
const res = await fetch(`${this.baseUrl}/api/oauth/token`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
grant_type: "refresh_token",
|
|
93
|
+
refresh_token: refreshToken,
|
|
94
|
+
client_id: this.clientId,
|
|
95
|
+
client_secret: this.clientSecret
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const body = await res.text().catch(() => "");
|
|
100
|
+
throw new Error(`Shadow OAuth token refresh failed (${res.status}): ${body}`);
|
|
101
|
+
}
|
|
102
|
+
const data = await res.json();
|
|
103
|
+
return {
|
|
104
|
+
accessToken: data.access_token,
|
|
105
|
+
refreshToken: data.refresh_token,
|
|
106
|
+
expiresIn: data.expires_in,
|
|
107
|
+
tokenType: data.token_type,
|
|
108
|
+
scope: data.scope
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the authenticated user's information using an access token.
|
|
113
|
+
*/
|
|
114
|
+
async getUser(accessToken) {
|
|
115
|
+
const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `Bearer ${accessToken}`,
|
|
118
|
+
Accept: "application/json"
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
const body = await res.text().catch(() => "");
|
|
123
|
+
throw new Error(`Shadow OAuth userinfo failed (${res.status}): ${body}`);
|
|
124
|
+
}
|
|
125
|
+
return res.json();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
129
|
+
0 && (module.exports = {
|
|
130
|
+
ShadowOAuth
|
|
131
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
interface ShadowOAuthConfig {
|
|
2
|
+
/** Your app's client_id from Shadow Developer Portal */
|
|
3
|
+
clientId: string;
|
|
4
|
+
/** Your app's client_secret (keep server-side only) */
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
/** The redirect URI registered with your app */
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
/** Shadow API base URL (default: https://shadowob.com) */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ShadowOAuthTokens {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
tokenType: string;
|
|
16
|
+
scope: string;
|
|
17
|
+
}
|
|
18
|
+
interface ShadowOAuthUser {
|
|
19
|
+
id: string;
|
|
20
|
+
username: string;
|
|
21
|
+
displayName: string | null;
|
|
22
|
+
avatarUrl: string | null;
|
|
23
|
+
email?: string;
|
|
24
|
+
}
|
|
25
|
+
type ShadowOAuthScope = 'user:read' | 'user:email';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Shadow OAuth SDK client.
|
|
29
|
+
*
|
|
30
|
+
* Use this in your server-side application to implement
|
|
31
|
+
* "Login with Shadow" via the OAuth 2.0 Authorization Code flow.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const oauth = new ShadowOAuth({
|
|
36
|
+
* clientId: 'shadow_xxx',
|
|
37
|
+
* clientSecret: 'shsec_xxx',
|
|
38
|
+
* redirectUri: 'https://myapp.com/callback',
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Step 1: Generate the authorization URL and redirect the user
|
|
42
|
+
* const url = oauth.getAuthorizeUrl({ scope: ['user:read', 'user:email'] })
|
|
43
|
+
*
|
|
44
|
+
* // Step 2: After callback, exchange the code for tokens
|
|
45
|
+
* const tokens = await oauth.getToken(code)
|
|
46
|
+
*
|
|
47
|
+
* // Step 3: Get user info
|
|
48
|
+
* const user = await oauth.getUser(tokens.accessToken)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare class ShadowOAuth {
|
|
52
|
+
private baseUrl;
|
|
53
|
+
private clientId;
|
|
54
|
+
private clientSecret;
|
|
55
|
+
private redirectUri;
|
|
56
|
+
constructor(config: ShadowOAuthConfig);
|
|
57
|
+
/**
|
|
58
|
+
* Generate the authorization URL to redirect users to Shadow for login.
|
|
59
|
+
*/
|
|
60
|
+
getAuthorizeUrl(options?: {
|
|
61
|
+
scope?: ShadowOAuthScope[];
|
|
62
|
+
state?: string;
|
|
63
|
+
}): string;
|
|
64
|
+
/**
|
|
65
|
+
* Exchange an authorization code for access and refresh tokens.
|
|
66
|
+
*/
|
|
67
|
+
getToken(code: string): Promise<ShadowOAuthTokens>;
|
|
68
|
+
/**
|
|
69
|
+
* Refresh an access token using a refresh token.
|
|
70
|
+
*/
|
|
71
|
+
refreshToken(refreshToken: string): Promise<ShadowOAuthTokens>;
|
|
72
|
+
/**
|
|
73
|
+
* Get the authenticated user's information using an access token.
|
|
74
|
+
*/
|
|
75
|
+
getUser(accessToken: string): Promise<ShadowOAuthUser>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { ShadowOAuth, type ShadowOAuthConfig, type ShadowOAuthScope, type ShadowOAuthTokens, type ShadowOAuthUser };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
interface ShadowOAuthConfig {
|
|
2
|
+
/** Your app's client_id from Shadow Developer Portal */
|
|
3
|
+
clientId: string;
|
|
4
|
+
/** Your app's client_secret (keep server-side only) */
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
/** The redirect URI registered with your app */
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
/** Shadow API base URL (default: https://shadowob.com) */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ShadowOAuthTokens {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
tokenType: string;
|
|
16
|
+
scope: string;
|
|
17
|
+
}
|
|
18
|
+
interface ShadowOAuthUser {
|
|
19
|
+
id: string;
|
|
20
|
+
username: string;
|
|
21
|
+
displayName: string | null;
|
|
22
|
+
avatarUrl: string | null;
|
|
23
|
+
email?: string;
|
|
24
|
+
}
|
|
25
|
+
type ShadowOAuthScope = 'user:read' | 'user:email';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Shadow OAuth SDK client.
|
|
29
|
+
*
|
|
30
|
+
* Use this in your server-side application to implement
|
|
31
|
+
* "Login with Shadow" via the OAuth 2.0 Authorization Code flow.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const oauth = new ShadowOAuth({
|
|
36
|
+
* clientId: 'shadow_xxx',
|
|
37
|
+
* clientSecret: 'shsec_xxx',
|
|
38
|
+
* redirectUri: 'https://myapp.com/callback',
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Step 1: Generate the authorization URL and redirect the user
|
|
42
|
+
* const url = oauth.getAuthorizeUrl({ scope: ['user:read', 'user:email'] })
|
|
43
|
+
*
|
|
44
|
+
* // Step 2: After callback, exchange the code for tokens
|
|
45
|
+
* const tokens = await oauth.getToken(code)
|
|
46
|
+
*
|
|
47
|
+
* // Step 3: Get user info
|
|
48
|
+
* const user = await oauth.getUser(tokens.accessToken)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare class ShadowOAuth {
|
|
52
|
+
private baseUrl;
|
|
53
|
+
private clientId;
|
|
54
|
+
private clientSecret;
|
|
55
|
+
private redirectUri;
|
|
56
|
+
constructor(config: ShadowOAuthConfig);
|
|
57
|
+
/**
|
|
58
|
+
* Generate the authorization URL to redirect users to Shadow for login.
|
|
59
|
+
*/
|
|
60
|
+
getAuthorizeUrl(options?: {
|
|
61
|
+
scope?: ShadowOAuthScope[];
|
|
62
|
+
state?: string;
|
|
63
|
+
}): string;
|
|
64
|
+
/**
|
|
65
|
+
* Exchange an authorization code for access and refresh tokens.
|
|
66
|
+
*/
|
|
67
|
+
getToken(code: string): Promise<ShadowOAuthTokens>;
|
|
68
|
+
/**
|
|
69
|
+
* Refresh an access token using a refresh token.
|
|
70
|
+
*/
|
|
71
|
+
refreshToken(refreshToken: string): Promise<ShadowOAuthTokens>;
|
|
72
|
+
/**
|
|
73
|
+
* Get the authenticated user's information using an access token.
|
|
74
|
+
*/
|
|
75
|
+
getUser(accessToken: string): Promise<ShadowOAuthUser>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { ShadowOAuth, type ShadowOAuthConfig, type ShadowOAuthScope, type ShadowOAuthTokens, type ShadowOAuthUser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://shadowob.com";
|
|
3
|
+
var ShadowOAuth = class {
|
|
4
|
+
baseUrl;
|
|
5
|
+
clientId;
|
|
6
|
+
clientSecret;
|
|
7
|
+
redirectUri;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
10
|
+
this.clientId = config.clientId;
|
|
11
|
+
this.clientSecret = config.clientSecret;
|
|
12
|
+
this.redirectUri = config.redirectUri;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Generate the authorization URL to redirect users to Shadow for login.
|
|
16
|
+
*/
|
|
17
|
+
getAuthorizeUrl(options) {
|
|
18
|
+
const scope = options?.scope?.join(" ") ?? "user:read";
|
|
19
|
+
const params = new URLSearchParams({
|
|
20
|
+
response_type: "code",
|
|
21
|
+
client_id: this.clientId,
|
|
22
|
+
redirect_uri: this.redirectUri,
|
|
23
|
+
scope
|
|
24
|
+
});
|
|
25
|
+
if (options?.state) {
|
|
26
|
+
params.set("state", options.state);
|
|
27
|
+
}
|
|
28
|
+
return `${this.baseUrl}/oauth/authorize?${params.toString()}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Exchange an authorization code for access and refresh tokens.
|
|
32
|
+
*/
|
|
33
|
+
async getToken(code) {
|
|
34
|
+
const res = await fetch(`${this.baseUrl}/api/oauth/token`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
grant_type: "authorization_code",
|
|
39
|
+
code,
|
|
40
|
+
client_id: this.clientId,
|
|
41
|
+
client_secret: this.clientSecret,
|
|
42
|
+
redirect_uri: this.redirectUri
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const body = await res.text().catch(() => "");
|
|
47
|
+
throw new Error(`Shadow OAuth token exchange failed (${res.status}): ${body}`);
|
|
48
|
+
}
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
return {
|
|
51
|
+
accessToken: data.access_token,
|
|
52
|
+
refreshToken: data.refresh_token,
|
|
53
|
+
expiresIn: data.expires_in,
|
|
54
|
+
tokenType: data.token_type,
|
|
55
|
+
scope: data.scope
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Refresh an access token using a refresh token.
|
|
60
|
+
*/
|
|
61
|
+
async refreshToken(refreshToken) {
|
|
62
|
+
const res = await fetch(`${this.baseUrl}/api/oauth/token`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
grant_type: "refresh_token",
|
|
67
|
+
refresh_token: refreshToken,
|
|
68
|
+
client_id: this.clientId,
|
|
69
|
+
client_secret: this.clientSecret
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const body = await res.text().catch(() => "");
|
|
74
|
+
throw new Error(`Shadow OAuth token refresh failed (${res.status}): ${body}`);
|
|
75
|
+
}
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
return {
|
|
78
|
+
accessToken: data.access_token,
|
|
79
|
+
refreshToken: data.refresh_token,
|
|
80
|
+
expiresIn: data.expires_in,
|
|
81
|
+
tokenType: data.token_type,
|
|
82
|
+
scope: data.scope
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the authenticated user's information using an access token.
|
|
87
|
+
*/
|
|
88
|
+
async getUser(accessToken) {
|
|
89
|
+
const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${accessToken}`,
|
|
92
|
+
Accept: "application/json"
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const body = await res.text().catch(() => "");
|
|
97
|
+
throw new Error(`Shadow OAuth userinfo failed (${res.status}): ${body}`);
|
|
98
|
+
}
|
|
99
|
+
return res.json();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export {
|
|
103
|
+
ShadowOAuth
|
|
104
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowob/oauth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Shadow OAuth SDK — typed client for integrating Shadow OAuth login into third-party applications",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"require": "./dist/index.cjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
8
10
|
"exports": {
|
|
9
|
-
".":
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"development": "./src/index.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
10
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
11
22
|
"dependencies": {},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.5.0",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
12
30
|
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"dev": "tsup --watch",
|
|
13
33
|
"test": "vitest run",
|
|
14
34
|
"test:watch": "vitest"
|
|
15
35
|
}
|
package/src/client.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ShadowOAuthConfig,
|
|
3
|
-
ShadowOAuthScope,
|
|
4
|
-
ShadowOAuthTokens,
|
|
5
|
-
ShadowOAuthUser,
|
|
6
|
-
} from './types'
|
|
7
|
-
|
|
8
|
-
const DEFAULT_BASE_URL = 'https://shadowob.com'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Shadow OAuth SDK client.
|
|
12
|
-
*
|
|
13
|
-
* Use this in your server-side application to implement
|
|
14
|
-
* "Login with Shadow" via the OAuth 2.0 Authorization Code flow.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* const oauth = new ShadowOAuth({
|
|
19
|
-
* clientId: 'shadow_xxx',
|
|
20
|
-
* clientSecret: 'shsec_xxx',
|
|
21
|
-
* redirectUri: 'https://myapp.com/callback',
|
|
22
|
-
* })
|
|
23
|
-
*
|
|
24
|
-
* // Step 1: Generate the authorization URL and redirect the user
|
|
25
|
-
* const url = oauth.getAuthorizeUrl({ scope: ['user:read', 'user:email'] })
|
|
26
|
-
*
|
|
27
|
-
* // Step 2: After callback, exchange the code for tokens
|
|
28
|
-
* const tokens = await oauth.getToken(code)
|
|
29
|
-
*
|
|
30
|
-
* // Step 3: Get user info
|
|
31
|
-
* const user = await oauth.getUser(tokens.accessToken)
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export class ShadowOAuth {
|
|
35
|
-
private baseUrl: string
|
|
36
|
-
private clientId: string
|
|
37
|
-
private clientSecret: string
|
|
38
|
-
private redirectUri: string
|
|
39
|
-
|
|
40
|
-
constructor(config: ShadowOAuthConfig) {
|
|
41
|
-
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '')
|
|
42
|
-
this.clientId = config.clientId
|
|
43
|
-
this.clientSecret = config.clientSecret
|
|
44
|
-
this.redirectUri = config.redirectUri
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Generate the authorization URL to redirect users to Shadow for login.
|
|
49
|
-
*/
|
|
50
|
-
getAuthorizeUrl(options?: { scope?: ShadowOAuthScope[]; state?: string }): string {
|
|
51
|
-
const scope = options?.scope?.join(' ') ?? 'user:read'
|
|
52
|
-
const params = new URLSearchParams({
|
|
53
|
-
response_type: 'code',
|
|
54
|
-
client_id: this.clientId,
|
|
55
|
-
redirect_uri: this.redirectUri,
|
|
56
|
-
scope,
|
|
57
|
-
})
|
|
58
|
-
if (options?.state) {
|
|
59
|
-
params.set('state', options.state)
|
|
60
|
-
}
|
|
61
|
-
return `${this.baseUrl}/oauth/authorize?${params.toString()}`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Exchange an authorization code for access and refresh tokens.
|
|
66
|
-
*/
|
|
67
|
-
async getToken(code: string): Promise<ShadowOAuthTokens> {
|
|
68
|
-
const res = await fetch(`${this.baseUrl}/api/oauth/token`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
headers: { 'Content-Type': 'application/json' },
|
|
71
|
-
body: JSON.stringify({
|
|
72
|
-
grant_type: 'authorization_code',
|
|
73
|
-
code,
|
|
74
|
-
client_id: this.clientId,
|
|
75
|
-
client_secret: this.clientSecret,
|
|
76
|
-
redirect_uri: this.redirectUri,
|
|
77
|
-
}),
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
if (!res.ok) {
|
|
81
|
-
const body = await res.text().catch(() => '')
|
|
82
|
-
throw new Error(`Shadow OAuth token exchange failed (${res.status}): ${body}`)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const data = (await res.json()) as {
|
|
86
|
-
access_token: string
|
|
87
|
-
refresh_token: string
|
|
88
|
-
expires_in: number
|
|
89
|
-
token_type: string
|
|
90
|
-
scope: string
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
accessToken: data.access_token,
|
|
95
|
-
refreshToken: data.refresh_token,
|
|
96
|
-
expiresIn: data.expires_in,
|
|
97
|
-
tokenType: data.token_type,
|
|
98
|
-
scope: data.scope,
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Refresh an access token using a refresh token.
|
|
104
|
-
*/
|
|
105
|
-
async refreshToken(refreshToken: string): Promise<ShadowOAuthTokens> {
|
|
106
|
-
const res = await fetch(`${this.baseUrl}/api/oauth/token`, {
|
|
107
|
-
method: 'POST',
|
|
108
|
-
headers: { 'Content-Type': 'application/json' },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
grant_type: 'refresh_token',
|
|
111
|
-
refresh_token: refreshToken,
|
|
112
|
-
client_id: this.clientId,
|
|
113
|
-
client_secret: this.clientSecret,
|
|
114
|
-
}),
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
if (!res.ok) {
|
|
118
|
-
const body = await res.text().catch(() => '')
|
|
119
|
-
throw new Error(`Shadow OAuth token refresh failed (${res.status}): ${body}`)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const data = (await res.json()) as {
|
|
123
|
-
access_token: string
|
|
124
|
-
refresh_token: string
|
|
125
|
-
expires_in: number
|
|
126
|
-
token_type: string
|
|
127
|
-
scope: string
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
accessToken: data.access_token,
|
|
132
|
-
refreshToken: data.refresh_token,
|
|
133
|
-
expiresIn: data.expires_in,
|
|
134
|
-
tokenType: data.token_type,
|
|
135
|
-
scope: data.scope,
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Get the authenticated user's information using an access token.
|
|
141
|
-
*/
|
|
142
|
-
async getUser(accessToken: string): Promise<ShadowOAuthUser> {
|
|
143
|
-
const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {
|
|
144
|
-
headers: {
|
|
145
|
-
Authorization: `Bearer ${accessToken}`,
|
|
146
|
-
Accept: 'application/json',
|
|
147
|
-
},
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
if (!res.ok) {
|
|
151
|
-
const body = await res.text().catch(() => '')
|
|
152
|
-
throw new Error(`Shadow OAuth userinfo failed (${res.status}): ${body}`)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return res.json() as Promise<ShadowOAuthUser>
|
|
156
|
-
}
|
|
157
|
-
}
|
package/src/index.ts
DELETED
package/src/types.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export interface ShadowOAuthConfig {
|
|
2
|
-
/** Your app's client_id from Shadow Developer Portal */
|
|
3
|
-
clientId: string
|
|
4
|
-
/** Your app's client_secret (keep server-side only) */
|
|
5
|
-
clientSecret: string
|
|
6
|
-
/** The redirect URI registered with your app */
|
|
7
|
-
redirectUri: string
|
|
8
|
-
/** Shadow API base URL (default: https://shadowob.com) */
|
|
9
|
-
baseUrl?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ShadowOAuthTokens {
|
|
13
|
-
accessToken: string
|
|
14
|
-
refreshToken: string
|
|
15
|
-
expiresIn: number
|
|
16
|
-
tokenType: string
|
|
17
|
-
scope: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ShadowOAuthUser {
|
|
21
|
-
id: string
|
|
22
|
-
username: string
|
|
23
|
-
displayName: string | null
|
|
24
|
-
avatarUrl: string | null
|
|
25
|
-
email?: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type ShadowOAuthScope = 'user:read' | 'user:email'
|