gemix 0.0.2 → 0.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/src/auth/oauth.ts DELETED
@@ -1,194 +0,0 @@
1
- import * as crypto from 'node:crypto';
2
- import * as readline from 'node:readline';
3
- import open from 'open';
4
- import type { TokenData } from '../storage/accounts.js';
5
-
6
- const OAUTH_CLIENT_ID =
7
- '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
8
-
9
- const OAUTH_CLIENT_SECRET = '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';
10
-
11
- const OAUTH_SCOPE = [
12
- 'openid',
13
- 'https://www.googleapis.com/auth/cloud-platform',
14
- 'https://www.googleapis.com/auth/userinfo.email',
15
- 'https://www.googleapis.com/auth/userinfo.profile',
16
- 'https://www.googleapis.com/auth/cclog',
17
- 'https://www.googleapis.com/auth/experimentsandconfigs',
18
- ];
19
-
20
- const REDIRECT_URI = 'https://antigravity.google/oauth-callback';
21
-
22
- function generateCodeVerifier(): string {
23
- return crypto.randomBytes(32).toString('base64url');
24
- }
25
-
26
- function generateCodeChallenge(verifier: string): string {
27
- return crypto.createHash('sha256').update(verifier).digest('base64url');
28
- }
29
-
30
- export interface OAuthResult {
31
- tokens: TokenData;
32
- email: string;
33
- name: string;
34
- picture?: string;
35
- }
36
-
37
- export async function authenticateWithGoogle(): Promise<OAuthResult> {
38
- const state = crypto.randomBytes(32).toString('hex');
39
- const codeVerifier = generateCodeVerifier();
40
- const codeChallenge = generateCodeChallenge(codeVerifier);
41
-
42
- const authParams = new URLSearchParams({
43
- client_id: OAUTH_CLIENT_ID,
44
- response_type: 'code',
45
- redirect_uri: REDIRECT_URI,
46
- scope: OAUTH_SCOPE.join(' '),
47
- access_type: 'offline',
48
- prompt: 'consent',
49
- state,
50
- code_challenge: codeChallenge,
51
- code_challenge_method: 'S256',
52
- });
53
-
54
- const authUrl = `https://accounts.google.com/o/oauth2/auth?${authParams.toString()}`;
55
-
56
- console.log('\nOpening browser for Google authentication...\n');
57
- console.log('After signing in, copy the authorization code from the redirect page.\n');
58
- console.log(`If the browser doesn't open, visit:\n${authUrl}\n`);
59
-
60
- const childProcess = await open(authUrl);
61
- childProcess.on('error', (error) => {
62
- console.error(`Failed to open browser: ${error.message}`);
63
- console.log(`Please visit:\n${authUrl}\n`);
64
- });
65
-
66
- const code = await promptForAuthCode();
67
-
68
- const startTime = Date.now();
69
- const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
70
- method: 'POST',
71
- headers: {
72
- 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
73
- },
74
- body: new URLSearchParams({
75
- client_id: OAUTH_CLIENT_ID,
76
- client_secret: OAUTH_CLIENT_SECRET,
77
- code,
78
- grant_type: 'authorization_code',
79
- redirect_uri: REDIRECT_URI,
80
- code_verifier: codeVerifier,
81
- }),
82
- });
83
-
84
- if (!tokenResponse.ok) {
85
- const errorText = await tokenResponse.text();
86
- throw new Error(`Token exchange failed: ${tokenResponse.status} ${errorText}`);
87
- }
88
-
89
- const tokenPayload = (await tokenResponse.json()) as {
90
- access_token: string;
91
- refresh_token: string;
92
- expires_in: number;
93
- token_type: string;
94
- scope: string;
95
- };
96
-
97
- const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
98
- headers: { Authorization: `Bearer ${tokenPayload.access_token}` },
99
- });
100
-
101
- if (!userInfoResponse.ok) {
102
- throw new Error(`Failed to fetch user info: ${userInfoResponse.status} ${userInfoResponse.statusText}`);
103
- }
104
-
105
- const userInfo = (await userInfoResponse.json()) as {
106
- email: string;
107
- name: string;
108
- picture?: string;
109
- };
110
-
111
- return {
112
- tokens: {
113
- access_token: tokenPayload.access_token,
114
- refresh_token: tokenPayload.refresh_token,
115
- id_token: '',
116
- scope: tokenPayload.scope || OAUTH_SCOPE.join(' '),
117
- token_type: tokenPayload.token_type || 'Bearer',
118
- expiry_date: startTime + tokenPayload.expires_in * 1000,
119
- },
120
- email: userInfo.email,
121
- name: userInfo.name,
122
- picture: userInfo.picture,
123
- };
124
- }
125
-
126
- function promptForAuthCode(): Promise<string> {
127
- return new Promise((resolve, reject) => {
128
- const rl = readline.createInterface({
129
- input: process.stdin,
130
- output: process.stdout,
131
- });
132
-
133
- const authTimeout = 5 * 60 * 1000;
134
- const timeoutId = setTimeout(() => {
135
- rl.close();
136
- reject(new Error('Authentication timed out after 5 minutes.'));
137
- }, authTimeout);
138
-
139
- const onSigint = () => {
140
- clearTimeout(timeoutId);
141
- rl.close();
142
- reject(new Error('Authentication cancelled by user.'));
143
- };
144
- process.on('SIGINT', onSigint);
145
-
146
- rl.question('Paste the authorization code: ', (answer) => {
147
- clearTimeout(timeoutId);
148
- process.removeListener('SIGINT', onSigint);
149
- rl.close();
150
- const code = answer.trim();
151
- if (!code) {
152
- reject(new Error('No authorization code provided.'));
153
- } else {
154
- resolve(code);
155
- }
156
- });
157
- });
158
- }
159
-
160
- export async function refreshTokens(tokens: TokenData): Promise<TokenData | null> {
161
- try {
162
- const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
163
- method: 'POST',
164
- headers: {
165
- 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
166
- },
167
- body: new URLSearchParams({
168
- client_id: OAUTH_CLIENT_ID,
169
- client_secret: OAUTH_CLIENT_SECRET,
170
- refresh_token: tokens.refresh_token,
171
- grant_type: 'refresh_token',
172
- }),
173
- });
174
-
175
- if (!tokenResponse.ok) return null;
176
-
177
- const tokenPayload = (await tokenResponse.json()) as {
178
- access_token: string;
179
- expires_in: number;
180
- token_type: string;
181
- };
182
-
183
- return {
184
- access_token: tokenPayload.access_token,
185
- refresh_token: tokens.refresh_token,
186
- id_token: tokens.id_token || '',
187
- scope: tokens.scope,
188
- token_type: tokenPayload.token_type || 'Bearer',
189
- expiry_date: Date.now() + tokenPayload.expires_in * 1000,
190
- };
191
- } catch {
192
- return null;
193
- }
194
- }
@@ -1,18 +0,0 @@
1
- import { loadAccounts, getActiveUsername } from '../storage/accounts.js';
2
- import { formatLimits } from '../utils/table.js';
3
-
4
- export async function currentCommand(): Promise<void> {
5
- const activeUsername = await getActiveUsername();
6
- const accounts = await loadAccounts();
7
-
8
- if (!activeUsername || !accounts.accounts[activeUsername]) {
9
- console.log('No active account.');
10
- console.log('Use "gemix list" to see available accounts.');
11
- console.log('Use "gemix use <username>" to switch accounts.');
12
- return;
13
- }
14
-
15
- const account = accounts.accounts[activeUsername];
16
- console.log(`Active account: ${activeUsername} (${account.email})`);
17
- console.log(`Limits: ${formatLimits(account.limits)}`);
18
- }
@@ -1,20 +0,0 @@
1
- import { loadAccounts } from '../storage/accounts.js';
2
- import { formatAccountTable, formatAccountTableJson } from '../utils/table.js';
3
-
4
- export async function listCommand(json: boolean = false): Promise<void> {
5
- const accounts = await loadAccounts();
6
- const activeUsername = accounts.active;
7
-
8
- if (Object.keys(accounts.accounts).length === 0) {
9
- console.log('No accounts registered.');
10
- console.log('Run "gemix register <username>" to add an account.');
11
- return;
12
- }
13
-
14
- if (json) {
15
- const result = formatAccountTableJson(accounts.accounts, activeUsername);
16
- console.log(JSON.stringify(result, null, 2));
17
- } else {
18
- console.log(formatAccountTable(accounts.accounts, activeUsername));
19
- }
20
- }
@@ -1,53 +0,0 @@
1
- import { authenticateWithGoogle } from '../auth/oauth.js';
2
- import { addAccount, loadAccounts } from '../storage/accounts.js';
3
- import { validateUsername } from '../utils/validation.js';
4
- import { formatLimits } from '../utils/table.js';
5
-
6
- export async function registerCommand(username: string): Promise<void> {
7
- const validation = validateUsername(username);
8
- if (!validation.valid) {
9
- console.error(`Error: ${validation.error}`);
10
- process.exit(1);
11
- }
12
-
13
- const accounts = await loadAccounts();
14
- if (accounts.accounts[username]) {
15
- console.error(`Error: Account '${username}' already exists.`);
16
- console.error(`Use 'gemix remove ${username}' to remove it first.`);
17
- process.exit(1);
18
- }
19
-
20
- try {
21
- console.log(`Registering account '${username}'...`);
22
- const result = await authenticateWithGoogle();
23
-
24
- await addAccount(
25
- username,
26
- result.email,
27
- result.name,
28
- result.picture,
29
- result.tokens
30
- );
31
-
32
- const isFirstAccount = Object.keys(accounts.accounts).length === 0;
33
-
34
- console.log('');
35
- console.log(`✓ Registered ${username} (${result.email})`);
36
- if (result.picture) {
37
- console.log(` ${result.picture}`);
38
- }
39
- console.log(` Limits: ${formatLimits({ requestsPerDay: 500, tokensPerDay: 1000000 })}`);
40
-
41
- if (isFirstAccount) {
42
- console.log(' Set as active account');
43
- }
44
- console.log('');
45
- } catch (error) {
46
- if (error instanceof Error && error.message.includes('cancelled')) {
47
- console.log('Registration cancelled.');
48
- } else {
49
- console.error('Registration failed:', error instanceof Error ? error.message : error);
50
- }
51
- process.exit(1);
52
- }
53
- }
@@ -1,37 +0,0 @@
1
- import * as readline from 'node:readline';
2
- import { removeAccount, loadAccounts } from '../storage/accounts.js';
3
-
4
- export async function removeCommand(username: string, yes: boolean = false): Promise<void> {
5
- const accounts = await loadAccounts();
6
-
7
- if (!accounts.accounts[username]) {
8
- console.error(`Error: Account '${username}' not found.`);
9
- console.error('Use "gemix list" to see available accounts.');
10
- process.exit(1);
11
- }
12
-
13
- if (!yes) {
14
- const rl = readline.createInterface({
15
- input: process.stdin,
16
- output: process.stdout,
17
- });
18
-
19
- const answer = await new Promise<string>((resolve) => {
20
- rl.question(`Remove account '${username}'? (y/N) `, resolve);
21
- });
22
- rl.close();
23
-
24
- if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
25
- console.log('Removal cancelled.');
26
- return;
27
- }
28
- }
29
-
30
- const success = await removeAccount(username);
31
- if (!success) {
32
- console.error('Error: Failed to remove account.');
33
- process.exit(1);
34
- }
35
-
36
- console.log(`✓ Removed account '${username}'`);
37
- }
@@ -1,25 +0,0 @@
1
- import { switchAccount, loadAccounts } from '../storage/accounts.js';
2
-
3
- export async function useCommand(username: string): Promise<void> {
4
- const accounts = await loadAccounts();
5
-
6
- if (!accounts.accounts[username]) {
7
- console.error(`Error: Account '${username}' not found.`);
8
- console.error('Use "gemix list" to see available accounts.');
9
- process.exit(1);
10
- }
11
-
12
- try {
13
- const success = await switchAccount(username);
14
- if (!success) {
15
- console.error('Error: Failed to switch account.');
16
- process.exit(1);
17
- }
18
-
19
- const account = accounts.accounts[username];
20
- console.log(`✓ Switched to ${username} (${account.email})`);
21
- } catch (error) {
22
- console.error('Error:', error instanceof Error ? error.message : error);
23
- process.exit(1);
24
- }
25
- }
package/src/index.ts DELETED
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import { registerCommand } from './commands/register.js';
5
- import { listCommand } from './commands/list.js';
6
- import { useCommand } from './commands/use.js';
7
- import { currentCommand } from './commands/current.js';
8
- import { removeCommand } from './commands/remove.js';
9
-
10
- const program = new Command();
11
-
12
- program
13
- .name('gemix')
14
- .description('Gemini multi-account CLI - manage multiple Google accounts')
15
- .version('1.0.0');
16
-
17
- program
18
- .command('register')
19
- .description('Register a new Google account')
20
- .argument('<username>', 'Unique username for this account')
21
- .action(async (username: string) => {
22
- await registerCommand(username);
23
- });
24
-
25
- program
26
- .command('list')
27
- .alias('ls')
28
- .description('List all registered accounts')
29
- .option('-j, --json', 'Output in JSON format')
30
- .action(async (options: { json?: boolean }) => {
31
- await listCommand(options.json);
32
- });
33
-
34
- program
35
- .command('use')
36
- .description('Switch to a different account')
37
- .argument('<username>', 'Username of the account to switch to')
38
- .action(async (username: string) => {
39
- await useCommand(username);
40
- });
41
-
42
- program
43
- .command('current')
44
- .description('Show the currently active account')
45
- .action(async () => {
46
- await currentCommand();
47
- });
48
-
49
- program
50
- .command('remove')
51
- .alias('rm')
52
- .description('Remove a registered account')
53
- .argument('<username>', 'Username of the account to remove')
54
- .option('-y, --yes', 'Skip confirmation')
55
- .action(async (username: string, options: { yes?: boolean }) => {
56
- await removeCommand(username, options.yes);
57
- });
58
-
59
- program.parse();
@@ -1,285 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import { execSync } from 'node:child_process';
3
- import {
4
- getAccountsFilePath,
5
- getActiveFilePath,
6
- getAccountTokensPath,
7
- getAccountDir,
8
- ensureGemxDir,
9
- ensureAccountDir,
10
- getGeminiOAuthCredsPath,
11
- getGeminiGoogleAccountsPath,
12
- getAntigravityOAuthTokenPath,
13
- } from './paths.js';
14
- import { refreshTokens } from '../auth/oauth.js';
15
-
16
- export interface AccountLimits {
17
- requestsPerDay: number;
18
- tokensPerDay: number;
19
- }
20
-
21
- export interface AccountInfo {
22
- email: string;
23
- name: string;
24
- picture?: string;
25
- limits: AccountLimits;
26
- created: string;
27
- lastUsed: string | null;
28
- }
29
-
30
- export interface AccountsData {
31
- accounts: Record<string, AccountInfo>;
32
- active: string | null;
33
- }
34
-
35
- export interface TokenData {
36
- access_token: string;
37
- refresh_token: string;
38
- id_token?: string;
39
- scope: string;
40
- token_type: string;
41
- expiry_date: number;
42
- }
43
-
44
- export interface GoogleAccountsData {
45
- active: string;
46
- old: string[];
47
- }
48
-
49
- const DEFAULT_LIMITS: AccountLimits = {
50
- requestsPerDay: 500,
51
- tokensPerDay: 1000000,
52
- };
53
-
54
- export async function loadAccounts(): Promise<AccountsData> {
55
- await ensureGemxDir();
56
- const filePath = getAccountsFilePath();
57
- try {
58
- const content = await fs.readFile(filePath, 'utf-8');
59
- return JSON.parse(content) as AccountsData;
60
- } catch {
61
- return { accounts: {}, active: null };
62
- }
63
- }
64
-
65
- export async function saveAccounts(data: AccountsData): Promise<void> {
66
- await ensureGemxDir();
67
- const filePath = getAccountsFilePath();
68
- await fs.writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
69
- }
70
-
71
- export async function loadTokens(username: string): Promise<TokenData | null> {
72
- const tokensPath = getAccountTokensPath(username);
73
- try {
74
- const content = await fs.readFile(tokensPath, 'utf-8');
75
- return JSON.parse(content) as TokenData;
76
- } catch {
77
- return null;
78
- }
79
- }
80
-
81
- export async function saveTokens(username: string, tokens: TokenData): Promise<void> {
82
- await ensureAccountDir(username);
83
- const tokensPath = getAccountTokensPath(username);
84
- await fs.writeFile(tokensPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
85
- }
86
-
87
- export async function loadGoogleAccounts(): Promise<GoogleAccountsData> {
88
- const filePath = getGeminiGoogleAccountsPath();
89
- try {
90
- const content = await fs.readFile(filePath, 'utf-8');
91
- return JSON.parse(content) as GoogleAccountsData;
92
- } catch {
93
- return { active: '', old: [] };
94
- }
95
- }
96
-
97
- export async function saveGoogleAccounts(data: GoogleAccountsData): Promise<void> {
98
- const filePath = getGeminiGoogleAccountsPath();
99
- const dir = filePath.substring(0, filePath.lastIndexOf('/'));
100
- await fs.mkdir(dir, { recursive: true });
101
- await fs.writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
102
- }
103
-
104
- export async function updateGoogleAccountsActive(email: string): Promise<void> {
105
- const data = await loadGoogleAccounts();
106
-
107
- if (data.active && data.active !== email) {
108
- data.old = [...(data.old || []), data.active];
109
- }
110
-
111
- data.active = email;
112
- await saveGoogleAccounts(data);
113
- }
114
-
115
- export async function copyTokensToAntigravity(tokens: TokenData): Promise<boolean> {
116
- try {
117
- const antigravityPath = getAntigravityOAuthTokenPath();
118
- const dir = antigravityPath.substring(0, antigravityPath.lastIndexOf('/'));
119
- await fs.mkdir(dir, { recursive: true });
120
-
121
- const antigravityData = {
122
- token: {
123
- access_token: tokens.access_token,
124
- token_type: tokens.token_type,
125
- refresh_token: tokens.refresh_token,
126
- expiry: new Date(tokens.expiry_date).toISOString(),
127
- },
128
- auth_method: 'consumer',
129
- };
130
-
131
- await fs.writeFile(antigravityPath, JSON.stringify(antigravityData, null, 2), { mode: 0o600 });
132
- return true;
133
- } catch {
134
- return false;
135
- }
136
- }
137
-
138
- export async function copyTokensToKeychain(tokens: TokenData): Promise<boolean> {
139
- try {
140
- const keychainData = {
141
- token: {
142
- access_token: tokens.access_token,
143
- token_type: tokens.token_type,
144
- refresh_token: tokens.refresh_token,
145
- expiry: new Date(tokens.expiry_date).toISOString(),
146
- },
147
- auth_method: 'consumer',
148
- };
149
-
150
- const password = `go-keyring-base64:${Buffer.from(JSON.stringify(keychainData)).toString('base64')}`;
151
-
152
- try {
153
- execSync(`security delete-generic-password -s "gemini" -a "antigravity" 2>/dev/null`, { stdio: 'ignore' });
154
- } catch {
155
- // Ignore if entry doesn't exist
156
- }
157
-
158
- execSync(`security add-generic-password -s "gemini" -a "antigravity" -w "${password}" -U`, { stdio: 'ignore' });
159
- return true;
160
- } catch {
161
- return false;
162
- }
163
- }
164
-
165
- export async function getActiveUsername(): Promise<string | null> {
166
- const activePath = getActiveFilePath();
167
- try {
168
- return await fs.readFile(activePath, 'utf-8');
169
- } catch {
170
- return null;
171
- }
172
- }
173
-
174
- export async function setActiveUsername(username: string): Promise<void> {
175
- await ensureGemxDir();
176
- const activePath = getActiveFilePath();
177
- await fs.writeFile(activePath, username, { mode: 0o600 });
178
- }
179
-
180
- export async function copyTokensToGemini(username: string): Promise<boolean> {
181
- const tokens = await loadTokens(username);
182
- if (!tokens) return false;
183
-
184
- const geminiPath = getGeminiOAuthCredsPath();
185
- const geminiDir = geminiPath.substring(0, geminiPath.lastIndexOf('/'));
186
- await fs.mkdir(geminiDir, { recursive: true });
187
- await fs.writeFile(geminiPath, JSON.stringify(buildGeminiCredsObject(tokens), null, 2), { mode: 0o600 });
188
- return true;
189
- }
190
-
191
- export async function addAccount(
192
- username: string,
193
- email: string,
194
- name: string,
195
- picture: string | undefined,
196
- tokens: TokenData,
197
- limits: AccountLimits = DEFAULT_LIMITS
198
- ): Promise<void> {
199
- const accounts = await loadAccounts();
200
-
201
- accounts.accounts[username] = {
202
- email,
203
- name,
204
- picture,
205
- limits,
206
- created: new Date().toISOString(),
207
- lastUsed: null,
208
- };
209
-
210
- await saveTokens(username, tokens);
211
- await saveAccounts(accounts);
212
-
213
- if (!accounts.active) {
214
- await setActiveUsername(username);
215
- accounts.active = username;
216
- await saveAccounts(accounts);
217
- await updateGoogleAccountsActive(email);
218
- }
219
- }
220
-
221
- export async function removeAccount(username: string): Promise<boolean> {
222
- const accounts = await loadAccounts();
223
- if (!accounts.accounts[username]) return false;
224
-
225
- delete accounts.accounts[username];
226
-
227
- if (accounts.active === username) {
228
- const remaining = Object.keys(accounts.accounts);
229
- accounts.active = remaining.length > 0 ? remaining[0] : null;
230
- }
231
-
232
- await fs.rm(getAccountDir(username), { recursive: true, force: true });
233
- await saveAccounts(accounts);
234
- return true;
235
- }
236
-
237
- export function buildGeminiCredsObject(tokens: TokenData): Record<string, unknown> {
238
- const creds: Record<string, unknown> = {
239
- access_token: tokens.access_token,
240
- refresh_token: tokens.refresh_token,
241
- scope: tokens.scope,
242
- token_type: tokens.token_type,
243
- expiry_date: tokens.expiry_date,
244
- };
245
- if (tokens.id_token) {
246
- creds.id_token = tokens.id_token;
247
- }
248
- return creds;
249
- }
250
-
251
- export async function switchAccount(username: string): Promise<boolean> {
252
- const accounts = await loadAccounts();
253
- if (!accounts.accounts[username]) return false;
254
-
255
- let tokens = await loadTokens(username);
256
- if (!tokens) return false;
257
-
258
- // Refresh tokens if expired
259
- const now = Date.now();
260
- const fiveMinutes = 5 * 60 * 1000;
261
- if (tokens.expiry_date && tokens.expiry_date - now < fiveMinutes) {
262
- const refreshed = await refreshTokens(tokens);
263
- if (refreshed) {
264
- tokens = refreshed;
265
- await saveTokens(username, tokens);
266
- }
267
- }
268
-
269
- await fs.mkdir(getGeminiOAuthCredsPath().substring(0, getGeminiOAuthCredsPath().lastIndexOf('/')), { recursive: true });
270
- await fs.writeFile(getGeminiOAuthCredsPath(), JSON.stringify(buildGeminiCredsObject(tokens), null, 2), { mode: 0o600 });
271
- await copyTokensToAntigravity(tokens);
272
- await copyTokensToKeychain(tokens);
273
-
274
- await updateGoogleAccountsActive(accounts.accounts[username].email);
275
-
276
- accounts.active = username;
277
- accounts.accounts[username].lastUsed = new Date().toISOString();
278
- await saveAccounts(accounts);
279
- await setActiveUsername(username);
280
- return true;
281
- }
282
-
283
- export function validateUsername(username: string): boolean {
284
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]{1,28}[a-zA-Z0-9])?$/.test(username);
285
- }