@anmiles/google-api-wrapper 18.0.2 → 19.0.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/.nycrc.json +27 -0
- package/CHANGELOG.md +32 -21
- package/README.md +1 -1
- package/cspell.json +22 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +5 -11
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +5 -6
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +2 -9
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +3 -23
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/credentials/generator.d.ts +4 -0
- package/dist/lib/credentials/generator.d.ts.map +1 -0
- package/dist/lib/credentials/generator.js +60 -0
- package/dist/lib/credentials/generator.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.d.ts.map +1 -0
- package/dist/lib/credentials/index.js +41 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/validator.d.ts +6 -0
- package/dist/lib/credentials/validator.d.ts.map +1 -0
- package/dist/lib/credentials/validator.js +21 -0
- package/dist/lib/credentials/validator.js.map +1 -0
- package/dist/lib/login.d.ts +3 -0
- package/dist/lib/login.d.ts.map +1 -0
- package/dist/lib/login.js +19 -0
- package/dist/lib/login.js.map +1 -0
- package/dist/lib/profiles.d.ts +4 -12
- package/dist/lib/profiles.d.ts.map +1 -1
- package/dist/lib/profiles.js +8 -11
- package/dist/lib/profiles.js.map +1 -1
- package/dist/lib/renderer.d.ts +7 -23
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +26 -20
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/scopes.d.ts +2 -0
- package/dist/lib/scopes.d.ts.map +1 -0
- package/dist/lib/scopes.js +22 -0
- package/dist/lib/scopes.js.map +1 -0
- package/dist/lib/secrets.d.ts +2 -28
- package/dist/lib/secrets.d.ts.map +1 -1
- package/dist/lib/secrets.js +11 -112
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/utils/paths.d.ts +7 -0
- package/dist/lib/utils/paths.d.ts.map +1 -0
- package/dist/lib/{paths.js → utils/paths.js} +6 -8
- package/dist/lib/utils/paths.js.map +1 -0
- package/dist/templates/auth.html +1 -1
- package/dist/templates/index.html +17 -0
- package/dist/templates/page.html +1 -2
- package/dist/templates/script.html +0 -0
- package/{src/templates/css.html → dist/templates/style.html} +2 -2
- package/dist/types/options.d.ts +2 -3
- package/dist/types/options.d.ts.map +1 -1
- package/dist/types/secrets.d.ts +1 -2
- package/dist/types/secrets.d.ts.map +1 -1
- package/eslint.config.mts +43 -0
- package/jest.config.js +9 -9
- package/package.json +40 -30
- package/src/index.ts +3 -2
- package/src/lib/__tests__/__snapshots__/renderer.test.ts.snap +273 -0
- package/src/lib/__tests__/__snapshots__/scopes.test.ts.snap +6 -0
- package/src/lib/__tests__/__snapshots__/secrets.test.ts.snap +38 -0
- package/src/lib/__tests__/api.test.ts +72 -74
- package/src/lib/__tests__/auth.test.ts +38 -114
- package/src/lib/__tests__/login.test.ts +71 -0
- package/src/lib/__tests__/profiles.test.ts +50 -93
- package/src/lib/__tests__/renderer.test.ts +16 -88
- package/src/lib/__tests__/scopes.test.ts +41 -0
- package/src/lib/__tests__/secrets.test.ts +47 -541
- package/src/lib/api.ts +19 -21
- package/src/lib/auth.ts +5 -25
- package/src/lib/credentials/__tests__/generator.test.ts +249 -0
- package/src/lib/credentials/__tests__/index.test.ts +213 -0
- package/src/lib/credentials/__tests__/validator.test.ts +34 -0
- package/src/lib/credentials/generator.ts +70 -0
- package/src/lib/credentials/index.ts +49 -0
- package/src/lib/credentials/validator.ts +24 -0
- package/src/lib/login.ts +22 -0
- package/src/lib/profiles.ts +9 -12
- package/src/lib/renderer.ts +31 -23
- package/src/lib/scopes.ts +18 -0
- package/src/lib/secrets.ts +17 -139
- package/src/lib/utils/paths.ts +30 -0
- package/src/templates/auth.html +1 -1
- package/src/templates/index.html +17 -0
- package/src/templates/page.html +1 -2
- package/src/templates/script.html +0 -0
- package/{dist/templates/css.html → src/templates/style.html} +2 -2
- package/src/types/options.ts +5 -7
- package/src/types/secrets.ts +8 -10
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +0 -5
- package/tsconfig.test.json +1 -1
- package/.eslintignore +0 -2
- package/.eslintrc.js +0 -30
- package/.vscode/settings.json +0 -9
- package/coverage.config.js +0 -8
- package/dist/lib/paths.d.ts +0 -16
- package/dist/lib/paths.d.ts.map +0 -1
- package/dist/lib/paths.js.map +0 -1
- package/src/lib/__tests__/paths.test.ts +0 -77
- package/src/lib/paths.ts +0 -32
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
import { warn } from '@anmiles/logger';
|
|
4
|
+
import type GoogleApis from 'googleapis';
|
|
5
|
+
import { open } from 'out-url';
|
|
6
|
+
import enableDestroy from 'server-destroy';
|
|
7
|
+
|
|
8
|
+
import type { AuthOptions } from '../../types/options';
|
|
9
|
+
import { renderAuth, renderDone } from '../renderer';
|
|
10
|
+
import { getScopes } from '../scopes';
|
|
11
|
+
|
|
12
|
+
const port = 6006;
|
|
13
|
+
const host = `localhost:${port}`;
|
|
14
|
+
const startURI = `http://${host}/`;
|
|
15
|
+
const serverRetryInterval = 1000;
|
|
16
|
+
|
|
17
|
+
export async function generateCredentials(
|
|
18
|
+
profile: string,
|
|
19
|
+
auth: GoogleApis.Auth.OAuth2Client,
|
|
20
|
+
options?: AuthOptions,
|
|
21
|
+
prompt?: GoogleApis.Auth.GenerateAuthUrlOpts['prompt'],
|
|
22
|
+
): Promise<GoogleApis.Auth.Credentials> {
|
|
23
|
+
const scope = options?.scopes ?? getScopes();
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const authUrl = auth.generateAuthUrl({
|
|
27
|
+
access_type: 'offline',
|
|
28
|
+
prompt,
|
|
29
|
+
scope,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const server = http.createServer();
|
|
33
|
+
enableDestroy(server);
|
|
34
|
+
|
|
35
|
+
server.on('request', (request, response) => {
|
|
36
|
+
if (!request.url) {
|
|
37
|
+
response.end('');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = new URL(`http://${request.headers.host}${request.url}`);
|
|
42
|
+
const code = url.searchParams.get('code');
|
|
43
|
+
|
|
44
|
+
if (!code) {
|
|
45
|
+
response.end(renderAuth({ profile, authUrl, scope }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
response.end(renderDone({ profile }));
|
|
50
|
+
server.destroy();
|
|
51
|
+
|
|
52
|
+
void (async () => {
|
|
53
|
+
const { tokens } = await auth.getToken(code);
|
|
54
|
+
resolve(tokens);
|
|
55
|
+
})();
|
|
56
|
+
});
|
|
57
|
+
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
58
|
+
if (error.code === 'EADDRINUSE') {
|
|
59
|
+
setTimeout(() => server.listen(port), serverRetryInterval);
|
|
60
|
+
} else {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.once('listening', () => {
|
|
65
|
+
warn('Please check your browser for further actions');
|
|
66
|
+
void open(startURI);
|
|
67
|
+
});
|
|
68
|
+
server.listen(port);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import type GoogleApis from 'googleapis';
|
|
4
|
+
|
|
5
|
+
import type { AuthOptions } from '../../types/options';
|
|
6
|
+
import { getCredentialsFile } from '../utils/paths';
|
|
7
|
+
|
|
8
|
+
import { generateCredentials } from './generator';
|
|
9
|
+
import { validateCredentials } from './validator';
|
|
10
|
+
|
|
11
|
+
export async function getCredentials(profile: string, auth: GoogleApis.Common.OAuth2Client, options?: AuthOptions): Promise<GoogleApis.Auth.Credentials> {
|
|
12
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
13
|
+
|
|
14
|
+
if (options?.temporary) {
|
|
15
|
+
const credentials = await generateCredentials(profile, auth, options);
|
|
16
|
+
const validationResult = await validateCredentials(credentials);
|
|
17
|
+
|
|
18
|
+
if (!validationResult.isValid) {
|
|
19
|
+
throw new Error(validationResult.validationError);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return credentials;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fs.getJSONAsync(credentialsFile, async () => {
|
|
26
|
+
const refreshToken = fs.existsSync(credentialsFile)
|
|
27
|
+
? fs.readJSON<GoogleApis.Auth.Credentials>(credentialsFile).refresh_token
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
const credentials = await generateCredentials(profile, auth, options,
|
|
31
|
+
refreshToken
|
|
32
|
+
? undefined
|
|
33
|
+
: 'consent',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
refresh_token: refreshToken,
|
|
38
|
+
...credentials,
|
|
39
|
+
};
|
|
40
|
+
}, validateCredentials);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function deleteCredentials(profile: string): void {
|
|
44
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(credentialsFile)) {
|
|
47
|
+
fs.rmSync(credentialsFile);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type GoogleApis from 'googleapis';
|
|
2
|
+
|
|
3
|
+
const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- pass sync function into async context
|
|
6
|
+
export async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Promise<{ isValid: boolean; validationError?: string }> {
|
|
7
|
+
if (!credentials.access_token) {
|
|
8
|
+
return { isValid: false, validationError: 'Credentials does not have access_token' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!credentials.refresh_token) {
|
|
12
|
+
return { isValid: false, validationError: 'Credentials does not have refresh_token' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!credentials.expiry_date) {
|
|
16
|
+
return { isValid: false, validationError: 'Credentials does not have expiry_date' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (new Date().getTime() - credentials.expiry_date >= tokenExpiration) {
|
|
20
|
+
return { isValid: false, validationError: 'Credentials expired' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { isValid: true };
|
|
24
|
+
}
|
package/src/lib/login.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { info, warn } from '@anmiles/logger';
|
|
2
|
+
|
|
3
|
+
import type { AuthOptions, CommonOptions } from '../types/options';
|
|
4
|
+
|
|
5
|
+
import { getAuth } from './auth';
|
|
6
|
+
import { getProfiles } from './profiles';
|
|
7
|
+
|
|
8
|
+
export async function login(profile?: string, options?: AuthOptions & CommonOptions): Promise<void> {
|
|
9
|
+
const profiles = getProfiles().filter((p) => !profile || p === profile);
|
|
10
|
+
|
|
11
|
+
for (const profile of profiles) {
|
|
12
|
+
if (!options?.hideProgress) {
|
|
13
|
+
warn(`${profile} - logging in...`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await getAuth(profile, options);
|
|
17
|
+
|
|
18
|
+
if (!options?.hideProgress) {
|
|
19
|
+
info(`${profile} - logged in successfully`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/lib/profiles.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
|
|
2
3
|
import '@anmiles/prototypes';
|
|
3
|
-
import { getProfilesFile } from './paths';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { getProfilesFile } from './utils/paths';
|
|
6
6
|
|
|
7
|
-
function getProfiles(): string[] {
|
|
7
|
+
export function getProfiles(): string[] {
|
|
8
8
|
const profilesFile = getProfilesFile();
|
|
9
9
|
return fs.getJSON(profilesFile, () => []);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function setProfiles(profiles: string[]): void {
|
|
12
|
+
export function setProfiles(profiles: string[]): void {
|
|
13
13
|
const profilesFile = getProfilesFile();
|
|
14
14
|
fs.writeJSON(profilesFile, profiles);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function createProfile(profile?: string): void {
|
|
17
|
+
export function createProfile(profile?: string): void {
|
|
18
18
|
if (!profile) {
|
|
19
19
|
throw new Error('Usage: `npm run create <profile>` where `profile` - is any profile name you want');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const existingProfiles =
|
|
22
|
+
const existingProfiles = getProfiles();
|
|
23
23
|
|
|
24
24
|
if (existingProfiles.includes(profile)) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
existingProfiles.push(profile);
|
|
29
|
-
|
|
29
|
+
setProfiles(existingProfiles);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function filterProfiles(profile?: string): string[] {
|
|
33
|
-
const existingProfiles =
|
|
32
|
+
export function filterProfiles(profile?: string): string[] {
|
|
33
|
+
const existingProfiles = getProfiles();
|
|
34
34
|
|
|
35
35
|
if (existingProfiles.length === 0) {
|
|
36
36
|
throw new Error('Please `npm run create` at least one profile');
|
|
@@ -46,6 +46,3 @@ function filterProfiles(profile?: string): string[] {
|
|
|
46
46
|
|
|
47
47
|
throw new Error(`Profile '${profile}' does not exist`);
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
export { getProfiles, setProfiles, createProfile, filterProfiles };
|
|
51
|
-
export default { getProfiles, setProfiles, createProfile, filterProfiles };
|
package/src/lib/renderer.ts
CHANGED
|
@@ -1,45 +1,56 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import renderer from './renderer';
|
|
3
|
-
import { getTemplateFile } from './paths';
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { getTemplateFile } from './utils/paths';
|
|
4
|
+
|
|
5
|
+
export const templates = {
|
|
6
|
+
index : [ 'style', 'page', 'script' ] as const,
|
|
7
|
+
page : [ 'content' ] as const,
|
|
8
|
+
style : [ ] as const,
|
|
9
|
+
script: [ ] as const,
|
|
10
|
+
auth : [ 'profile', 'authUrl', 'scopesList' ] as const,
|
|
9
11
|
scope : [ 'type', 'title', 'name' ] as const,
|
|
10
12
|
done : [ 'profile' ] as const,
|
|
11
13
|
} as const;
|
|
12
14
|
|
|
13
15
|
type TemplateName = keyof typeof templates;
|
|
14
16
|
|
|
15
|
-
const allHTML = {} as Record<TemplateName, string>;
|
|
17
|
+
const allHTML = {} as Record<TemplateName, string>; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
16
18
|
|
|
17
|
-
function renderAuth({ profile, authUrl, scope }: { profile
|
|
19
|
+
export function renderAuth({ profile, authUrl, scope }: { profile: string; authUrl: string; scope: string[] }): string {
|
|
18
20
|
const scopesList = scope.map((s) => render('scope', {
|
|
19
|
-
name
|
|
20
|
-
title
|
|
21
|
-
type
|
|
21
|
+
name : s.split('/').pop()!,
|
|
22
|
+
title: s.endsWith('.readonly') ? 'Readonly (cannot change or delete your data)' : 'Writable (can change or delete your data)',
|
|
23
|
+
type : s.endsWith('.readonly') ? 'readonly' : '',
|
|
22
24
|
})).join('\n');
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const style = render('style', {});
|
|
27
|
+
const script = render('script', {});
|
|
25
28
|
const content = render('auth', { profile, authUrl, scopesList });
|
|
26
|
-
|
|
29
|
+
const page = render('page', { content });
|
|
30
|
+
return render('index', { style, page, script });
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
function renderDone({ profile }: { profile
|
|
30
|
-
const
|
|
33
|
+
export function renderDone({ profile }: { profile: string }): string {
|
|
34
|
+
const style = render('style', {});
|
|
35
|
+
const script = render('script', {});
|
|
31
36
|
const content = render('done', { profile });
|
|
32
|
-
|
|
37
|
+
const page = render('page', { content });
|
|
38
|
+
return render('index', { style, page, script });
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
// TODO: Use react
|
|
36
42
|
function render<T extends TemplateName>(templateName: T, values: Record<typeof templates[T][number], string | undefined>): string {
|
|
37
|
-
let html =
|
|
43
|
+
let html = getTemplate(templateName);
|
|
38
44
|
const allValues = values as Record<typeof templates[TemplateName][number], string | undefined>;
|
|
39
45
|
|
|
40
46
|
for (const variable of templates[templateName]) {
|
|
41
|
-
const value = allValues[variable]
|
|
42
|
-
|
|
47
|
+
const value = allValues[variable];
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'undefined') {
|
|
50
|
+
throw new Error(`Missing required value '${variable}' while rendering template '${templateName}'`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
html = html.replaceAll(`\${${variable}}`, value);
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
return html;
|
|
@@ -48,12 +59,9 @@ function render<T extends TemplateName>(templateName: T, values: Record<typeof t
|
|
|
48
59
|
function getTemplate(templateName: TemplateName): string {
|
|
49
60
|
if (!(templateName in allHTML)) {
|
|
50
61
|
const file = getTemplateFile(templateName);
|
|
51
|
-
const template = fs.readFileSync(file).toString();
|
|
62
|
+
const template = fs.readFileSync(file).toString().trim();
|
|
52
63
|
allHTML[templateName] = template;
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
return allHTML[templateName];
|
|
56
67
|
}
|
|
57
|
-
|
|
58
|
-
export { templates, renderAuth, renderDone };
|
|
59
|
-
export default { templates, render, getTemplate, renderAuth, renderDone };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import { getScopesFile } from './utils/paths';
|
|
4
|
+
|
|
5
|
+
export function getScopes(): string[] {
|
|
6
|
+
const scopesFile = getScopesFile();
|
|
7
|
+
const scopes = fs.getJSON<string[]>(scopesFile, () => {
|
|
8
|
+
throw new Error(getScopesError(scopesFile));
|
|
9
|
+
});
|
|
10
|
+
return scopes;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getScopesError(scopesFile: string): string {
|
|
14
|
+
return [
|
|
15
|
+
`File ${scopesFile} not found!`,
|
|
16
|
+
`This application had to have pre-defined file ${scopesFile} that will declare needed scopes`,
|
|
17
|
+
].join('\n');
|
|
18
|
+
}
|
package/src/lib/secrets.ts
CHANGED
|
@@ -1,149 +1,30 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import { open } from 'out-url';
|
|
4
|
-
import enableDestroy from 'server-destroy';
|
|
5
|
-
import type GoogleApis from 'googleapis';
|
|
6
|
-
import { warn } from '@anmiles/logger';
|
|
7
|
-
import type { Secrets } from '../types/secrets';
|
|
8
|
-
import type { AuthOptions } from '../types/options';
|
|
9
|
-
import '@anmiles/prototypes';
|
|
10
|
-
import { getScopesFile, getSecretsFile, getCredentialsFile } from './paths';
|
|
11
|
-
import { renderAuth, renderDone } from './renderer';
|
|
12
|
-
|
|
13
|
-
import secrets from './secrets';
|
|
14
|
-
|
|
15
|
-
const port = 6006;
|
|
16
|
-
const host = `localhost:${port}`;
|
|
17
|
-
const startURI = `http://${host}/`;
|
|
18
|
-
const callbackURI = `http://${host}/oauthcallback`;
|
|
19
|
-
const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
|
|
20
|
-
const serverRetryInterval = 1000;
|
|
21
|
-
|
|
22
|
-
function getScopes(): string[] {
|
|
23
|
-
const scopesFile = getScopesFile();
|
|
24
|
-
const scopes = fs.getJSON<string[]>(scopesFile, () => {
|
|
25
|
-
throw new Error(secrets.getScopesError(scopesFile));
|
|
26
|
-
});
|
|
27
|
-
return scopes;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function getSecrets(profile: string): Secrets {
|
|
31
|
-
const secretsFile = getSecretsFile(profile);
|
|
32
|
-
const secretsObject = fs.getJSON<Secrets>(secretsFile, () => {
|
|
33
|
-
throw new Error(secrets.getSecretsError(profile, secretsFile));
|
|
34
|
-
});
|
|
35
|
-
secrets.checkSecrets(profile, secretsObject, secretsFile);
|
|
36
|
-
return secretsObject;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function getCredentials(profile: string, auth: GoogleApis.Common.OAuth2Client, options?: AuthOptions): Promise<GoogleApis.Auth.Credentials> {
|
|
40
|
-
const credentialsFile = getCredentialsFile(profile);
|
|
41
|
-
|
|
42
|
-
if (options?.temporary) {
|
|
43
|
-
return secrets.createCredentials(profile, auth, options);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return fs.getJSONAsync(credentialsFile, async () => {
|
|
47
|
-
const refreshToken = fs.existsSync(credentialsFile) ? fs.readJSON<GoogleApis.Auth.Credentials>(credentialsFile).refresh_token : undefined;
|
|
48
|
-
const credentials = await secrets.createCredentials(profile, auth, options, refreshToken ? undefined : 'consent');
|
|
49
|
-
return { refresh_token : refreshToken, ...credentials };
|
|
50
|
-
}, secrets.validateCredentials);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- pass sync function into async context
|
|
54
|
-
async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Promise<{ isValid : boolean; validationError? : string }> {
|
|
55
|
-
if (!credentials.access_token) {
|
|
56
|
-
return { isValid : false, validationError : 'Credentials does not have access_token' };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!credentials.refresh_token) {
|
|
60
|
-
return { isValid : false, validationError : 'Credentials does not have refresh_token' };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!credentials.expiry_date) {
|
|
64
|
-
return { isValid : false, validationError : 'Credentials does not have expiry_date' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (new Date().getTime() - credentials.expiry_date >= tokenExpiration) {
|
|
68
|
-
return { isValid : false, validationError : 'Credentials expired' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { isValid : true };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function createCredentials(profile: string, auth: GoogleApis.Auth.OAuth2Client, options?: AuthOptions, prompt?: GoogleApis.Auth.GenerateAuthUrlOpts['prompt']): Promise<GoogleApis.Auth.Credentials> {
|
|
75
|
-
const scope = options?.scopes ?? secrets.getScopes();
|
|
76
|
-
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
const authUrl = auth.generateAuthUrl({
|
|
79
|
-
access_type : 'offline',
|
|
80
|
-
prompt,
|
|
81
|
-
scope,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const server = http.createServer();
|
|
85
|
-
enableDestroy(server);
|
|
86
|
-
|
|
87
|
-
server.on('request', (request, response) => {
|
|
88
|
-
if (!request.url) {
|
|
89
|
-
response.end('');
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
2
|
|
|
93
|
-
|
|
94
|
-
const code = url.searchParams.get('code');
|
|
95
|
-
|
|
96
|
-
if (!code) {
|
|
97
|
-
response.end(renderAuth({ profile, authUrl, scope }));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
response.end(renderDone({ profile }));
|
|
102
|
-
server.destroy();
|
|
103
|
-
|
|
104
|
-
void (async () => {
|
|
105
|
-
const { tokens } = await auth.getToken(code);
|
|
106
|
-
resolve(tokens);
|
|
107
|
-
})();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
111
|
-
if (error.code === 'EADDRINUSE') {
|
|
112
|
-
setTimeout(() => server.listen(port), serverRetryInterval);
|
|
113
|
-
} else {
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
server.once('listening', () => {
|
|
119
|
-
warn('Please check your browser for further actions');
|
|
120
|
-
void open(startURI);
|
|
121
|
-
});
|
|
3
|
+
import '@anmiles/prototypes';
|
|
122
4
|
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
}
|
|
5
|
+
import type { Secrets } from '../types/secrets';
|
|
126
6
|
|
|
127
|
-
|
|
128
|
-
|
|
7
|
+
import { getScopes } from './scopes';
|
|
8
|
+
import { getSecretsFile } from './utils/paths';
|
|
129
9
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
10
|
+
const port = 6006;
|
|
11
|
+
const host = `localhost:${port}`;
|
|
12
|
+
const callbackURI = `http://${host}/oauthcallback`;
|
|
134
13
|
|
|
135
|
-
function checkSecrets(
|
|
14
|
+
function checkSecrets(secretsObject: Secrets): true {
|
|
136
15
|
if (secretsObject.web.redirect_uris[0] === callbackURI) {
|
|
137
16
|
return true;
|
|
138
17
|
}
|
|
139
|
-
throw new Error(`Error in credentials file: redirect URI should be ${callbackURI}
|
|
18
|
+
throw new Error(`Error in credentials file: redirect URI should be ${callbackURI}`);
|
|
140
19
|
}
|
|
141
20
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
21
|
+
export function getSecrets(profile: string): Secrets {
|
|
22
|
+
const secretsFile = getSecretsFile(profile);
|
|
23
|
+
const secretsObject = fs.getJSON<Secrets>(secretsFile, () => {
|
|
24
|
+
throw new Error(getSecretsError(profile, secretsFile));
|
|
25
|
+
});
|
|
26
|
+
checkSecrets(secretsObject);
|
|
27
|
+
return secretsObject;
|
|
147
28
|
}
|
|
148
29
|
|
|
149
30
|
function getSecretsError(profile: string, secretsFile: string): string {
|
|
@@ -166,7 +47,7 @@ function getSecretsError(profile: string, secretsFile: string): string {
|
|
|
166
47
|
'\t\t\t\tSpecify your email as user support email and as developer contact information on the very bottom',
|
|
167
48
|
'\t\t\t\tClick "Save and continue"',
|
|
168
49
|
'\t\t\tClick "Add or remove scopes"',
|
|
169
|
-
`\t\t\t\tAdd scopes: ${
|
|
50
|
+
`\t\t\t\tAdd scopes: ${getScopes().join(',')}`,
|
|
170
51
|
'\t\t\t\tClick "Save and continue"',
|
|
171
52
|
'\t\t\tClick "Add users"',
|
|
172
53
|
'\t\t\t\tAdd your email',
|
|
@@ -182,6 +63,3 @@ function getSecretsError(profile: string, secretsFile: string): string {
|
|
|
182
63
|
'Then start this script again',
|
|
183
64
|
].join('\n');
|
|
184
65
|
}
|
|
185
|
-
|
|
186
|
-
export { getSecrets, getCredentials, deleteCredentials };
|
|
187
|
-
export default { getScopes, getSecrets, getCredentials, validateCredentials, createCredentials, deleteCredentials, checkSecrets, getSecretsError, getScopesError };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import type { templates } from '../renderer';
|
|
4
|
+
|
|
5
|
+
const dirPaths = {
|
|
6
|
+
input : 'input',
|
|
7
|
+
secrets : 'secrets',
|
|
8
|
+
// TODO: Remove this hack after moving to React
|
|
9
|
+
templates: path.relative(process.cwd(), path.join(__dirname, '../../templates')),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function getProfilesFile(): string {
|
|
13
|
+
return path.join(dirPaths.input, 'profiles.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getScopesFile(): string {
|
|
17
|
+
return 'scopes.json';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getSecretsFile(profile: string): string {
|
|
21
|
+
return path.join(dirPaths.secrets, `${profile}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getCredentialsFile(profile: string): string {
|
|
25
|
+
return path.join(dirPaths.secrets, `${profile}.credentials.json`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTemplateFile(templateName: keyof typeof templates): string {
|
|
29
|
+
return path.join(dirPaths.templates, `${templateName}.html`);
|
|
30
|
+
}
|
package/src/templates/auth.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1>Welcome ${profile}!</h1>
|
|
2
2
|
<p>Please authorize:</p>
|
|
3
3
|
<ul>
|
|
4
|
-
|
|
4
|
+
${scopesList}
|
|
5
5
|
</ul>
|
|
6
6
|
<a id="button" href="${authUrl}">Continue</a>
|
|
7
7
|
<script type="text/javascript">document.addEventListener('DOMContentLoaded', function(){ document.getElementById('button').focus(); });</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Google API sign-in'</title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
|
|
10
|
+
${style}
|
|
11
|
+
|
|
12
|
+
${page}
|
|
13
|
+
|
|
14
|
+
${script}
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/src/templates/page.html
CHANGED
|
File without changes
|
package/src/types/options.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
interface CommonOptions {
|
|
2
|
-
hideProgress
|
|
1
|
+
export interface CommonOptions {
|
|
2
|
+
hideProgress?: boolean;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
interface AuthOptions {
|
|
6
|
-
temporary
|
|
7
|
-
scopes
|
|
5
|
+
export interface AuthOptions {
|
|
6
|
+
temporary?: boolean;
|
|
7
|
+
scopes?: string[];
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
export type { CommonOptions, AuthOptions };
|
package/src/types/secrets.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
interface Secrets {
|
|
1
|
+
export interface Secrets {
|
|
2
2
|
web: {
|
|
3
|
-
client_id
|
|
4
|
-
project_id
|
|
5
|
-
auth_uri
|
|
6
|
-
token_uri
|
|
7
|
-
auth_provider_x509_cert_url
|
|
8
|
-
client_secret
|
|
9
|
-
redirect_uris
|
|
3
|
+
client_id: `${string}.apps.googleusercontent.com`;
|
|
4
|
+
project_id: string;
|
|
5
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth';
|
|
6
|
+
token_uri: 'https://oauth2.googleapis.com/token';
|
|
7
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs';
|
|
8
|
+
client_secret: string;
|
|
9
|
+
redirect_uris: string[];
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
export type { Secrets };
|
package/tsconfig.build.json
CHANGED
package/tsconfig.json
CHANGED
package/tsconfig.test.json
CHANGED
package/.eslintignore
DELETED