/home/techb158/cosmic.abdallabala.com/src/services
NameSizeModeActions
dashboardService.js21770666editdlrm
gateWorkflowService.js72960666editdlrm
integrationService.js274610666editdlrm
oauthService.js92820666editdlrm
reportingService.js186700666editdlrm
Edit: /home/techb158/cosmic.abdallabala.com/src/services/oauthService.js (9282B)
const crypto = require("crypto"); const { list, findById, insert, update, audit } = require("../storage/jsonDatabase.js"); const { encryptToken, decryptToken, redactToken, hasEncryptionSecret } = require("../security/tokenVault.js"); const { listConfigs, getConfig, getCredentialStatus, buildAuthorizationUrl, buildTokenRequest } = require("../integrations/oauthProviderConfig.js"); function nowPlusSeconds(seconds) { return new Date(Date.now() + Number(seconds || 0) * 1000).toISOString(); } function stateId() { return crypto.randomBytes(24).toString("hex"); } function safeJson(value) { if (!value) return null; try { return JSON.stringify(value); } catch (_error) { return null; } } function parseJson(value) { if (!value) return null; try { return typeof value === "string" ? JSON.parse(value) : value; } catch (_error) { return null; } } function providerSlug(provider) { return String(provider || "pm").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); } class OAuthService { constructor(database, options = {}) { this.database = database; this.fetchImpl = options.fetchImpl || global.fetch; this.env = options.env || process.env; } listProviders() { return listConfigs().map(config => { const status = getCredentialStatus(config.provider, this.env); return { provider: config.provider, authType: config.authType, scopes: config.scopes, configured: status.configured, tokenAvailable: status.tokenAvailable || this.hasStoredToken(config.provider), encryptionConfigured: status.encryptionConfigured, requiredEnv: status.requiredEnv, optionalEnv: status.optionalEnv, missingRequired: status.missingRequired, redirectUri: status.redirectUri }; }); } getProviderStatus(provider) { const status = getCredentialStatus(provider, this.env); return Object.assign(status, { storedTokenAvailable: this.hasStoredToken(provider) }); } hasStoredToken(provider, integrationId = null) { const db = this.database.read(); return list(db, "oauth_tokens", token => token.provider === provider && (!integrationId || token.integration_id === integrationId) && token.revoked_at == null).length > 0; } buildAuthorizePayload(provider, integrationId, actorUserId = "system") { const state = stateId(); const authorizeUrl = buildAuthorizationUrl(provider, state, this.env); this.database.transaction(db => { const integration = integrationId ? findById(db, "project_management_integrations", integrationId) : null; insert(db, "oauth_states", { provider, integration_id: integrationId || null, project_id: integration ? integration.project_id : null, actor_user_id: actorUserId, state, status: "Pending", expires_at: nowPlusSeconds(600) }, "OAUTHSTATE"); audit(db, { project_id: integration ? integration.project_id : "unknown", actor_user_id: actorUserId, entity_type: "OAuthState", entity_id: state, action: "authorize-url-created", after_json: { provider, integrationId, expiresInSeconds: 600 } }); }); return { provider, integrationId: integrationId || null, authorizeUrl, state, expiresInSeconds: 600 }; } validateState(db, provider, state) { const row = list(db, "oauth_states", item => item.provider === provider && item.state === state)[0]; if (!row) throw new Error("Invalid OAuth state"); if (row.status !== "Pending") throw new Error("OAuth state has already been used"); if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) throw new Error("OAuth state has expired"); return row; } async exchangeAuthorizationCode(provider, code, state) { if (!code) throw new Error("OAuth code is required"); if (!state) throw new Error("OAuth state is required"); const request = buildTokenRequest(provider, code, this.env); if (!this.fetchImpl) throw new Error("Global fetch is not available in this Node.js runtime"); const response = await this.fetchImpl(request.url, { method: "POST", headers: { "Content-Type": request.contentType, "Accept": "application/json" }, body: request.body }); const bodyText = await response.text(); let tokenResponse = {}; try { tokenResponse = bodyText ? JSON.parse(bodyText) : {}; } catch (_error) { tokenResponse = { raw: bodyText }; } if (!response.ok) { throw new Error(`${provider} OAuth token exchange failed with HTTP ${response.status}: ${bodyText.slice(0, 300)}`); } return this.storeOAuthToken(provider, tokenResponse, { state, actorUserId: "oauth-callback" }); } storeTrelloToken(token, state) { if (!token) throw new Error("Trello token is required"); return this.storeOAuthToken("Trello", { access_token: token, token_type: "trello-token" }, { state, actorUserId: "trello-callback" }); } storeOAuthToken(provider, tokenResponse, context = {}) { if (!hasEncryptionSecret(this.env)) { throw new Error("COSMIC_TOKEN_ENCRYPTION_KEY must be set before OAuth tokens can be stored locally"); } const config = getConfig(provider); const accessToken = tokenResponse.access_token || tokenResponse.token; if (!accessToken) throw new Error(`${provider} token response did not include an access token`); const refreshToken = tokenResponse.refresh_token || null; return this.database.transaction(db => { let stateRow = null; if (context.state) { stateRow = this.validateState(db, provider, context.state); update(db, "oauth_states", stateRow.id, { status: "Used", used_at: new Date().toISOString() }); } const integrationId = context.integrationId || (stateRow && stateRow.integration_id) || null; const integration = integrationId ? findById(db, "project_management_integrations", integrationId) : null; const projectId = integration ? integration.project_id : stateRow && stateRow.project_id || "unknown"; const existing = list(db, "oauth_tokens", item => item.provider === provider && item.integration_id === integrationId && item.revoked_at == null)[0]; if (existing) update(db, "oauth_tokens", existing.id, { revoked_at: new Date().toISOString(), status: "Replaced" }); const token = insert(db, "oauth_tokens", { provider, integration_id: integrationId, project_id: projectId, token_type: tokenResponse.token_type || "Bearer", access_token_encrypted: encryptToken(accessToken, this.env), access_token_redacted: redactToken(accessToken), refresh_token_encrypted: refreshToken ? encryptToken(refreshToken, this.env) : null, refresh_token_redacted: refreshToken ? redactToken(refreshToken) : null, expires_at: tokenResponse.expires_in ? nowPlusSeconds(tokenResponse.expires_in) : null, scope: tokenResponse.scope || (config.scopes || []).join(" "), status: "Active", raw_metadata_json: safeJson(Object.assign({}, tokenResponse, { access_token: redactToken(accessToken), refresh_token: refreshToken ? redactToken(refreshToken) : null })), revoked_at: null }, "OAUTHTOKEN"); if (integrationId) { update(db, "project_management_integrations", integrationId, { auth_mode: "OAuth token stored", connection_status: "Connected" }); } audit(db, { project_id: projectId, actor_user_id: context.actorUserId || "system", entity_type: "OAuthToken", entity_id: token.id, action: "store-token", after_json: { provider, integrationId, accessToken: token.access_token_redacted, expiresAt: token.expires_at } }); return this.toTokenView(token); }); } getAccessToken(provider, integrationId = null) { const config = getConfig(provider); if (config.tokenEnv && this.env[config.tokenEnv]) return this.env[config.tokenEnv]; const db = this.database.read(); const token = list(db, "oauth_tokens", item => item.provider === provider && (!integrationId || item.integration_id === integrationId) && item.revoked_at == null) .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))[0]; if (!token || !token.access_token_encrypted) return null; return decryptToken(token.access_token_encrypted, this.env); } listTokens(projectId = null) { const db = this.database.read(); return list(db, "oauth_tokens", token => !projectId || token.project_id === projectId).map(this.toTokenView); } toTokenView(token) { const raw = parseJson(token.raw_metadata_json) || {}; return { id: token.id, provider: token.provider, integrationId: token.integration_id, projectId: token.project_id, tokenType: token.token_type, accessToken: token.access_token_redacted, refreshToken: token.refresh_token_redacted, expiresAt: token.expires_at, scope: token.scope, status: token.status, revokedAt: token.revoked_at, metadata: raw, createdAt: token.created_at, updatedAt: token.updated_at }; } } module.exports = { OAuthService };