feat: implement oauth with remote server
This commit is contained in:
@@ -108,6 +108,12 @@ export class AuthCommand extends Command {
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Exit cleanly after successful authentication
|
||||
// Small delay to ensure all output is flushed
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 100);
|
||||
} catch (error: any) {
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
|
||||
139
package-lock.json
generated
139
package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^3.5.0",
|
||||
"figlet": "^1.8.0",
|
||||
@@ -100,7 +101,6 @@
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^12.1.0",
|
||||
"inquirer": "^9.2.10",
|
||||
"open": "^10.2.0",
|
||||
"ora": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -8948,6 +8948,80 @@
|
||||
"version": "0.0.22",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz",
|
||||
"integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz",
|
||||
"integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "1.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.3.tgz",
|
||||
"integrity": "sha512-rg3DmmZQKEVCreXq6Am29hMVe1CzemXyIWVYyyua69y6XubfP+DzGfLxME/1uvdgwqdoaPbtjBDpEBhqxq1ZwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.4.tgz",
|
||||
"integrity": "sha512-e/FYIWjvQJHOCNACWehnKvg26zosju3694k0NMUNb+JGLdvHJzEa29ZVVLmawd2kvx4hdbv8mxSqfttRnH3+DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.13",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.11.0.tgz",
|
||||
"integrity": "sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.57.0.tgz",
|
||||
"integrity": "sha512-h9ttcL0MY4h+cGqZl95F/RuqccuRBjHU9B7Qqvw0Da+pPK2sUlU1/UdvyqUGj37UsnSphr9pdGfeXjesYkBcyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.71.1",
|
||||
"@supabase/functions-js": "2.4.5",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "1.21.3",
|
||||
"@supabase/realtime-js": "2.15.4",
|
||||
"@supabase/storage-js": "^2.10.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"dev": true,
|
||||
@@ -9344,6 +9418,12 @@
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"dev": true,
|
||||
@@ -9401,6 +9481,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"dev": true,
|
||||
@@ -12834,6 +12923,49 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-expand": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.2.tgz",
|
||||
"integrity": "sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-mono": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-mono/-/dotenv-mono-1.5.1.tgz",
|
||||
"integrity": "sha512-dt7bK/WKQvL0gcdTxjI7wD4MhVR5F4bCk70XMAgnrbWN3fdhpyhWCypYbZalr/vjLURLA7Ib9/VCzazRLJnp1Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"dotenv": "^17.2.0",
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"minimist": "^1.2.8"
|
||||
},
|
||||
"bin": {
|
||||
"dotenv": "dist/cli.js",
|
||||
"dotenv-mono": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-mono/node_modules/dotenv": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
@@ -19593,9 +19725,7 @@
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -25880,7 +26010,6 @@
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -26163,6 +26292,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.57.0",
|
||||
"chalk": "^5.3.0",
|
||||
"open": "^10.2.0",
|
||||
"zod": "^3.22.4"
|
||||
@@ -26171,6 +26301,7 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/node": "^20.11.30",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.4.3",
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^3.5.0",
|
||||
"figlet": "^1.8.0",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.57.0",
|
||||
"chalk": "^5.3.0",
|
||||
"open": "^10.2.0",
|
||||
"zod": "^3.22.4"
|
||||
@@ -34,6 +35,7 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/node": "^20.11.30",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.4.3",
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
AuthConfig
|
||||
} from './types';
|
||||
import { CredentialStore } from './credential-store';
|
||||
import { ApiClient } from './api-client';
|
||||
import { OAuthService } from './oauth-service';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client';
|
||||
|
||||
/**
|
||||
* Authentication manager class
|
||||
@@ -18,17 +18,13 @@ import { OAuthService } from './oauth-service';
|
||||
export class AuthManager {
|
||||
private static instance: AuthManager;
|
||||
private credentialStore: CredentialStore;
|
||||
private apiClient: ApiClient;
|
||||
private oauthService: OAuthService;
|
||||
private supabaseClient: SupabaseAuthClient;
|
||||
|
||||
private constructor(config?: Partial<AuthConfig>) {
|
||||
this.credentialStore = new CredentialStore(config);
|
||||
this.apiClient = new ApiClient(config);
|
||||
this.oauthService = new OAuthService(
|
||||
this.apiClient,
|
||||
this.credentialStore,
|
||||
config
|
||||
);
|
||||
this.supabaseClient = new SupabaseAuthClient();
|
||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,56 +60,24 @@ export class AuthManager {
|
||||
return this.oauthService.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with email and password
|
||||
*/
|
||||
async authenticateWithCredentials(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<AuthCredentials> {
|
||||
try {
|
||||
const response = await this.apiClient.login(email, password);
|
||||
|
||||
// Save authentication data
|
||||
const authData: AuthCredentials = {
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
userId: response.userId,
|
||||
email: email,
|
||||
expiresAt: response.expiresAt,
|
||||
tokenType: 'standard',
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(authData);
|
||||
return authData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with API key
|
||||
* Note: This would require a custom implementation or Supabase RLS policies
|
||||
*/
|
||||
async authenticateWithApiKey(apiKey: string): Promise<AuthCredentials> {
|
||||
try {
|
||||
const response = await this.apiClient.validateApiKey(apiKey);
|
||||
// TODO: Implement API key validation if needed
|
||||
// For now, we can store the API key directly as a token
|
||||
const authData: AuthCredentials = {
|
||||
token: apiKey,
|
||||
tokenType: 'api_key',
|
||||
userId: 'api-user',
|
||||
email: undefined,
|
||||
expiresAt: undefined, // API keys don't expire
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Save authentication data
|
||||
const authData: AuthCredentials = {
|
||||
token: apiKey,
|
||||
tokenType: 'api_key',
|
||||
userId: response.userId,
|
||||
email: response.email,
|
||||
expiresAt: undefined, // API keys don't expire
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(authData);
|
||||
return authData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
this.credentialStore.saveCredentials(authData);
|
||||
return authData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,12 +94,16 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.apiClient.refreshToken(authData.refreshToken);
|
||||
// Use Supabase client to refresh the token
|
||||
const response = await this.supabaseClient.refreshSession(
|
||||
authData.refreshToken
|
||||
);
|
||||
|
||||
// Update authentication data
|
||||
const newAuthData: AuthCredentials = {
|
||||
...authData,
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
expiresAt: response.expiresAt,
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
@@ -6,10 +6,13 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
import { AuthConfig } from './types';
|
||||
|
||||
// Centralized URL configuration - change these for different environments
|
||||
// For production, use: https://tryhamster.com
|
||||
// For local testing, use: http://localhost:8080
|
||||
const BASE_DOMAIN = 'http://localhost:8080'; // 'https://tryhamster.com';
|
||||
// Use build-time value if available, otherwise use runtime env or default
|
||||
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsup's env option
|
||||
// Runtime: TM_BASE_DOMAIN or HAMSTER_BASE_URL from user's environment
|
||||
// Default: https://tryhamster.com for production
|
||||
const BASE_DOMAIN =
|
||||
process.env.TM_PUBLIC_BASE_DOMAIN || // This gets replaced at build time by tsup
|
||||
'https://tryhamster.com';
|
||||
|
||||
/**
|
||||
* Default authentication configuration
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
export { AuthManager } from './auth-manager';
|
||||
export { CredentialStore } from './credential-store';
|
||||
export { ApiClient } from './api-client';
|
||||
export { OAuthService } from './oauth-service';
|
||||
|
||||
export type {
|
||||
|
||||
@@ -6,8 +6,6 @@ import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import crypto from 'crypto';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import open from 'open';
|
||||
import {
|
||||
AuthCredentials,
|
||||
@@ -16,32 +14,26 @@ import {
|
||||
AuthConfig,
|
||||
CliData
|
||||
} from './types';
|
||||
import { ApiClient } from './api-client';
|
||||
import { CredentialStore } from './credential-store';
|
||||
import {
|
||||
getSuccessHtml,
|
||||
getErrorHtml,
|
||||
getSecurityErrorHtml
|
||||
} from './templates';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client';
|
||||
import { getAuthConfig } from './config';
|
||||
import { getLogger } from '../logger';
|
||||
import packageJson from '../../../../package.json' with { type: 'json' };
|
||||
|
||||
export class OAuthService {
|
||||
private logger = getLogger('OAuthService');
|
||||
private apiClient: ApiClient;
|
||||
private credentialStore: CredentialStore;
|
||||
private supabaseClient: SupabaseAuthClient;
|
||||
private webBaseUrl: string;
|
||||
private authorizationUrl: string | null = null;
|
||||
private originalState: string | null = null;
|
||||
|
||||
constructor(
|
||||
apiClient: ApiClient,
|
||||
credentialStore: CredentialStore,
|
||||
config: Partial<AuthConfig> = {}
|
||||
) {
|
||||
this.apiClient = apiClient;
|
||||
this.credentialStore = credentialStore;
|
||||
this.supabaseClient = new SupabaseAuthClient();
|
||||
const authConfig = getAuthConfig(config);
|
||||
this.webBaseUrl = authConfig.webBaseUrl;
|
||||
}
|
||||
@@ -129,7 +121,7 @@ export class OAuthService {
|
||||
// Store the original state for verification
|
||||
this.originalState = state;
|
||||
|
||||
// Prepare CLI data object
|
||||
// Prepare CLI data object (server handles OAuth/PKCE)
|
||||
const cliData: CliData = {
|
||||
callback: callbackUrl,
|
||||
state: state,
|
||||
@@ -211,16 +203,20 @@ export class OAuthService {
|
||||
resolve: (value: AuthCredentials) => void,
|
||||
reject: (error: any) => void
|
||||
): Promise<void> {
|
||||
const code = url.searchParams.get('code');
|
||||
// Server now returns tokens directly instead of code
|
||||
const type = url.searchParams.get('type');
|
||||
const returnedState = url.searchParams.get('state');
|
||||
const accessToken = url.searchParams.get('access_token');
|
||||
const refreshToken = url.searchParams.get('refresh_token');
|
||||
const expiresIn = url.searchParams.get('expires_in');
|
||||
const error = url.searchParams.get('error');
|
||||
const errorDescription = url.searchParams.get('error_description');
|
||||
|
||||
// Send response to browser
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
// Server handles displaying success/failure, just close connection
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
|
||||
if (error) {
|
||||
res.end(getErrorHtml(errorDescription || error));
|
||||
if (!serverClosed) {
|
||||
server.close();
|
||||
reject(
|
||||
@@ -235,7 +231,6 @@ export class OAuthService {
|
||||
|
||||
// Verify state parameter for CSRF protection
|
||||
if (returnedState !== this.originalState) {
|
||||
res.end(getSecurityErrorHtml());
|
||||
if (!serverClosed) {
|
||||
server.close();
|
||||
reject(
|
||||
@@ -245,20 +240,29 @@ export class OAuthService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
res.end(getSuccessHtml());
|
||||
|
||||
// Handle direct token response from server
|
||||
if (
|
||||
accessToken &&
|
||||
(type === 'oauth_success' || type === 'session_transfer')
|
||||
) {
|
||||
try {
|
||||
// Exchange authorization code for tokens
|
||||
const response = await this.apiClient.exchangeCode(code);
|
||||
this.logger.info(`Received tokens via ${type}`);
|
||||
|
||||
// Get user info using the access token if possible
|
||||
const user = await this.supabaseClient.getUser(accessToken);
|
||||
|
||||
// Calculate expiration time
|
||||
const expiresAt = expiresIn
|
||||
? new Date(Date.now() + parseInt(expiresIn) * 1000).toISOString()
|
||||
: undefined;
|
||||
|
||||
// Save authentication data
|
||||
const authData: AuthCredentials = {
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
userId: response.userId,
|
||||
email: response.email,
|
||||
expiresAt: response.expiresAt,
|
||||
token: accessToken,
|
||||
refreshToken: refreshToken || undefined,
|
||||
userId: user?.id || 'unknown',
|
||||
email: user?.email,
|
||||
expiresAt: expiresAt,
|
||||
tokenType: 'standard',
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
@@ -275,6 +279,11 @@ export class OAuthService {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!serverClosed) {
|
||||
server.close();
|
||||
reject(new AuthenticationError('No access token received', 'NO_TOKEN'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
packages/tm-core/src/clients/index.ts
Normal file
5
packages/tm-core/src/clients/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Client exports
|
||||
*/
|
||||
|
||||
export { SupabaseAuthClient } from './supabase-client';
|
||||
154
packages/tm-core/src/clients/supabase-client.ts
Normal file
154
packages/tm-core/src/clients/supabase-client.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Supabase client for authentication
|
||||
*/
|
||||
|
||||
import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
|
||||
import { AuthenticationError } from '../auth/types';
|
||||
import { getLogger } from '../logger';
|
||||
|
||||
export class SupabaseAuthClient {
|
||||
private client: SupabaseClient | null = null;
|
||||
private logger = getLogger('SupabaseAuthClient');
|
||||
|
||||
/**
|
||||
* Initialize Supabase client
|
||||
*/
|
||||
private getClient(): SupabaseClient {
|
||||
if (!this.client) {
|
||||
// Get Supabase configuration from environment
|
||||
const supabaseUrl = process.env.SUPABASE_URL || 'http://localhost:8080';
|
||||
const supabaseAnonKey =
|
||||
process.env.SUPABASE_ANON_KEY ||
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0';
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new AuthenticationError(
|
||||
'Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.',
|
||||
'CONFIG_MISSING'
|
||||
);
|
||||
}
|
||||
|
||||
this.client = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: false, // We handle persistence ourselves
|
||||
detectSessionInUrl: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Code exchange is now handled server-side
|
||||
* The server returns tokens directly to avoid PKCE issues
|
||||
* This method is kept for potential future use
|
||||
*/
|
||||
async exchangeCodeForSession(_code: string): Promise<{
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
userId: string;
|
||||
email?: string;
|
||||
expiresAt?: string;
|
||||
}> {
|
||||
throw new AuthenticationError(
|
||||
'Code exchange is handled server-side. CLI receives tokens directly.',
|
||||
'NOT_SUPPORTED'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an access token
|
||||
*/
|
||||
async refreshSession(refreshToken: string): Promise<{
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: string;
|
||||
}> {
|
||||
try {
|
||||
const client = this.getClient();
|
||||
|
||||
this.logger.info('Refreshing session...');
|
||||
|
||||
// Set the session with refresh token
|
||||
const { data, error } = await client.auth.refreshSession({
|
||||
refresh_token: refreshToken
|
||||
});
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Failed to refresh session:', error);
|
||||
throw new AuthenticationError(
|
||||
`Failed to refresh session: ${error.message}`,
|
||||
'REFRESH_FAILED'
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.session) {
|
||||
throw new AuthenticationError(
|
||||
'No session data returned',
|
||||
'INVALID_RESPONSE'
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info('Successfully refreshed session');
|
||||
|
||||
return {
|
||||
token: data.session.access_token,
|
||||
refreshToken: data.session.refresh_token,
|
||||
expiresAt: data.session.expires_at
|
||||
? new Date(data.session.expires_at * 1000).toISOString()
|
||||
: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
`Failed to refresh session: ${(error as Error).message}`,
|
||||
'REFRESH_FAILED'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user details from token
|
||||
*/
|
||||
async getUser(token: string): Promise<User | null> {
|
||||
try {
|
||||
const client = this.getClient();
|
||||
|
||||
// Get user with the token
|
||||
const { data, error } = await client.auth.getUser(token);
|
||||
|
||||
if (error) {
|
||||
this.logger.warn('Failed to get user:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.user;
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting user:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out (revoke tokens)
|
||||
*/
|
||||
async signOut(token: string): Promise<void> {
|
||||
try {
|
||||
const client = this.getClient();
|
||||
|
||||
// Sign out using the token
|
||||
const { error } = await client.auth.admin.signOut(token);
|
||||
|
||||
if (error) {
|
||||
this.logger.warn('Failed to sign out:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error during sign out:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export interface LoggerConfig {
|
||||
export class Logger {
|
||||
private config: Required<LoggerConfig>;
|
||||
private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = {
|
||||
level: LogLevel.INFO,
|
||||
level: LogLevel.WARN,
|
||||
silent: false,
|
||||
prefix: '',
|
||||
timestamp: false,
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { dotenvLoad } from 'dotenv-mono';
|
||||
dotenvLoad();
|
||||
|
||||
// Get all TM_PUBLIC_* env variables for build-time injection
|
||||
const getBuildTimeEnvs = () => {
|
||||
const envs: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith('TM_PUBLIC_')) {
|
||||
// Return the actual value, not JSON.stringify'd
|
||||
envs[key] = value || '';
|
||||
}
|
||||
}
|
||||
return envs;
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
@@ -20,7 +34,13 @@ export default defineConfig({
|
||||
target: 'es2022',
|
||||
tsconfig: './tsconfig.json',
|
||||
outDir: 'dist',
|
||||
external: ['zod'],
|
||||
// Replace process.env.TM_PUBLIC_* with actual values at build time
|
||||
env: getBuildTimeEnvs(),
|
||||
// Auto-external all dependencies from package.json
|
||||
external: [
|
||||
// External all node_modules - everything not starting with . or /
|
||||
/^[^./]/
|
||||
],
|
||||
esbuildOptions(options) {
|
||||
options.conditions = ['module'];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { dotenvLoad } from 'dotenv-mono';
|
||||
|
||||
// Load .env from root level (monorepo support)
|
||||
dotenvLoad();
|
||||
|
||||
// Get all TM_PUBLIC_* env variables for build-time injection
|
||||
const getBuildTimeEnvs = () => {
|
||||
const envs: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith('TM_PUBLIC_')) {
|
||||
// Return the actual value, not JSON.stringify'd
|
||||
envs[key] = value || '';
|
||||
}
|
||||
}
|
||||
return envs;
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
@@ -18,6 +34,8 @@ export default defineConfig({
|
||||
'.js': 'jsx',
|
||||
'.ts': 'ts'
|
||||
},
|
||||
// Replace process.env.TM_PUBLIC_* with actual values at build time
|
||||
env: getBuildTimeEnvs(),
|
||||
esbuildOptions(options) {
|
||||
options.platform = 'node';
|
||||
// Allow importing TypeScript from JavaScript
|
||||
@@ -25,31 +43,9 @@ export default defineConfig({
|
||||
},
|
||||
// Bundle our monorepo packages but keep node_modules external
|
||||
noExternal: [/@tm\/.*/],
|
||||
external: [
|
||||
// Keep native node modules external
|
||||
'fs',
|
||||
'path',
|
||||
'child_process',
|
||||
'crypto',
|
||||
'os',
|
||||
'url',
|
||||
'util',
|
||||
'stream',
|
||||
'http',
|
||||
'https',
|
||||
'events',
|
||||
'assert',
|
||||
'buffer',
|
||||
'querystring',
|
||||
'readline',
|
||||
'zlib',
|
||||
'tty',
|
||||
'net',
|
||||
'dgram',
|
||||
'dns',
|
||||
'tls',
|
||||
'cluster',
|
||||
'process',
|
||||
'module'
|
||||
]
|
||||
// Don't bundle any other dependencies (auto-external all node_modules)
|
||||
// This regex matches anything that doesn't start with . or /
|
||||
external: [/^[^./]/],
|
||||
// Add success message for debugging
|
||||
onSuccess: 'echo "✅ Build completed successfully"'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user