@topgrid/grid-license 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Pro license validation runtime
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`@topgrid/grid-license` is the runtime license validation module for
|
|
7
|
+
`@topgrid/grid-license` is the runtime license validation module for topgrid Pro packages.
|
|
8
8
|
All Pro packages (`@topgrid/grid-pro-*`) depend on this module to verify a valid license at runtime.
|
|
9
9
|
|
|
10
10
|
> **Note**: This is an internal package. It is not published to npm separately. You do not need to install it directly — Pro packages include it as a dependency.
|
|
@@ -42,8 +42,8 @@ Without a valid license key, Pro components will render with a watermark overlay
|
|
|
42
42
|
|
|
43
43
|
SEE LICENSE IN EULA
|
|
44
44
|
|
|
45
|
-
Contact [sales@
|
|
45
|
+
Contact [sales@platree.com](mailto:sales@platree.com) to obtain a license key.
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
-
[Documentation](https://
|
|
49
|
+
[Documentation](https://topgrid.platree.com) | [Pricing](https://topgrid.platree.com/pricing)
|
package/dist/index.cjs
CHANGED
|
@@ -181,7 +181,7 @@ function mountPortal() {
|
|
|
181
181
|
if (typeof document === "undefined") return;
|
|
182
182
|
if (_portalContainer !== null) return;
|
|
183
183
|
_portalContainer = document.createElement("div");
|
|
184
|
-
_portalContainer.setAttribute("data-
|
|
184
|
+
_portalContainer.setAttribute("data-topgrid-watermark", "");
|
|
185
185
|
document.body.appendChild(_portalContainer);
|
|
186
186
|
_portalRoot = client.createRoot(_portalContainer);
|
|
187
187
|
renderWatermark();
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/verifySignature.ts","../src/state.ts","../src/setLicenseKey.ts","../src/checkLicense.ts","../src/Watermark.tsx","../src/useLicenseStatus.ts","../src/useWatermarkEnforcement.tsx"],"names":["jsx","useSyncExternalStore","createRoot","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { LicenseStatus } from './types.js';\r\n\r\ninterface KeyPayload {\r\n domain: string;\r\n expiresAt: number; // Unix ms\r\n tier: string;\r\n}\r\n\r\nfunction isKeyPayload(v: unknown): v is KeyPayload {\r\n return (\r\n typeof v === 'object' &&\r\n v !== null &&\r\n typeof (v as Record<string, unknown>)['domain'] === 'string' &&\r\n typeof (v as Record<string, unknown>)['expiresAt'] === 'number' &&\r\n typeof (v as Record<string, unknown>)['tier'] === 'string'\r\n );\r\n}\r\n\r\nfunction base64urlToBytes(s: string): Uint8Array {\r\n const base64 = s.replace(/-/g, '+').replace(/_/g, '/');\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\r\n const binary = atob(padded);\r\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\r\n}\r\n\r\n/** D7: C-32 pure async helper — no React, no DOM side-effects */\r\nexport async function verifySignature(rawKey: string): Promise<LicenseStatus> {\r\n const parts = rawKey.split('.');\r\n if (parts.length !== 3) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const [pubKeyB64, sigB64, payloadB64] = parts;\r\n\r\n let payload: unknown;\r\n try {\r\n payload = JSON.parse(new TextDecoder().decode(base64urlToBytes(payloadB64)));\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!isKeyPayload(payload)) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // Web Crypto API — Ed25519\r\n let cryptoSubtle: SubtleCrypto;\r\n try {\r\n cryptoSubtle = crypto.subtle;\r\n } catch {\r\n // SSR/Node 18 fallback: crypto.subtle 미지원\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n let pubKey: CryptoKey;\r\n try {\r\n pubKey = await cryptoSubtle.importKey(\r\n 'raw',\r\n base64urlToBytes(pubKeyB64),\r\n { name: 'Ed25519' },\r\n false,\r\n ['verify'],\r\n );\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const sigBytes = base64urlToBytes(sigB64);\r\n const msgBytes = base64urlToBytes(payloadB64);\r\n\r\n let sigOk: boolean;\r\n try {\r\n sigOk = await cryptoSubtle.verify('Ed25519', pubKey, sigBytes, msgBytes);\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!sigOk) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // expiry check\r\n const now = Date.now();\r\n if (payload.expiresAt < now) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'expired' } as { reason: 'expired' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n\r\n // domain check — D5: SSR window undefined → skip\r\n let hostname: string | null = null;\r\n if (typeof window !== 'undefined') {\r\n hostname = window.location.hostname;\r\n }\r\n\r\n if (hostname !== null) {\r\n const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1';\r\n if (!isLocalhost && hostname !== payload.domain) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'domain-mismatch' } as { reason: 'domain-mismatch' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n }\r\n\r\n return {\r\n valid: true,\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n}\r\n","import type { LicenseCheckResult, LicenseState, LicenseStatus } from './types.js';\r\n\r\nlet _state: LicenseState | null = null;\r\n\r\ntype LicenseListener = () => void;\r\nconst _listeners = new Set<LicenseListener>();\r\n\r\n// `useSyncExternalStore` REQUIRES the snapshot function to return the same\r\n// reference between calls unless the underlying state has actually changed —\r\n// otherwise React enters an infinite render loop in Strict Mode (React docs:\r\n// \"Do not return a new object from getSnapshot every time\"). Since\r\n// `checkLicense()` allocates a fresh `LicenseCheckResult` on every call, we\r\n// cache the most recent result here and invalidate it whenever `setLicenseState`\r\n// runs (i.e. when the underlying state actually changes).\r\nlet _cachedCheck: LicenseCheckResult | null = null;\r\n\r\nexport function setLicenseState(s: LicenseState): void {\r\n _state = s;\r\n _cachedCheck = null;\r\n _listeners.forEach((l) => l());\r\n}\r\n\r\nexport function getLicenseState(): LicenseStatus {\r\n if (_state === null) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n return _state.status;\r\n}\r\n\r\n/**\r\n * Returns a cached `LicenseCheckResult` — computes via `compute()` only on\r\n * the first call after a state change. Subsequent calls return the same\r\n * reference until `setLicenseState` invalidates the cache.\r\n *\r\n * Used by `useLicenseStatus` (via `useSyncExternalStore`) to satisfy React's\r\n * snapshot-stability requirement.\r\n */\r\nexport function getCachedCheck(\r\n compute: () => LicenseCheckResult,\r\n): LicenseCheckResult {\r\n if (_cachedCheck === null) _cachedCheck = compute();\r\n return _cachedCheck;\r\n}\r\n\r\n/**\r\n * Subscribe to license state changes. Listener is invoked synchronously\r\n * after every `setLicenseState` call. Returns an unsubscribe function.\r\n *\r\n * Used internally by `useLicenseStatus` (via `useSyncExternalStore`) and\r\n * by `useWatermarkEnforcement` (singleton portal re-render trigger).\r\n */\r\nexport function subscribeLicense(listener: LicenseListener): () => void {\r\n _listeners.add(listener);\r\n return () => {\r\n _listeners.delete(listener);\r\n };\r\n}\r\n","import type { LicenseStatus } from './types.js';\r\nimport { verifySignature } from './verifySignature.js';\r\nimport { setLicenseState } from './state.js';\r\n\r\n/**\r\n * Pro 패키지 전역 라이선스 등록 API.\r\n * 앱 entry(main.tsx / App.tsx)에서 1회 호출.\r\n * @param key - Base64url(pubKey).Base64url(sig).Base64url(payload) 형식 라이선스 키\r\n * @returns LicenseStatus — 즉시 반환 (동기 wrapper, 내부 비동기 검증 완료 후 상태 갱신)\r\n *\r\n * 주의: 반환값은 Promise 없이 즉시 사용 가능하도록 동기 API로 설계.\r\n * 내부적으로 verifySignature (async) 결과를 저장. 비동기 완료 전 getLicenseState() 호출 시\r\n * 기본값 {valid:false, reason:'invalid'} 반환 (D6).\r\n */\r\nexport function setLicenseKey(key: string): LicenseStatus {\r\n // 기본값 초기화 (D6: 검증 완료 전 getLicenseState 호출 대비)\r\n const pending: LicenseStatus = { valid: false };\r\n\r\n // 비동기 검증 시작 (fire-and-forget, 결과는 state에 저장)\r\n verifySignature(key).then((status) => {\r\n setLicenseState({ status, rawKey: key, setAt: Date.now() });\r\n }).catch(() => {\r\n setLicenseState({\r\n status: { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) },\r\n rawKey: key,\r\n setAt: Date.now(),\r\n });\r\n });\r\n\r\n return pending;\r\n}\r\n","// checkLicense.ts\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { getLicenseState } from './state.js';\r\n\r\nconst SIXTY_DAYS_MS = 60 * 24 * 3600 * 1000;\r\nlet warned = false;\r\n\r\n/**\r\n * 현재 라이선스 상태를 동기 검사하여 `LicenseCheckResult`를 반환한다.\r\n *\r\n * - valid=false 이면 `watermarkRequired=true`.\r\n * - 유효하고 `expiresAt`까지 60일 미만이면 `expiryWarning='soon-expiring'` + `console.warn` (1회).\r\n * - 유효하고 만료 여유가 충분하면 `{ valid: true, watermarkRequired: false }`.\r\n */\r\nexport function checkLicense(): LicenseCheckResult {\r\n const status = getLicenseState(); // LicenseStatus (sync)\r\n\r\n if (!status.valid) {\r\n const result: LicenseCheckResult = { valid: false, watermarkRequired: true };\r\n if (status.reason !== undefined) result.reason = status.reason;\r\n if (status.expiresAt !== undefined) result.expiresAt = status.expiresAt;\r\n return result;\r\n }\r\n\r\n if (status.expiresAt !== undefined) {\r\n const msLeft = status.expiresAt.getTime() - Date.now();\r\n if (msLeft < SIXTY_DAYS_MS) {\r\n if (!warned) {\r\n console.warn(\r\n `[grid-license] 라이선스가 ${Math.ceil(msLeft / (24 * 3600 * 1000))}일 후 만료됩니다.`\r\n );\r\n warned = true;\r\n }\r\n return {\r\n valid: true,\r\n watermarkRequired: false,\r\n expiryWarning: 'soon-expiring',\r\n expiresAt: status.expiresAt,\r\n };\r\n }\r\n }\r\n\r\n return { valid: true, watermarkRequired: false };\r\n}\r\n","// Watermark.tsx\r\nimport React from 'react';\r\n\r\ninterface WatermarkProps {\r\n required: boolean;\r\n}\r\n\r\n/**\r\n * Pro 라이선스가 없을 때 그리드 위에 표시되는 워터마크 컴포넌트.\r\n *\r\n * `required=false` 이면 `null` 반환 (렌더링 없음).\r\n */\r\nexport function Watermark({ required }: WatermarkProps): React.ReactElement | null {\r\n if (!required) return null;\r\n return (\r\n <div className=\"absolute top-0 right-0 opacity-40 pointer-events-none select-none text-sm font-semibold text-gray-500 p-2\">\r\n Unlicensed @topgrid/grid\r\n </div>\r\n );\r\n}\r\n","import { useSyncExternalStore } from 'react';\r\n\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { getCachedCheck, subscribeLicense } from './state.js';\r\n\r\n// `useSyncExternalStore` requires `getSnapshot` to return the same reference\r\n// across calls unless the underlying state actually changed; otherwise React\r\n// throws \"The result of getSnapshot should be cached to avoid an infinite\r\n// loop\" in Strict Mode. We delegate to `getCachedCheck`, which memoises until\r\n// `setLicenseState` invalidates the cache.\r\nconst getSnapshot = (): LicenseCheckResult => getCachedCheck(checkLicense);\r\n\r\n/**\r\n * React hook returning the current license check result. Re-renders when the\r\n * license state changes (e.g. async `setLicenseKey` resolution).\r\n *\r\n * Backed by `useSyncExternalStore` — no tearing under React 18 concurrent mode.\r\n *\r\n * @example\r\n * ```tsx\r\n * function MyGrid() {\r\n * const lic = useLicenseStatus();\r\n * return (\r\n * <div className=\"relative\">\r\n * <table>{ ... }</table>\r\n * {lic.watermarkRequired && <Watermark required />}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useLicenseStatus(): LicenseCheckResult {\r\n return useSyncExternalStore(subscribeLicense, getSnapshot, getSnapshot);\r\n}\r\n","import { useEffect } from 'react';\r\nimport { createRoot, type Root } from 'react-dom/client';\r\n\r\nimport { Watermark } from './Watermark.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { subscribeLicense } from './state.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Module-level singleton state.\r\n//\r\n// Multiple components calling `useWatermarkEnforcement()` (e.g. 500\r\n// `<DataMapCell>` instances) only mount ONE portal at `document.body`. The\r\n// ref-count tracks active subscribers — when it returns to 0, the portal is\r\n// torn down.\r\n//\r\n// Re-renders are driven by `subscribeLicense` — when `setLicenseKey` resolves\r\n// and flips `watermarkRequired`, the singleton React root re-renders.\r\n// ---------------------------------------------------------------------------\r\n\r\nlet _activeCount = 0;\r\nlet _portalContainer: HTMLDivElement | null = null;\r\nlet _portalRoot: Root | null = null;\r\nlet _unsubLicense: (() => void) | null = null;\r\n\r\nfunction renderWatermark(): void {\r\n if (_portalRoot === null || typeof document === 'undefined') return;\r\n const lic = checkLicense();\r\n _portalRoot.render(lic.watermarkRequired ? <Watermark required /> : null);\r\n}\r\n\r\nfunction mountPortal(): void {\r\n if (typeof document === 'undefined') return;\r\n if (_portalContainer !== null) return; // already mounted\r\n _portalContainer = document.createElement('div');\r\n _portalContainer.setAttribute('data-tomis-watermark', '');\r\n document.body.appendChild(_portalContainer);\r\n _portalRoot = createRoot(_portalContainer);\r\n renderWatermark();\r\n _unsubLicense = subscribeLicense(renderWatermark);\r\n}\r\n\r\nfunction unmountPortal(): void {\r\n if (_unsubLicense !== null) _unsubLicense();\r\n if (_portalRoot !== null) _portalRoot.unmount();\r\n if (_portalContainer !== null && _portalContainer.parentNode !== null) {\r\n _portalContainer.parentNode.removeChild(_portalContainer);\r\n }\r\n _portalRoot = null;\r\n _portalContainer = null;\r\n _unsubLicense = null;\r\n}\r\n\r\n/**\r\n * Void registration hook for license watermark enforcement via a singleton\r\n * portal mounted at `document.body`.\r\n *\r\n * - Each mount increments a module-level ref-count.\r\n * - First mount creates the singleton portal + React root.\r\n * - License state changes (`setLicenseKey`) re-render the portal via\r\n * `subscribeLicense`.\r\n * - Last unmount (ref-count → 0) tears down the portal.\r\n *\r\n * Use case: per-cell renderers (e.g. `DataMapCell`) where the component\r\n * itself has no host DOM suitable for wrapper-based watermarking.\r\n *\r\n * SSR-safe: portal setup is skipped when `document` is undefined.\r\n *\r\n * @example\r\n * ```tsx\r\n * export function DataMapCell(info) {\r\n * useWatermarkEnforcement(); // void — no return value\r\n * return <span>{...}</span>;\r\n * }\r\n * ```\r\n */\r\nexport function useWatermarkEnforcement(): void {\r\n useEffect(() => {\r\n _activeCount += 1;\r\n if (_activeCount === 1) mountPortal();\r\n return () => {\r\n _activeCount = Math.max(0, _activeCount - 1);\r\n if (_activeCount === 0) unmountPortal();\r\n };\r\n }, []);\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/verifySignature.ts","../src/state.ts","../src/setLicenseKey.ts","../src/checkLicense.ts","../src/Watermark.tsx","../src/useLicenseStatus.ts","../src/useWatermarkEnforcement.tsx"],"names":["jsx","useSyncExternalStore","createRoot","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { LicenseStatus } from './types.js';\r\n\r\ninterface KeyPayload {\r\n domain: string;\r\n expiresAt: number; // Unix ms\r\n tier: string;\r\n}\r\n\r\nfunction isKeyPayload(v: unknown): v is KeyPayload {\r\n return (\r\n typeof v === 'object' &&\r\n v !== null &&\r\n typeof (v as Record<string, unknown>)['domain'] === 'string' &&\r\n typeof (v as Record<string, unknown>)['expiresAt'] === 'number' &&\r\n typeof (v as Record<string, unknown>)['tier'] === 'string'\r\n );\r\n}\r\n\r\nfunction base64urlToBytes(s: string): Uint8Array {\r\n const base64 = s.replace(/-/g, '+').replace(/_/g, '/');\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\r\n const binary = atob(padded);\r\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\r\n}\r\n\r\n/** D7: C-32 pure async helper — no React, no DOM side-effects */\r\nexport async function verifySignature(rawKey: string): Promise<LicenseStatus> {\r\n const parts = rawKey.split('.');\r\n if (parts.length !== 3) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const [pubKeyB64, sigB64, payloadB64] = parts;\r\n\r\n let payload: unknown;\r\n try {\r\n payload = JSON.parse(new TextDecoder().decode(base64urlToBytes(payloadB64)));\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!isKeyPayload(payload)) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // Web Crypto API — Ed25519\r\n let cryptoSubtle: SubtleCrypto;\r\n try {\r\n cryptoSubtle = crypto.subtle;\r\n } catch {\r\n // SSR/Node 18 fallback: crypto.subtle 미지원\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n let pubKey: CryptoKey;\r\n try {\r\n pubKey = await cryptoSubtle.importKey(\r\n 'raw',\r\n base64urlToBytes(pubKeyB64),\r\n { name: 'Ed25519' },\r\n false,\r\n ['verify'],\r\n );\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const sigBytes = base64urlToBytes(sigB64);\r\n const msgBytes = base64urlToBytes(payloadB64);\r\n\r\n let sigOk: boolean;\r\n try {\r\n sigOk = await cryptoSubtle.verify('Ed25519', pubKey, sigBytes, msgBytes);\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!sigOk) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // expiry check\r\n const now = Date.now();\r\n if (payload.expiresAt < now) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'expired' } as { reason: 'expired' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n\r\n // domain check — D5: SSR window undefined → skip\r\n let hostname: string | null = null;\r\n if (typeof window !== 'undefined') {\r\n hostname = window.location.hostname;\r\n }\r\n\r\n if (hostname !== null) {\r\n const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1';\r\n if (!isLocalhost && hostname !== payload.domain) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'domain-mismatch' } as { reason: 'domain-mismatch' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n }\r\n\r\n return {\r\n valid: true,\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n}\r\n","import type { LicenseCheckResult, LicenseState, LicenseStatus } from './types.js';\r\n\r\nlet _state: LicenseState | null = null;\r\n\r\ntype LicenseListener = () => void;\r\nconst _listeners = new Set<LicenseListener>();\r\n\r\n// `useSyncExternalStore` REQUIRES the snapshot function to return the same\r\n// reference between calls unless the underlying state has actually changed —\r\n// otherwise React enters an infinite render loop in Strict Mode (React docs:\r\n// \"Do not return a new object from getSnapshot every time\"). Since\r\n// `checkLicense()` allocates a fresh `LicenseCheckResult` on every call, we\r\n// cache the most recent result here and invalidate it whenever `setLicenseState`\r\n// runs (i.e. when the underlying state actually changes).\r\nlet _cachedCheck: LicenseCheckResult | null = null;\r\n\r\nexport function setLicenseState(s: LicenseState): void {\r\n _state = s;\r\n _cachedCheck = null;\r\n _listeners.forEach((l) => l());\r\n}\r\n\r\nexport function getLicenseState(): LicenseStatus {\r\n if (_state === null) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n return _state.status;\r\n}\r\n\r\n/**\r\n * Returns a cached `LicenseCheckResult` — computes via `compute()` only on\r\n * the first call after a state change. Subsequent calls return the same\r\n * reference until `setLicenseState` invalidates the cache.\r\n *\r\n * Used by `useLicenseStatus` (via `useSyncExternalStore`) to satisfy React's\r\n * snapshot-stability requirement.\r\n */\r\nexport function getCachedCheck(\r\n compute: () => LicenseCheckResult,\r\n): LicenseCheckResult {\r\n if (_cachedCheck === null) _cachedCheck = compute();\r\n return _cachedCheck;\r\n}\r\n\r\n/**\r\n * Subscribe to license state changes. Listener is invoked synchronously\r\n * after every `setLicenseState` call. Returns an unsubscribe function.\r\n *\r\n * Used internally by `useLicenseStatus` (via `useSyncExternalStore`) and\r\n * by `useWatermarkEnforcement` (singleton portal re-render trigger).\r\n */\r\nexport function subscribeLicense(listener: LicenseListener): () => void {\r\n _listeners.add(listener);\r\n return () => {\r\n _listeners.delete(listener);\r\n };\r\n}\r\n","import type { LicenseStatus } from './types.js';\r\nimport { verifySignature } from './verifySignature.js';\r\nimport { setLicenseState } from './state.js';\r\n\r\n/**\r\n * Pro 패키지 전역 라이선스 등록 API.\r\n * 앱 entry(main.tsx / App.tsx)에서 1회 호출.\r\n * @param key - Base64url(pubKey).Base64url(sig).Base64url(payload) 형식 라이선스 키\r\n * @returns LicenseStatus — 즉시 반환 (동기 wrapper, 내부 비동기 검증 완료 후 상태 갱신)\r\n *\r\n * 주의: 반환값은 Promise 없이 즉시 사용 가능하도록 동기 API로 설계.\r\n * 내부적으로 verifySignature (async) 결과를 저장. 비동기 완료 전 getLicenseState() 호출 시\r\n * 기본값 {valid:false, reason:'invalid'} 반환 (D6).\r\n */\r\nexport function setLicenseKey(key: string): LicenseStatus {\r\n // 기본값 초기화 (D6: 검증 완료 전 getLicenseState 호출 대비)\r\n const pending: LicenseStatus = { valid: false };\r\n\r\n // 비동기 검증 시작 (fire-and-forget, 결과는 state에 저장)\r\n verifySignature(key).then((status) => {\r\n setLicenseState({ status, rawKey: key, setAt: Date.now() });\r\n }).catch(() => {\r\n setLicenseState({\r\n status: { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) },\r\n rawKey: key,\r\n setAt: Date.now(),\r\n });\r\n });\r\n\r\n return pending;\r\n}\r\n","// checkLicense.ts\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { getLicenseState } from './state.js';\r\n\r\nconst SIXTY_DAYS_MS = 60 * 24 * 3600 * 1000;\r\nlet warned = false;\r\n\r\n/**\r\n * 현재 라이선스 상태를 동기 검사하여 `LicenseCheckResult`를 반환한다.\r\n *\r\n * - valid=false 이면 `watermarkRequired=true`.\r\n * - 유효하고 `expiresAt`까지 60일 미만이면 `expiryWarning='soon-expiring'` + `console.warn` (1회).\r\n * - 유효하고 만료 여유가 충분하면 `{ valid: true, watermarkRequired: false }`.\r\n */\r\nexport function checkLicense(): LicenseCheckResult {\r\n const status = getLicenseState(); // LicenseStatus (sync)\r\n\r\n if (!status.valid) {\r\n const result: LicenseCheckResult = { valid: false, watermarkRequired: true };\r\n if (status.reason !== undefined) result.reason = status.reason;\r\n if (status.expiresAt !== undefined) result.expiresAt = status.expiresAt;\r\n return result;\r\n }\r\n\r\n if (status.expiresAt !== undefined) {\r\n const msLeft = status.expiresAt.getTime() - Date.now();\r\n if (msLeft < SIXTY_DAYS_MS) {\r\n if (!warned) {\r\n console.warn(\r\n `[grid-license] 라이선스가 ${Math.ceil(msLeft / (24 * 3600 * 1000))}일 후 만료됩니다.`\r\n );\r\n warned = true;\r\n }\r\n return {\r\n valid: true,\r\n watermarkRequired: false,\r\n expiryWarning: 'soon-expiring',\r\n expiresAt: status.expiresAt,\r\n };\r\n }\r\n }\r\n\r\n return { valid: true, watermarkRequired: false };\r\n}\r\n","// Watermark.tsx\r\nimport React from 'react';\r\n\r\ninterface WatermarkProps {\r\n required: boolean;\r\n}\r\n\r\n/**\r\n * Pro 라이선스가 없을 때 그리드 위에 표시되는 워터마크 컴포넌트.\r\n *\r\n * `required=false` 이면 `null` 반환 (렌더링 없음).\r\n */\r\nexport function Watermark({ required }: WatermarkProps): React.ReactElement | null {\r\n if (!required) return null;\r\n return (\r\n <div className=\"absolute top-0 right-0 opacity-40 pointer-events-none select-none text-sm font-semibold text-gray-500 p-2\">\r\n Unlicensed @topgrid/grid\r\n </div>\r\n );\r\n}\r\n","import { useSyncExternalStore } from 'react';\r\n\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { getCachedCheck, subscribeLicense } from './state.js';\r\n\r\n// `useSyncExternalStore` requires `getSnapshot` to return the same reference\r\n// across calls unless the underlying state actually changed; otherwise React\r\n// throws \"The result of getSnapshot should be cached to avoid an infinite\r\n// loop\" in Strict Mode. We delegate to `getCachedCheck`, which memoises until\r\n// `setLicenseState` invalidates the cache.\r\nconst getSnapshot = (): LicenseCheckResult => getCachedCheck(checkLicense);\r\n\r\n/**\r\n * React hook returning the current license check result. Re-renders when the\r\n * license state changes (e.g. async `setLicenseKey` resolution).\r\n *\r\n * Backed by `useSyncExternalStore` — no tearing under React 18 concurrent mode.\r\n *\r\n * @example\r\n * ```tsx\r\n * function MyGrid() {\r\n * const lic = useLicenseStatus();\r\n * return (\r\n * <div className=\"relative\">\r\n * <table>{ ... }</table>\r\n * {lic.watermarkRequired && <Watermark required />}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useLicenseStatus(): LicenseCheckResult {\r\n return useSyncExternalStore(subscribeLicense, getSnapshot, getSnapshot);\r\n}\r\n","import { useEffect } from 'react';\r\nimport { createRoot, type Root } from 'react-dom/client';\r\n\r\nimport { Watermark } from './Watermark.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { subscribeLicense } from './state.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Module-level singleton state.\r\n//\r\n// Multiple components calling `useWatermarkEnforcement()` (e.g. 500\r\n// `<DataMapCell>` instances) only mount ONE portal at `document.body`. The\r\n// ref-count tracks active subscribers — when it returns to 0, the portal is\r\n// torn down.\r\n//\r\n// Re-renders are driven by `subscribeLicense` — when `setLicenseKey` resolves\r\n// and flips `watermarkRequired`, the singleton React root re-renders.\r\n// ---------------------------------------------------------------------------\r\n\r\nlet _activeCount = 0;\r\nlet _portalContainer: HTMLDivElement | null = null;\r\nlet _portalRoot: Root | null = null;\r\nlet _unsubLicense: (() => void) | null = null;\r\n\r\nfunction renderWatermark(): void {\r\n if (_portalRoot === null || typeof document === 'undefined') return;\r\n const lic = checkLicense();\r\n _portalRoot.render(lic.watermarkRequired ? <Watermark required /> : null);\r\n}\r\n\r\nfunction mountPortal(): void {\r\n if (typeof document === 'undefined') return;\r\n if (_portalContainer !== null) return; // already mounted\r\n _portalContainer = document.createElement('div');\r\n // DOM marker for the singleton watermark portal container (one per document).\r\n _portalContainer.setAttribute('data-topgrid-watermark', '');\r\n document.body.appendChild(_portalContainer);\r\n _portalRoot = createRoot(_portalContainer);\r\n renderWatermark();\r\n _unsubLicense = subscribeLicense(renderWatermark);\r\n}\r\n\r\nfunction unmountPortal(): void {\r\n if (_unsubLicense !== null) _unsubLicense();\r\n if (_portalRoot !== null) _portalRoot.unmount();\r\n if (_portalContainer !== null && _portalContainer.parentNode !== null) {\r\n _portalContainer.parentNode.removeChild(_portalContainer);\r\n }\r\n _portalRoot = null;\r\n _portalContainer = null;\r\n _unsubLicense = null;\r\n}\r\n\r\n/**\r\n * Void registration hook for license watermark enforcement via a singleton\r\n * portal mounted at `document.body`.\r\n *\r\n * - Each mount increments a module-level ref-count.\r\n * - First mount creates the singleton portal + React root.\r\n * - License state changes (`setLicenseKey`) re-render the portal via\r\n * `subscribeLicense`.\r\n * - Last unmount (ref-count → 0) tears down the portal.\r\n *\r\n * Use case: per-cell renderers (e.g. `DataMapCell`) where the component\r\n * itself has no host DOM suitable for wrapper-based watermarking.\r\n *\r\n * SSR-safe: portal setup is skipped when `document` is undefined.\r\n *\r\n * @example\r\n * ```tsx\r\n * export function DataMapCell(info) {\r\n * useWatermarkEnforcement(); // void — no return value\r\n * return <span>{...}</span>;\r\n * }\r\n * ```\r\n */\r\nexport function useWatermarkEnforcement(): void {\r\n useEffect(() => {\r\n _activeCount += 1;\r\n if (_activeCount === 1) mountPortal();\r\n return () => {\r\n _activeCount = Math.max(0, _activeCount - 1);\r\n if (_activeCount === 0) unmountPortal();\r\n };\r\n }, []);\r\n}\r\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -179,7 +179,7 @@ function mountPortal() {
|
|
|
179
179
|
if (typeof document === "undefined") return;
|
|
180
180
|
if (_portalContainer !== null) return;
|
|
181
181
|
_portalContainer = document.createElement("div");
|
|
182
|
-
_portalContainer.setAttribute("data-
|
|
182
|
+
_portalContainer.setAttribute("data-topgrid-watermark", "");
|
|
183
183
|
document.body.appendChild(_portalContainer);
|
|
184
184
|
_portalRoot = createRoot(_portalContainer);
|
|
185
185
|
renderWatermark();
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/verifySignature.ts","../src/state.ts","../src/setLicenseKey.ts","../src/checkLicense.ts","../src/Watermark.tsx","../src/useLicenseStatus.ts","../src/useWatermarkEnforcement.tsx"],"names":["jsx"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { LicenseStatus } from './types.js';\r\n\r\ninterface KeyPayload {\r\n domain: string;\r\n expiresAt: number; // Unix ms\r\n tier: string;\r\n}\r\n\r\nfunction isKeyPayload(v: unknown): v is KeyPayload {\r\n return (\r\n typeof v === 'object' &&\r\n v !== null &&\r\n typeof (v as Record<string, unknown>)['domain'] === 'string' &&\r\n typeof (v as Record<string, unknown>)['expiresAt'] === 'number' &&\r\n typeof (v as Record<string, unknown>)['tier'] === 'string'\r\n );\r\n}\r\n\r\nfunction base64urlToBytes(s: string): Uint8Array {\r\n const base64 = s.replace(/-/g, '+').replace(/_/g, '/');\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\r\n const binary = atob(padded);\r\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\r\n}\r\n\r\n/** D7: C-32 pure async helper — no React, no DOM side-effects */\r\nexport async function verifySignature(rawKey: string): Promise<LicenseStatus> {\r\n const parts = rawKey.split('.');\r\n if (parts.length !== 3) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const [pubKeyB64, sigB64, payloadB64] = parts;\r\n\r\n let payload: unknown;\r\n try {\r\n payload = JSON.parse(new TextDecoder().decode(base64urlToBytes(payloadB64)));\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!isKeyPayload(payload)) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // Web Crypto API — Ed25519\r\n let cryptoSubtle: SubtleCrypto;\r\n try {\r\n cryptoSubtle = crypto.subtle;\r\n } catch {\r\n // SSR/Node 18 fallback: crypto.subtle 미지원\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n let pubKey: CryptoKey;\r\n try {\r\n pubKey = await cryptoSubtle.importKey(\r\n 'raw',\r\n base64urlToBytes(pubKeyB64),\r\n { name: 'Ed25519' },\r\n false,\r\n ['verify'],\r\n );\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const sigBytes = base64urlToBytes(sigB64);\r\n const msgBytes = base64urlToBytes(payloadB64);\r\n\r\n let sigOk: boolean;\r\n try {\r\n sigOk = await cryptoSubtle.verify('Ed25519', pubKey, sigBytes, msgBytes);\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!sigOk) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // expiry check\r\n const now = Date.now();\r\n if (payload.expiresAt < now) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'expired' } as { reason: 'expired' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n\r\n // domain check — D5: SSR window undefined → skip\r\n let hostname: string | null = null;\r\n if (typeof window !== 'undefined') {\r\n hostname = window.location.hostname;\r\n }\r\n\r\n if (hostname !== null) {\r\n const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1';\r\n if (!isLocalhost && hostname !== payload.domain) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'domain-mismatch' } as { reason: 'domain-mismatch' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n }\r\n\r\n return {\r\n valid: true,\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n}\r\n","import type { LicenseCheckResult, LicenseState, LicenseStatus } from './types.js';\r\n\r\nlet _state: LicenseState | null = null;\r\n\r\ntype LicenseListener = () => void;\r\nconst _listeners = new Set<LicenseListener>();\r\n\r\n// `useSyncExternalStore` REQUIRES the snapshot function to return the same\r\n// reference between calls unless the underlying state has actually changed —\r\n// otherwise React enters an infinite render loop in Strict Mode (React docs:\r\n// \"Do not return a new object from getSnapshot every time\"). Since\r\n// `checkLicense()` allocates a fresh `LicenseCheckResult` on every call, we\r\n// cache the most recent result here and invalidate it whenever `setLicenseState`\r\n// runs (i.e. when the underlying state actually changes).\r\nlet _cachedCheck: LicenseCheckResult | null = null;\r\n\r\nexport function setLicenseState(s: LicenseState): void {\r\n _state = s;\r\n _cachedCheck = null;\r\n _listeners.forEach((l) => l());\r\n}\r\n\r\nexport function getLicenseState(): LicenseStatus {\r\n if (_state === null) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n return _state.status;\r\n}\r\n\r\n/**\r\n * Returns a cached `LicenseCheckResult` — computes via `compute()` only on\r\n * the first call after a state change. Subsequent calls return the same\r\n * reference until `setLicenseState` invalidates the cache.\r\n *\r\n * Used by `useLicenseStatus` (via `useSyncExternalStore`) to satisfy React's\r\n * snapshot-stability requirement.\r\n */\r\nexport function getCachedCheck(\r\n compute: () => LicenseCheckResult,\r\n): LicenseCheckResult {\r\n if (_cachedCheck === null) _cachedCheck = compute();\r\n return _cachedCheck;\r\n}\r\n\r\n/**\r\n * Subscribe to license state changes. Listener is invoked synchronously\r\n * after every `setLicenseState` call. Returns an unsubscribe function.\r\n *\r\n * Used internally by `useLicenseStatus` (via `useSyncExternalStore`) and\r\n * by `useWatermarkEnforcement` (singleton portal re-render trigger).\r\n */\r\nexport function subscribeLicense(listener: LicenseListener): () => void {\r\n _listeners.add(listener);\r\n return () => {\r\n _listeners.delete(listener);\r\n };\r\n}\r\n","import type { LicenseStatus } from './types.js';\r\nimport { verifySignature } from './verifySignature.js';\r\nimport { setLicenseState } from './state.js';\r\n\r\n/**\r\n * Pro 패키지 전역 라이선스 등록 API.\r\n * 앱 entry(main.tsx / App.tsx)에서 1회 호출.\r\n * @param key - Base64url(pubKey).Base64url(sig).Base64url(payload) 형식 라이선스 키\r\n * @returns LicenseStatus — 즉시 반환 (동기 wrapper, 내부 비동기 검증 완료 후 상태 갱신)\r\n *\r\n * 주의: 반환값은 Promise 없이 즉시 사용 가능하도록 동기 API로 설계.\r\n * 내부적으로 verifySignature (async) 결과를 저장. 비동기 완료 전 getLicenseState() 호출 시\r\n * 기본값 {valid:false, reason:'invalid'} 반환 (D6).\r\n */\r\nexport function setLicenseKey(key: string): LicenseStatus {\r\n // 기본값 초기화 (D6: 검증 완료 전 getLicenseState 호출 대비)\r\n const pending: LicenseStatus = { valid: false };\r\n\r\n // 비동기 검증 시작 (fire-and-forget, 결과는 state에 저장)\r\n verifySignature(key).then((status) => {\r\n setLicenseState({ status, rawKey: key, setAt: Date.now() });\r\n }).catch(() => {\r\n setLicenseState({\r\n status: { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) },\r\n rawKey: key,\r\n setAt: Date.now(),\r\n });\r\n });\r\n\r\n return pending;\r\n}\r\n","// checkLicense.ts\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { getLicenseState } from './state.js';\r\n\r\nconst SIXTY_DAYS_MS = 60 * 24 * 3600 * 1000;\r\nlet warned = false;\r\n\r\n/**\r\n * 현재 라이선스 상태를 동기 검사하여 `LicenseCheckResult`를 반환한다.\r\n *\r\n * - valid=false 이면 `watermarkRequired=true`.\r\n * - 유효하고 `expiresAt`까지 60일 미만이면 `expiryWarning='soon-expiring'` + `console.warn` (1회).\r\n * - 유효하고 만료 여유가 충분하면 `{ valid: true, watermarkRequired: false }`.\r\n */\r\nexport function checkLicense(): LicenseCheckResult {\r\n const status = getLicenseState(); // LicenseStatus (sync)\r\n\r\n if (!status.valid) {\r\n const result: LicenseCheckResult = { valid: false, watermarkRequired: true };\r\n if (status.reason !== undefined) result.reason = status.reason;\r\n if (status.expiresAt !== undefined) result.expiresAt = status.expiresAt;\r\n return result;\r\n }\r\n\r\n if (status.expiresAt !== undefined) {\r\n const msLeft = status.expiresAt.getTime() - Date.now();\r\n if (msLeft < SIXTY_DAYS_MS) {\r\n if (!warned) {\r\n console.warn(\r\n `[grid-license] 라이선스가 ${Math.ceil(msLeft / (24 * 3600 * 1000))}일 후 만료됩니다.`\r\n );\r\n warned = true;\r\n }\r\n return {\r\n valid: true,\r\n watermarkRequired: false,\r\n expiryWarning: 'soon-expiring',\r\n expiresAt: status.expiresAt,\r\n };\r\n }\r\n }\r\n\r\n return { valid: true, watermarkRequired: false };\r\n}\r\n","// Watermark.tsx\r\nimport React from 'react';\r\n\r\ninterface WatermarkProps {\r\n required: boolean;\r\n}\r\n\r\n/**\r\n * Pro 라이선스가 없을 때 그리드 위에 표시되는 워터마크 컴포넌트.\r\n *\r\n * `required=false` 이면 `null` 반환 (렌더링 없음).\r\n */\r\nexport function Watermark({ required }: WatermarkProps): React.ReactElement | null {\r\n if (!required) return null;\r\n return (\r\n <div className=\"absolute top-0 right-0 opacity-40 pointer-events-none select-none text-sm font-semibold text-gray-500 p-2\">\r\n Unlicensed @topgrid/grid\r\n </div>\r\n );\r\n}\r\n","import { useSyncExternalStore } from 'react';\r\n\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { getCachedCheck, subscribeLicense } from './state.js';\r\n\r\n// `useSyncExternalStore` requires `getSnapshot` to return the same reference\r\n// across calls unless the underlying state actually changed; otherwise React\r\n// throws \"The result of getSnapshot should be cached to avoid an infinite\r\n// loop\" in Strict Mode. We delegate to `getCachedCheck`, which memoises until\r\n// `setLicenseState` invalidates the cache.\r\nconst getSnapshot = (): LicenseCheckResult => getCachedCheck(checkLicense);\r\n\r\n/**\r\n * React hook returning the current license check result. Re-renders when the\r\n * license state changes (e.g. async `setLicenseKey` resolution).\r\n *\r\n * Backed by `useSyncExternalStore` — no tearing under React 18 concurrent mode.\r\n *\r\n * @example\r\n * ```tsx\r\n * function MyGrid() {\r\n * const lic = useLicenseStatus();\r\n * return (\r\n * <div className=\"relative\">\r\n * <table>{ ... }</table>\r\n * {lic.watermarkRequired && <Watermark required />}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useLicenseStatus(): LicenseCheckResult {\r\n return useSyncExternalStore(subscribeLicense, getSnapshot, getSnapshot);\r\n}\r\n","import { useEffect } from 'react';\r\nimport { createRoot, type Root } from 'react-dom/client';\r\n\r\nimport { Watermark } from './Watermark.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { subscribeLicense } from './state.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Module-level singleton state.\r\n//\r\n// Multiple components calling `useWatermarkEnforcement()` (e.g. 500\r\n// `<DataMapCell>` instances) only mount ONE portal at `document.body`. The\r\n// ref-count tracks active subscribers — when it returns to 0, the portal is\r\n// torn down.\r\n//\r\n// Re-renders are driven by `subscribeLicense` — when `setLicenseKey` resolves\r\n// and flips `watermarkRequired`, the singleton React root re-renders.\r\n// ---------------------------------------------------------------------------\r\n\r\nlet _activeCount = 0;\r\nlet _portalContainer: HTMLDivElement | null = null;\r\nlet _portalRoot: Root | null = null;\r\nlet _unsubLicense: (() => void) | null = null;\r\n\r\nfunction renderWatermark(): void {\r\n if (_portalRoot === null || typeof document === 'undefined') return;\r\n const lic = checkLicense();\r\n _portalRoot.render(lic.watermarkRequired ? <Watermark required /> : null);\r\n}\r\n\r\nfunction mountPortal(): void {\r\n if (typeof document === 'undefined') return;\r\n if (_portalContainer !== null) return; // already mounted\r\n _portalContainer = document.createElement('div');\r\n _portalContainer.setAttribute('data-tomis-watermark', '');\r\n document.body.appendChild(_portalContainer);\r\n _portalRoot = createRoot(_portalContainer);\r\n renderWatermark();\r\n _unsubLicense = subscribeLicense(renderWatermark);\r\n}\r\n\r\nfunction unmountPortal(): void {\r\n if (_unsubLicense !== null) _unsubLicense();\r\n if (_portalRoot !== null) _portalRoot.unmount();\r\n if (_portalContainer !== null && _portalContainer.parentNode !== null) {\r\n _portalContainer.parentNode.removeChild(_portalContainer);\r\n }\r\n _portalRoot = null;\r\n _portalContainer = null;\r\n _unsubLicense = null;\r\n}\r\n\r\n/**\r\n * Void registration hook for license watermark enforcement via a singleton\r\n * portal mounted at `document.body`.\r\n *\r\n * - Each mount increments a module-level ref-count.\r\n * - First mount creates the singleton portal + React root.\r\n * - License state changes (`setLicenseKey`) re-render the portal via\r\n * `subscribeLicense`.\r\n * - Last unmount (ref-count → 0) tears down the portal.\r\n *\r\n * Use case: per-cell renderers (e.g. `DataMapCell`) where the component\r\n * itself has no host DOM suitable for wrapper-based watermarking.\r\n *\r\n * SSR-safe: portal setup is skipped when `document` is undefined.\r\n *\r\n * @example\r\n * ```tsx\r\n * export function DataMapCell(info) {\r\n * useWatermarkEnforcement(); // void — no return value\r\n * return <span>{...}</span>;\r\n * }\r\n * ```\r\n */\r\nexport function useWatermarkEnforcement(): void {\r\n useEffect(() => {\r\n _activeCount += 1;\r\n if (_activeCount === 1) mountPortal();\r\n return () => {\r\n _activeCount = Math.max(0, _activeCount - 1);\r\n if (_activeCount === 0) unmountPortal();\r\n };\r\n }, []);\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/verifySignature.ts","../src/state.ts","../src/setLicenseKey.ts","../src/checkLicense.ts","../src/Watermark.tsx","../src/useLicenseStatus.ts","../src/useWatermarkEnforcement.tsx"],"names":["jsx"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { LicenseStatus } from './types.js';\r\n\r\ninterface KeyPayload {\r\n domain: string;\r\n expiresAt: number; // Unix ms\r\n tier: string;\r\n}\r\n\r\nfunction isKeyPayload(v: unknown): v is KeyPayload {\r\n return (\r\n typeof v === 'object' &&\r\n v !== null &&\r\n typeof (v as Record<string, unknown>)['domain'] === 'string' &&\r\n typeof (v as Record<string, unknown>)['expiresAt'] === 'number' &&\r\n typeof (v as Record<string, unknown>)['tier'] === 'string'\r\n );\r\n}\r\n\r\nfunction base64urlToBytes(s: string): Uint8Array {\r\n const base64 = s.replace(/-/g, '+').replace(/_/g, '/');\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\r\n const binary = atob(padded);\r\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\r\n}\r\n\r\n/** D7: C-32 pure async helper — no React, no DOM side-effects */\r\nexport async function verifySignature(rawKey: string): Promise<LicenseStatus> {\r\n const parts = rawKey.split('.');\r\n if (parts.length !== 3) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const [pubKeyB64, sigB64, payloadB64] = parts;\r\n\r\n let payload: unknown;\r\n try {\r\n payload = JSON.parse(new TextDecoder().decode(base64urlToBytes(payloadB64)));\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!isKeyPayload(payload)) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // Web Crypto API — Ed25519\r\n let cryptoSubtle: SubtleCrypto;\r\n try {\r\n cryptoSubtle = crypto.subtle;\r\n } catch {\r\n // SSR/Node 18 fallback: crypto.subtle 미지원\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n let pubKey: CryptoKey;\r\n try {\r\n pubKey = await cryptoSubtle.importKey(\r\n 'raw',\r\n base64urlToBytes(pubKeyB64),\r\n { name: 'Ed25519' },\r\n false,\r\n ['verify'],\r\n );\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n const sigBytes = base64urlToBytes(sigB64);\r\n const msgBytes = base64urlToBytes(payloadB64);\r\n\r\n let sigOk: boolean;\r\n try {\r\n sigOk = await cryptoSubtle.verify('Ed25519', pubKey, sigBytes, msgBytes);\r\n } catch {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n if (!sigOk) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n\r\n // expiry check\r\n const now = Date.now();\r\n if (payload.expiresAt < now) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'expired' } as { reason: 'expired' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n\r\n // domain check — D5: SSR window undefined → skip\r\n let hostname: string | null = null;\r\n if (typeof window !== 'undefined') {\r\n hostname = window.location.hostname;\r\n }\r\n\r\n if (hostname !== null) {\r\n const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1';\r\n if (!isLocalhost && hostname !== payload.domain) {\r\n return {\r\n valid: false,\r\n ...({ reason: 'domain-mismatch' } as { reason: 'domain-mismatch' }),\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n }\r\n }\r\n\r\n return {\r\n valid: true,\r\n expiresAt: new Date(payload.expiresAt),\r\n domain: payload.domain,\r\n };\r\n}\r\n","import type { LicenseCheckResult, LicenseState, LicenseStatus } from './types.js';\r\n\r\nlet _state: LicenseState | null = null;\r\n\r\ntype LicenseListener = () => void;\r\nconst _listeners = new Set<LicenseListener>();\r\n\r\n// `useSyncExternalStore` REQUIRES the snapshot function to return the same\r\n// reference between calls unless the underlying state has actually changed —\r\n// otherwise React enters an infinite render loop in Strict Mode (React docs:\r\n// \"Do not return a new object from getSnapshot every time\"). Since\r\n// `checkLicense()` allocates a fresh `LicenseCheckResult` on every call, we\r\n// cache the most recent result here and invalidate it whenever `setLicenseState`\r\n// runs (i.e. when the underlying state actually changes).\r\nlet _cachedCheck: LicenseCheckResult | null = null;\r\n\r\nexport function setLicenseState(s: LicenseState): void {\r\n _state = s;\r\n _cachedCheck = null;\r\n _listeners.forEach((l) => l());\r\n}\r\n\r\nexport function getLicenseState(): LicenseStatus {\r\n if (_state === null) {\r\n return { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) };\r\n }\r\n return _state.status;\r\n}\r\n\r\n/**\r\n * Returns a cached `LicenseCheckResult` — computes via `compute()` only on\r\n * the first call after a state change. Subsequent calls return the same\r\n * reference until `setLicenseState` invalidates the cache.\r\n *\r\n * Used by `useLicenseStatus` (via `useSyncExternalStore`) to satisfy React's\r\n * snapshot-stability requirement.\r\n */\r\nexport function getCachedCheck(\r\n compute: () => LicenseCheckResult,\r\n): LicenseCheckResult {\r\n if (_cachedCheck === null) _cachedCheck = compute();\r\n return _cachedCheck;\r\n}\r\n\r\n/**\r\n * Subscribe to license state changes. Listener is invoked synchronously\r\n * after every `setLicenseState` call. Returns an unsubscribe function.\r\n *\r\n * Used internally by `useLicenseStatus` (via `useSyncExternalStore`) and\r\n * by `useWatermarkEnforcement` (singleton portal re-render trigger).\r\n */\r\nexport function subscribeLicense(listener: LicenseListener): () => void {\r\n _listeners.add(listener);\r\n return () => {\r\n _listeners.delete(listener);\r\n };\r\n}\r\n","import type { LicenseStatus } from './types.js';\r\nimport { verifySignature } from './verifySignature.js';\r\nimport { setLicenseState } from './state.js';\r\n\r\n/**\r\n * Pro 패키지 전역 라이선스 등록 API.\r\n * 앱 entry(main.tsx / App.tsx)에서 1회 호출.\r\n * @param key - Base64url(pubKey).Base64url(sig).Base64url(payload) 형식 라이선스 키\r\n * @returns LicenseStatus — 즉시 반환 (동기 wrapper, 내부 비동기 검증 완료 후 상태 갱신)\r\n *\r\n * 주의: 반환값은 Promise 없이 즉시 사용 가능하도록 동기 API로 설계.\r\n * 내부적으로 verifySignature (async) 결과를 저장. 비동기 완료 전 getLicenseState() 호출 시\r\n * 기본값 {valid:false, reason:'invalid'} 반환 (D6).\r\n */\r\nexport function setLicenseKey(key: string): LicenseStatus {\r\n // 기본값 초기화 (D6: 검증 완료 전 getLicenseState 호출 대비)\r\n const pending: LicenseStatus = { valid: false };\r\n\r\n // 비동기 검증 시작 (fire-and-forget, 결과는 state에 저장)\r\n verifySignature(key).then((status) => {\r\n setLicenseState({ status, rawKey: key, setAt: Date.now() });\r\n }).catch(() => {\r\n setLicenseState({\r\n status: { valid: false, ...({ reason: 'invalid' } as { reason: 'invalid' }) },\r\n rawKey: key,\r\n setAt: Date.now(),\r\n });\r\n });\r\n\r\n return pending;\r\n}\r\n","// checkLicense.ts\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { getLicenseState } from './state.js';\r\n\r\nconst SIXTY_DAYS_MS = 60 * 24 * 3600 * 1000;\r\nlet warned = false;\r\n\r\n/**\r\n * 현재 라이선스 상태를 동기 검사하여 `LicenseCheckResult`를 반환한다.\r\n *\r\n * - valid=false 이면 `watermarkRequired=true`.\r\n * - 유효하고 `expiresAt`까지 60일 미만이면 `expiryWarning='soon-expiring'` + `console.warn` (1회).\r\n * - 유효하고 만료 여유가 충분하면 `{ valid: true, watermarkRequired: false }`.\r\n */\r\nexport function checkLicense(): LicenseCheckResult {\r\n const status = getLicenseState(); // LicenseStatus (sync)\r\n\r\n if (!status.valid) {\r\n const result: LicenseCheckResult = { valid: false, watermarkRequired: true };\r\n if (status.reason !== undefined) result.reason = status.reason;\r\n if (status.expiresAt !== undefined) result.expiresAt = status.expiresAt;\r\n return result;\r\n }\r\n\r\n if (status.expiresAt !== undefined) {\r\n const msLeft = status.expiresAt.getTime() - Date.now();\r\n if (msLeft < SIXTY_DAYS_MS) {\r\n if (!warned) {\r\n console.warn(\r\n `[grid-license] 라이선스가 ${Math.ceil(msLeft / (24 * 3600 * 1000))}일 후 만료됩니다.`\r\n );\r\n warned = true;\r\n }\r\n return {\r\n valid: true,\r\n watermarkRequired: false,\r\n expiryWarning: 'soon-expiring',\r\n expiresAt: status.expiresAt,\r\n };\r\n }\r\n }\r\n\r\n return { valid: true, watermarkRequired: false };\r\n}\r\n","// Watermark.tsx\r\nimport React from 'react';\r\n\r\ninterface WatermarkProps {\r\n required: boolean;\r\n}\r\n\r\n/**\r\n * Pro 라이선스가 없을 때 그리드 위에 표시되는 워터마크 컴포넌트.\r\n *\r\n * `required=false` 이면 `null` 반환 (렌더링 없음).\r\n */\r\nexport function Watermark({ required }: WatermarkProps): React.ReactElement | null {\r\n if (!required) return null;\r\n return (\r\n <div className=\"absolute top-0 right-0 opacity-40 pointer-events-none select-none text-sm font-semibold text-gray-500 p-2\">\r\n Unlicensed @topgrid/grid\r\n </div>\r\n );\r\n}\r\n","import { useSyncExternalStore } from 'react';\r\n\r\nimport type { LicenseCheckResult } from './types.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { getCachedCheck, subscribeLicense } from './state.js';\r\n\r\n// `useSyncExternalStore` requires `getSnapshot` to return the same reference\r\n// across calls unless the underlying state actually changed; otherwise React\r\n// throws \"The result of getSnapshot should be cached to avoid an infinite\r\n// loop\" in Strict Mode. We delegate to `getCachedCheck`, which memoises until\r\n// `setLicenseState` invalidates the cache.\r\nconst getSnapshot = (): LicenseCheckResult => getCachedCheck(checkLicense);\r\n\r\n/**\r\n * React hook returning the current license check result. Re-renders when the\r\n * license state changes (e.g. async `setLicenseKey` resolution).\r\n *\r\n * Backed by `useSyncExternalStore` — no tearing under React 18 concurrent mode.\r\n *\r\n * @example\r\n * ```tsx\r\n * function MyGrid() {\r\n * const lic = useLicenseStatus();\r\n * return (\r\n * <div className=\"relative\">\r\n * <table>{ ... }</table>\r\n * {lic.watermarkRequired && <Watermark required />}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useLicenseStatus(): LicenseCheckResult {\r\n return useSyncExternalStore(subscribeLicense, getSnapshot, getSnapshot);\r\n}\r\n","import { useEffect } from 'react';\r\nimport { createRoot, type Root } from 'react-dom/client';\r\n\r\nimport { Watermark } from './Watermark.js';\r\nimport { checkLicense } from './checkLicense.js';\r\nimport { subscribeLicense } from './state.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Module-level singleton state.\r\n//\r\n// Multiple components calling `useWatermarkEnforcement()` (e.g. 500\r\n// `<DataMapCell>` instances) only mount ONE portal at `document.body`. The\r\n// ref-count tracks active subscribers — when it returns to 0, the portal is\r\n// torn down.\r\n//\r\n// Re-renders are driven by `subscribeLicense` — when `setLicenseKey` resolves\r\n// and flips `watermarkRequired`, the singleton React root re-renders.\r\n// ---------------------------------------------------------------------------\r\n\r\nlet _activeCount = 0;\r\nlet _portalContainer: HTMLDivElement | null = null;\r\nlet _portalRoot: Root | null = null;\r\nlet _unsubLicense: (() => void) | null = null;\r\n\r\nfunction renderWatermark(): void {\r\n if (_portalRoot === null || typeof document === 'undefined') return;\r\n const lic = checkLicense();\r\n _portalRoot.render(lic.watermarkRequired ? <Watermark required /> : null);\r\n}\r\n\r\nfunction mountPortal(): void {\r\n if (typeof document === 'undefined') return;\r\n if (_portalContainer !== null) return; // already mounted\r\n _portalContainer = document.createElement('div');\r\n // DOM marker for the singleton watermark portal container (one per document).\r\n _portalContainer.setAttribute('data-topgrid-watermark', '');\r\n document.body.appendChild(_portalContainer);\r\n _portalRoot = createRoot(_portalContainer);\r\n renderWatermark();\r\n _unsubLicense = subscribeLicense(renderWatermark);\r\n}\r\n\r\nfunction unmountPortal(): void {\r\n if (_unsubLicense !== null) _unsubLicense();\r\n if (_portalRoot !== null) _portalRoot.unmount();\r\n if (_portalContainer !== null && _portalContainer.parentNode !== null) {\r\n _portalContainer.parentNode.removeChild(_portalContainer);\r\n }\r\n _portalRoot = null;\r\n _portalContainer = null;\r\n _unsubLicense = null;\r\n}\r\n\r\n/**\r\n * Void registration hook for license watermark enforcement via a singleton\r\n * portal mounted at `document.body`.\r\n *\r\n * - Each mount increments a module-level ref-count.\r\n * - First mount creates the singleton portal + React root.\r\n * - License state changes (`setLicenseKey`) re-render the portal via\r\n * `subscribeLicense`.\r\n * - Last unmount (ref-count → 0) tears down the portal.\r\n *\r\n * Use case: per-cell renderers (e.g. `DataMapCell`) where the component\r\n * itself has no host DOM suitable for wrapper-based watermarking.\r\n *\r\n * SSR-safe: portal setup is skipped when `document` is undefined.\r\n *\r\n * @example\r\n * ```tsx\r\n * export function DataMapCell(info) {\r\n * useWatermarkEnforcement(); // void — no return value\r\n * return <span>{...}</span>;\r\n * }\r\n * ```\r\n */\r\nexport function useWatermarkEnforcement(): void {\r\n useEffect(() => {\r\n _activeCount += 1;\r\n if (_activeCount === 1) mountPortal();\r\n return () => {\r\n _activeCount = Math.max(0, _activeCount - 1);\r\n if (_activeCount === 0) unmountPortal();\r\n };\r\n }, []);\r\n}\r\n"]}
|