import crypto from 'crypto' /** * At-rest encryption for Shopify custom-app client id/secret. * * AES-256-GCM with a dedicated env key, mirroring the WooCommerce credential * store (extensions/general/woocommerce/lib/credentials.ts) and the * Skatteverket token store: 12-byte IV, 16-byte auth tag, layout * iv|tag|ciphertext, base64url encoded. The key is deployment-wide (not * per-tenant); what makes rows useless off-server is that * SHOPIFY_CREDENTIALS_ENCRYPTION_KEY never leaves the environment. */ const ALGORITHM = 'aes-256-gcm' /** Whether the integration is configured on this deployment. */ export function isShopifyConfigured(): boolean { return Boolean(process.env.SHOPIFY_CREDENTIALS_ENCRYPTION_KEY) } function getEncryptionKey(): Buffer { const key = process.env.SHOPIFY_CREDENTIALS_ENCRYPTION_KEY if (!key) throw new Error('SHOPIFY_CREDENTIALS_ENCRYPTION_KEY is required') return crypto.createHash('sha256').update(key).digest() } export function encryptCredential(plaintext: string): string { const key = getEncryptionKey() const iv = crypto.randomBytes(12) const cipher = crypto.createCipheriv(ALGORITHM, key, iv) const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) const tag = cipher.getAuthTag() return Buffer.concat([iv, tag, encrypted]).toString('base64url') } export function decryptCredential(ciphertext: string): string { const key = getEncryptionKey() const combined = Buffer.from(ciphertext, 'base64url') const iv = combined.subarray(0, 12) const tag = combined.subarray(12, 28) const encrypted = combined.subarray(28) const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) decipher.setAuthTag(tag) return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8') } /** Decrypted API credentials for an active connection. */ export function credentialsOf(connection: { shop_domain: string client_id_encrypted: string | null client_secret_encrypted: string | null }): { shopDomain: string; clientId: string; clientSecret: string } { if (!connection.client_id_encrypted || !connection.client_secret_encrypted) { throw new Error('Connection has no stored credentials') } return { shopDomain: connection.shop_domain, clientId: decryptCredential(connection.client_id_encrypted), clientSecret: decryptCredential(connection.client_secret_encrypted), } }