/opt/canhelp/apps/api/src/lib
NameSizeModeActions
activity.ts13240644editdlrm
apple-client-secret.ts40730644editdlrm
balance.ts35060644editdlrm
daily-limits.ts42820644editdlrm
email.ts527080644editdlrm
fomo.ts113660644editdlrm
google-oauth.ts105260644editdlrm
helper-capability.ts42070644editdlrm
messaging-rules.ts31230644editdlrm
nameUtils.ts7090644editdlrm
notif.ts189290644editdlrm
plan-history.ts10200644editdlrm
push.ts63860644editdlrm
referral.ts61100644editdlrm
telegram.ts62070644editdlrm
turnstile.ts7410644editdlrm
Edit: /opt/canhelp/apps/api/src/lib/apple-client-secret.ts (4073B)
import { createSign } from 'node:crypto' import { readFileSync } from 'node:fs' const APPLE_AUD = 'https://appleid.apple.com' const DEFAULT_TTL_SECONDS = 60 * 60 * 24 * 170 const REFRESH_WINDOW_SECONDS = 60 * 60 * 24 * 7 function b64url(input: string | Buffer): string { const buff = Buffer.isBuffer(input) ? input : Buffer.from(input) return buff.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') } function parseJwtExp(token: string): number | null { const parts = token.split('.') if (parts.length < 2) return null try { const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8')) return typeof payload?.exp === 'number' ? payload.exp : null } catch { return null } } function shouldRefresh(token: string): boolean { const exp = parseJwtExp(token) if (!exp) return true const now = Math.floor(Date.now() / 1000) return exp - now <= REFRESH_WINDOW_SECONDS } function resolveApplePrivateKey(): string | null { const inline = process.env.APPLE_PRIVATE_KEY?.trim() if (inline) { return inline.includes('\\n') ? inline.replace(/\\n/g, '\n') : inline } const keyPath = process.env.APPLE_PRIVATE_KEY_PATH?.trim() if (!keyPath) return null try { return readFileSync(keyPath, 'utf8') } catch { return null } } function createAppleClientSecret(params: { clientId: string teamId: string keyId: string privateKey: string }): string { const now = Math.floor(Date.now() / 1000) const header = { alg: 'ES256', kid: params.keyId, typ: 'JWT' } const payload = { iss: params.teamId, iat: now, exp: now + DEFAULT_TTL_SECONDS, aud: APPLE_AUD, sub: params.clientId, } const encodedHeader = b64url(JSON.stringify(header)) const encodedPayload = b64url(JSON.stringify(payload)) const signingInput = `${encodedHeader}.${encodedPayload}` const signer = createSign('SHA256') signer.update(signingInput) signer.end() const signature = signer.sign({ key: params.privateKey, dsaEncoding: 'ieee-p1363' }) return `${signingInput}.${b64url(signature)}` } let cachedSecret: string | null = null export function getAppleClientSecret(): string | null { const clientId = process.env.APPLE_CLIENT_ID?.trim() if (!clientId) return null if (cachedSecret && !shouldRefresh(cachedSecret)) { return cachedSecret } const teamId = process.env.APPLE_TEAM_ID?.trim() const keyId = process.env.APPLE_KEY_ID?.trim() const privateKey = resolveApplePrivateKey() if (!teamId || !keyId || !privateKey) { return null } try { const generated = createAppleClientSecret({ clientId, teamId, keyId, privateKey }) cachedSecret = generated return generated } catch (err) { console.error('[Auth] Apple client secret generation failed:', err) return null } } export function isAppleAuthConfigured(): boolean { return Boolean(process.env.APPLE_CLIENT_ID?.trim() && getAppleClientSecret()) } export function getAppleClientSecretExpDebugInfo(): { configured: boolean expUnix: number | null expIso: string | null expiresInSeconds: number | null expiresInDays: number | null refreshWindowSeconds: number } { const token = getAppleClientSecret() if (!token) { return { configured: false, expUnix: null, expIso: null, expiresInSeconds: null, expiresInDays: null, refreshWindowSeconds: REFRESH_WINDOW_SECONDS, } } const expUnix = parseJwtExp(token) if (!expUnix) { return { configured: true, expUnix: null, expIso: null, expiresInSeconds: null, expiresInDays: null, refreshWindowSeconds: REFRESH_WINDOW_SECONDS, } } const now = Math.floor(Date.now() / 1000) const expiresInSeconds = Math.max(0, expUnix - now) return { configured: true, expUnix, expIso: new Date(expUnix * 1000).toISOString(), expiresInSeconds, expiresInDays: Number((expiresInSeconds / 86400).toFixed(2)), refreshWindowSeconds: REFRESH_WINDOW_SECONDS, } }