/**
* Google Cloud Secret Manager 整合
*/
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'
let client: SecretManagerServiceClient | null = null
function getClient(): SecretManagerServiceClient {
if (!client) {
client = new SecretManagerServiceClient()
}
return client
}
/** Secret 快取(避免重複讀取) */
const secretCache = new Map<string, { value: string; expiresAt: number }>()
const CACHE_TTL_MS = 5 * 60 * 1000 // 5 分鐘
/**
* 從 Secret Manager 讀取 secret
*/
export async function getSecret(secretName: string): Promise<string> {
const projectId = process.env.GCP_PROJECT_ID
if (!projectId) {
throw new Error('GCP_PROJECT_ID 環境變數未設定')
}
// 檢查快取
const cached = secretCache.get(secretName)
if (cached && cached.expiresAt > Date.now()) {
return cached.value
}
const name = `projects/${projectId}/secrets/${secretName}/versions/latest`
try {
const [response] = await getClient().accessSecretVersion({ name })
const value = response.payload?.data?.toString() || ''
// 存入快取
secretCache.set(secretName, {
value,
expiresAt: Date.now() + CACHE_TTL_MS,
})
return value
} catch (error) {
throw new Error(`無法讀取 secret ${secretName}: ${error instanceof Error ? error.message : String(error)}`)
}
}
/**
* 批量讀取 secrets
*/
export async function getSecrets(secretNames: string[]): Promise<Record<string, string>> {
const results: Record<string, string> = {}
await Promise.all(
secretNames.map(async (name) => {
results[name] = await getSecret(name)
})
)
return results
}
/** 開發環境下使用環境變數替代 Secret Manager */
export function getEnvOrSecret(envKey: string): string {
const value = process.env[envKey]
if (!value) {
throw new Error(`環境變數 ${envKey} 未設定`)
}
return value
}
/**
* 初始化所有需要的 secrets
* Cloud Run 使用 --set-secrets 會自動將 Secret Manager 的值注入到環境變數
* 所以直接從環境變數讀取即可(開發和生產環境都是如此)
*/
export interface AppSecrets {
// FX-CRM
fxcrmAppId: string
fxcrmAppSecret: string
fxcrmPermanentCode: string
fxcrmCorpId: string
fxcrmDefaultUserId: string
// Odoo
odooUrl: string
odooDb: string
odooApiKey: string
odooUserId: number
// Shopify
shopifyStore: string
shopifyAccessToken: string
// GitHub
githubToken: string
githubOwner: string
}
let cachedSecrets: AppSecrets | null = null
export async function getAppSecrets(): Promise<AppSecrets> {
if (cachedSecrets) {
return cachedSecrets
}
// Cloud Run 使用 --set-secrets 會自動將 secrets 注入到環境變數
// 開發環境也從環境變數讀取,所以統一處理
cachedSecrets = {
fxcrmAppId: getEnvOrSecret('FXCRM_APP_ID'),
fxcrmAppSecret: getEnvOrSecret('FXCRM_APP_SECRET'),
fxcrmPermanentCode: getEnvOrSecret('FXCRM_PERMANENT_CODE'),
fxcrmCorpId: getEnvOrSecret('FXCRM_CORP_ID'),
fxcrmDefaultUserId: getEnvOrSecret('FXCRM_DEFAULT_USER_ID'),
odooUrl: getEnvOrSecret('ODOO_URL'),
odooDb: getEnvOrSecret('ODOO_DB'),
odooApiKey: getEnvOrSecret('ODOO_API_KEY'),
odooUserId: parseInt(getEnvOrSecret('ODOO_USER_ID'), 10),
shopifyStore: getEnvOrSecret('SHOPIFY_STORE'),
shopifyAccessToken: getEnvOrSecret('SHOPIFY_ACCESS_TOKEN'),
githubToken: getEnvOrSecret('GITHUB_TOKEN'),
githubOwner: getEnvOrSecret('GITHUB_OWNER'),
}
return cachedSecrets
}
/** 清除 secrets 快取(用於測試) */
export function clearSecretsCache(): void {
cachedSecrets = null
secretCache.clear()
}