(function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); return; } root.GongbizStoreAuthIntent = factory(); })(typeof globalThis !== 'undefined' ? globalThis : this, function () { const STORAGE_KEY = 'gongbiz-store-auth-intent'; const STATE_TTL_MS = 10 * 60 * 1000; const SOCIAL_INTENTS = ['kakao', 'naver']; const INTENTS = ['owner'].concat(SOCIAL_INTENTS); const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); const normalizeIntent = (intent) => { const normalized = String(intent || '').toLowerCase(); if (!INTENTS.includes(normalized)) { throw new Error('Unsupported login intent'); } return normalized; }; const normalizeProvider = (provider) => { if (provider == null) return null; const normalized = String(provider).trim().toLowerCase(); return normalized || null; }; const defaultNow = () => Date.now(); const defaultNonce = () => { const cryptoObj = typeof crypto !== 'undefined' ? crypto : null; if (cryptoObj && typeof cryptoObj.randomUUID === 'function') { return cryptoObj.randomUUID(); } if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') { const bytes = new Uint8Array(16); cryptoObj.getRandomValues(bytes); return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); } return `${Date.now()}-${Math.random().toString(36).slice(2)}`; }; const getDefaultStorage = () => { if (typeof window === 'undefined' || !window.sessionStorage) return null; return window.sessionStorage; }; const requireStorage = (storage) => { const resolved = storage || getDefaultStorage(); if (!resolved) { throw new Error('sessionStorage is required'); } return resolved; }; const persistState = ({ storage, nonce, intent, now }) => { requireStorage(storage).setItem( STORAGE_KEY, JSON.stringify({ nonce, intent, createdAt: now, }) ); }; const buildLoginUrl = ({ keycloakBaseUrl, realm, clientId, redirectUri, intent, storage, nonce, now, }) => { const normalizedIntent = normalizeIntent(intent); const state = nonce || defaultNonce(); const createdAt = now == null ? defaultNow() : now; persistState({ storage, nonce: state, intent: normalizedIntent, now: createdAt }); const url = new URL(`${keycloakBaseUrl}/realms/${realm}/protocol/openid-connect/auth`); url.searchParams.set('client_id', clientId); url.searchParams.set('response_type', 'code'); url.searchParams.set('redirect_uri', redirectUri); url.searchParams.set('state', state); url.searchParams.set('scope', 'openid'); // 스토어 로그인은 기존 Keycloak SSO 세션(예: 공비서 원장님 직접 로그인)을 재사용하면 // 소셜 provider claim 없이 cafe24 토큰이 발급될 수 있으므로 매번 명시적으로 재인증시킨다. url.searchParams.set('prompt', 'login'); url.searchParams.set('max_age', '0'); if (normalizedIntent !== 'owner') { url.searchParams.set('kc_idp_hint', normalizedIntent); } return { url: url.toString(), state, intent: normalizedIntent, }; }; const buildLogoutUrl = ({ keycloakBaseUrl, realm, clientId, postLogoutRedirectUri, idTokenHint }) => { const url = new URL(`${keycloakBaseUrl}/realms/${realm}/protocol/openid-connect/logout`); url.searchParams.set('client_id', clientId); url.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri); if (idTokenHint) { url.searchParams.set('id_token_hint', idTokenHint); } return url.toString(); }; const consumeCallbackState = ({ storage, state, now }) => { const resolvedStorage = requireStorage(storage); const raw = resolvedStorage.getItem(STORAGE_KEY); if (raw != null) { resolvedStorage.removeItem(STORAGE_KEY); } if (!state) { return { valid: false, reason: 'STATE_MISSING' }; } if (!raw) { return { valid: false, reason: 'STATE_MISSING' }; } let record; try { record = JSON.parse(raw); } catch (e) { return { valid: false, reason: 'STATE_PARSE_FAILED' }; } if (!record || !hasOwn(record, 'nonce') || !hasOwn(record, 'intent') || !hasOwn(record, 'createdAt')) { return { valid: false, reason: 'STATE_PARSE_FAILED' }; } let intent; try { intent = normalizeIntent(record.intent); } catch (e) { return { valid: false, reason: 'STATE_PARSE_FAILED' }; } if (record.nonce !== state) { return { valid: false, reason: 'STATE_MISMATCH' }; } const checkedAt = now == null ? defaultNow() : now; if (typeof record.createdAt !== 'number' || checkedAt - record.createdAt > STATE_TTL_MS) { return { valid: false, reason: 'STATE_EXPIRED' }; } return { valid: true, intent, state, createdAt: record.createdAt, }; }; const evaluateLoginIntent = ({ intent, provider }) => { const normalizedIntent = normalizeIntent(intent); const normalizedProvider = normalizeProvider(provider); if (normalizedIntent === 'owner') { if (normalizedProvider) { return { allowed: false, reason: 'OWNER_PROVIDER_MISMATCH', }; } return { allowed: true, action: 'OWNER_SESSION', allowSignUp: false, allowCreateNcpstoreSession: true, }; } if (!normalizedProvider) { return { allowed: false, reason: 'PROVIDER_UNKNOWN', }; } if (normalizedProvider !== normalizedIntent) { return { allowed: false, reason: 'PROVIDER_MISMATCH', }; } return { allowed: true, action: 'CONSUMER_SESSION', allowSignUp: true, allowCreateNcpstoreSession: true, provider: normalizedProvider, }; }; return { STORAGE_KEY, STATE_TTL_MS, buildLoginUrl, buildLogoutUrl, consumeCallbackState, evaluateLoginIntent, normalizeProvider, }; });