@topgrid/grid-license 0.1.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 ADDED
@@ -0,0 +1,49 @@
1
+ # @topgrid/grid-license
2
+
3
+ Pro license validation runtime
4
+
5
+ ## Overview
6
+
7
+ `@topgrid/grid-license` is the runtime license validation module for TOMIS Grid Pro packages.
8
+ All Pro packages (`@topgrid/grid-pro-*`) depend on this module to verify a valid license at runtime.
9
+
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.
11
+
12
+ ## Usage (for Pro package consumers)
13
+
14
+ Call `setLicenseKey` once at your application entry point (e.g., `main.tsx`) before rendering any Pro grid component:
15
+
16
+ ```tsx
17
+ import { setLicenseKey } from '@topgrid/grid-license';
18
+
19
+ // Call once at app startup
20
+ setLicenseKey('YOUR-LICENSE-KEY');
21
+ ```
22
+
23
+ Without a valid license key, Pro components will render with a watermark overlay.
24
+
25
+ ## API
26
+
27
+ | Export | Signature | Description |
28
+ |--------|-----------|-------------|
29
+ | `setLicenseKey` | `(key: string) => void` | Registers your Pro license key |
30
+ | `checkLicense` | `() => LicenseCheckResult` | Validates the registered key (called internally) |
31
+ | `Watermark` | React component | Rendered when no valid license is detected |
32
+ | `LicenseStatus` | type | `'valid' \| 'invalid' \| 'missing'` |
33
+
34
+ ## Peer Dependencies
35
+
36
+ | Package | Version |
37
+ |---------|---------|
38
+ | `react` | `^18.0.0 \|\| ^19.0.0` |
39
+ | `react-dom` | `^18.0.0 \|\| ^19.0.0` |
40
+
41
+ ## License
42
+
43
+ SEE LICENSE IN EULA
44
+
45
+ Contact [sales@topvel.com](mailto:sales@topvel.com) to obtain a license key.
46
+
47
+ ---
48
+
49
+ [Documentation](https://grid.tomis.dev) | [Pricing](https://topvel.com/grid/pricing)
package/dist/index.cjs ADDED
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var client = require('react-dom/client');
6
+
7
+ // src/verifySignature.ts
8
+ function isKeyPayload(v) {
9
+ return typeof v === "object" && v !== null && typeof v["domain"] === "string" && typeof v["expiresAt"] === "number" && typeof v["tier"] === "string";
10
+ }
11
+ function base64urlToBytes(s) {
12
+ const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
13
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
14
+ const binary = atob(padded);
15
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
16
+ }
17
+ async function verifySignature(rawKey) {
18
+ const parts = rawKey.split(".");
19
+ if (parts.length !== 3) {
20
+ return { valid: false, ...{ reason: "invalid" } };
21
+ }
22
+ const [pubKeyB64, sigB64, payloadB64] = parts;
23
+ let payload;
24
+ try {
25
+ payload = JSON.parse(new TextDecoder().decode(base64urlToBytes(payloadB64)));
26
+ } catch {
27
+ return { valid: false, ...{ reason: "invalid" } };
28
+ }
29
+ if (!isKeyPayload(payload)) {
30
+ return { valid: false, ...{ reason: "invalid" } };
31
+ }
32
+ let cryptoSubtle;
33
+ try {
34
+ cryptoSubtle = crypto.subtle;
35
+ } catch {
36
+ return { valid: false, ...{ reason: "invalid" } };
37
+ }
38
+ let pubKey;
39
+ try {
40
+ pubKey = await cryptoSubtle.importKey(
41
+ "raw",
42
+ base64urlToBytes(pubKeyB64),
43
+ { name: "Ed25519" },
44
+ false,
45
+ ["verify"]
46
+ );
47
+ } catch {
48
+ return { valid: false, ...{ reason: "invalid" } };
49
+ }
50
+ const sigBytes = base64urlToBytes(sigB64);
51
+ const msgBytes = base64urlToBytes(payloadB64);
52
+ let sigOk;
53
+ try {
54
+ sigOk = await cryptoSubtle.verify("Ed25519", pubKey, sigBytes, msgBytes);
55
+ } catch {
56
+ return { valid: false, ...{ reason: "invalid" } };
57
+ }
58
+ if (!sigOk) {
59
+ return { valid: false, ...{ reason: "invalid" } };
60
+ }
61
+ const now = Date.now();
62
+ if (payload.expiresAt < now) {
63
+ return {
64
+ valid: false,
65
+ ...{ reason: "expired" },
66
+ expiresAt: new Date(payload.expiresAt),
67
+ domain: payload.domain
68
+ };
69
+ }
70
+ let hostname = null;
71
+ if (typeof window !== "undefined") {
72
+ hostname = window.location.hostname;
73
+ }
74
+ if (hostname !== null) {
75
+ const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1";
76
+ if (!isLocalhost && hostname !== payload.domain) {
77
+ return {
78
+ valid: false,
79
+ ...{ reason: "domain-mismatch" },
80
+ expiresAt: new Date(payload.expiresAt),
81
+ domain: payload.domain
82
+ };
83
+ }
84
+ }
85
+ return {
86
+ valid: true,
87
+ expiresAt: new Date(payload.expiresAt),
88
+ domain: payload.domain
89
+ };
90
+ }
91
+
92
+ // src/state.ts
93
+ var _state = null;
94
+ var _listeners = /* @__PURE__ */ new Set();
95
+ var _cachedCheck = null;
96
+ function setLicenseState(s) {
97
+ _state = s;
98
+ _cachedCheck = null;
99
+ _listeners.forEach((l) => l());
100
+ }
101
+ function getLicenseState() {
102
+ if (_state === null) {
103
+ return { valid: false, ...{ reason: "invalid" } };
104
+ }
105
+ return _state.status;
106
+ }
107
+ function getCachedCheck(compute) {
108
+ if (_cachedCheck === null) _cachedCheck = compute();
109
+ return _cachedCheck;
110
+ }
111
+ function subscribeLicense(listener) {
112
+ _listeners.add(listener);
113
+ return () => {
114
+ _listeners.delete(listener);
115
+ };
116
+ }
117
+
118
+ // src/setLicenseKey.ts
119
+ function setLicenseKey(key) {
120
+ const pending = { valid: false };
121
+ verifySignature(key).then((status) => {
122
+ setLicenseState({ status, rawKey: key, setAt: Date.now() });
123
+ }).catch(() => {
124
+ setLicenseState({
125
+ status: { valid: false, ...{ reason: "invalid" } },
126
+ rawKey: key,
127
+ setAt: Date.now()
128
+ });
129
+ });
130
+ return pending;
131
+ }
132
+
133
+ // src/checkLicense.ts
134
+ var SIXTY_DAYS_MS = 60 * 24 * 3600 * 1e3;
135
+ var warned = false;
136
+ function checkLicense() {
137
+ const status = getLicenseState();
138
+ if (!status.valid) {
139
+ const result = { valid: false, watermarkRequired: true };
140
+ if (status.reason !== void 0) result.reason = status.reason;
141
+ if (status.expiresAt !== void 0) result.expiresAt = status.expiresAt;
142
+ return result;
143
+ }
144
+ if (status.expiresAt !== void 0) {
145
+ const msLeft = status.expiresAt.getTime() - Date.now();
146
+ if (msLeft < SIXTY_DAYS_MS) {
147
+ if (!warned) {
148
+ console.warn(
149
+ `[grid-license] \uB77C\uC774\uC120\uC2A4\uAC00 ${Math.ceil(msLeft / (24 * 3600 * 1e3))}\uC77C \uD6C4 \uB9CC\uB8CC\uB429\uB2C8\uB2E4.`
150
+ );
151
+ warned = true;
152
+ }
153
+ return {
154
+ valid: true,
155
+ watermarkRequired: false,
156
+ expiryWarning: "soon-expiring",
157
+ expiresAt: status.expiresAt
158
+ };
159
+ }
160
+ }
161
+ return { valid: true, watermarkRequired: false };
162
+ }
163
+ function Watermark({ required }) {
164
+ if (!required) return null;
165
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 right-0 opacity-40 pointer-events-none select-none text-sm font-semibold text-gray-500 p-2", children: "Unlicensed @topgrid/grid" });
166
+ }
167
+ var getSnapshot = () => getCachedCheck(checkLicense);
168
+ function useLicenseStatus() {
169
+ return react.useSyncExternalStore(subscribeLicense, getSnapshot, getSnapshot);
170
+ }
171
+ var _activeCount = 0;
172
+ var _portalContainer = null;
173
+ var _portalRoot = null;
174
+ var _unsubLicense = null;
175
+ function renderWatermark() {
176
+ if (_portalRoot === null || typeof document === "undefined") return;
177
+ const lic = checkLicense();
178
+ _portalRoot.render(lic.watermarkRequired ? /* @__PURE__ */ jsxRuntime.jsx(Watermark, { required: true }) : null);
179
+ }
180
+ function mountPortal() {
181
+ if (typeof document === "undefined") return;
182
+ if (_portalContainer !== null) return;
183
+ _portalContainer = document.createElement("div");
184
+ _portalContainer.setAttribute("data-tomis-watermark", "");
185
+ document.body.appendChild(_portalContainer);
186
+ _portalRoot = client.createRoot(_portalContainer);
187
+ renderWatermark();
188
+ _unsubLicense = subscribeLicense(renderWatermark);
189
+ }
190
+ function unmountPortal() {
191
+ if (_unsubLicense !== null) _unsubLicense();
192
+ if (_portalRoot !== null) _portalRoot.unmount();
193
+ if (_portalContainer !== null && _portalContainer.parentNode !== null) {
194
+ _portalContainer.parentNode.removeChild(_portalContainer);
195
+ }
196
+ _portalRoot = null;
197
+ _portalContainer = null;
198
+ _unsubLicense = null;
199
+ }
200
+ function useWatermarkEnforcement() {
201
+ react.useEffect(() => {
202
+ _activeCount += 1;
203
+ if (_activeCount === 1) mountPortal();
204
+ return () => {
205
+ _activeCount = Math.max(0, _activeCount - 1);
206
+ if (_activeCount === 0) unmountPortal();
207
+ };
208
+ }, []);
209
+ }
210
+
211
+ exports.Watermark = Watermark;
212
+ exports.checkLicense = checkLicense;
213
+ exports.setLicenseKey = setLicenseKey;
214
+ exports.setLicenseState = setLicenseState;
215
+ exports.subscribeLicense = subscribeLicense;
216
+ exports.useLicenseStatus = useLicenseStatus;
217
+ exports.useWatermarkEnforcement = useWatermarkEnforcement;
218
+ //# sourceMappingURL=index.cjs.map
219
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+
3
+ type LicenseReason = 'invalid' | 'expired' | 'domain-mismatch';
4
+ interface LicenseStatus {
5
+ valid: boolean;
6
+ reason?: LicenseReason;
7
+ expiresAt?: Date;
8
+ domain?: string;
9
+ }
10
+ interface LicenseState {
11
+ status: LicenseStatus;
12
+ rawKey: string;
13
+ setAt: number;
14
+ }
15
+ /** 만료 경고 유형. 현재는 'soon-expiring' (60일 이내) 단일 값. */
16
+ type ExpiryWarning = 'soon-expiring';
17
+ /**
18
+ * `checkLicense()` 반환 타입.
19
+ * watermarkRequired: true → Pro grid 워터마크 표시 필요.
20
+ * expiryWarning: 'soon-expiring' → 60일 이내 만료 (console.warn 발생).
21
+ */
22
+ interface LicenseCheckResult {
23
+ valid: boolean;
24
+ watermarkRequired: boolean;
25
+ expiryWarning?: ExpiryWarning;
26
+ expiresAt?: Date;
27
+ reason?: LicenseReason;
28
+ }
29
+
30
+ /**
31
+ * Pro 패키지 전역 라이선스 등록 API.
32
+ * 앱 entry(main.tsx / App.tsx)에서 1회 호출.
33
+ * @param key - Base64url(pubKey).Base64url(sig).Base64url(payload) 형식 라이선스 키
34
+ * @returns LicenseStatus — 즉시 반환 (동기 wrapper, 내부 비동기 검증 완료 후 상태 갱신)
35
+ *
36
+ * 주의: 반환값은 Promise 없이 즉시 사용 가능하도록 동기 API로 설계.
37
+ * 내부적으로 verifySignature (async) 결과를 저장. 비동기 완료 전 getLicenseState() 호출 시
38
+ * 기본값 {valid:false, reason:'invalid'} 반환 (D6).
39
+ */
40
+ declare function setLicenseKey(key: string): LicenseStatus;
41
+
42
+ /**
43
+ * 현재 라이선스 상태를 동기 검사하여 `LicenseCheckResult`를 반환한다.
44
+ *
45
+ * - valid=false 이면 `watermarkRequired=true`.
46
+ * - 유효하고 `expiresAt`까지 60일 미만이면 `expiryWarning='soon-expiring'` + `console.warn` (1회).
47
+ * - 유효하고 만료 여유가 충분하면 `{ valid: true, watermarkRequired: false }`.
48
+ */
49
+ declare function checkLicense(): LicenseCheckResult;
50
+
51
+ interface WatermarkProps {
52
+ required: boolean;
53
+ }
54
+ /**
55
+ * Pro 라이선스가 없을 때 그리드 위에 표시되는 워터마크 컴포넌트.
56
+ *
57
+ * `required=false` 이면 `null` 반환 (렌더링 없음).
58
+ */
59
+ declare function Watermark({ required }: WatermarkProps): React.ReactElement | null;
60
+
61
+ /**
62
+ * React hook returning the current license check result. Re-renders when the
63
+ * license state changes (e.g. async `setLicenseKey` resolution).
64
+ *
65
+ * Backed by `useSyncExternalStore` — no tearing under React 18 concurrent mode.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * function MyGrid() {
70
+ * const lic = useLicenseStatus();
71
+ * return (
72
+ * <div className="relative">
73
+ * <table>{ ... }</table>
74
+ * {lic.watermarkRequired && <Watermark required />}
75
+ * </div>
76
+ * );
77
+ * }
78
+ * ```
79
+ */
80
+ declare function useLicenseStatus(): LicenseCheckResult;
81
+
82
+ /**
83
+ * Void registration hook for license watermark enforcement via a singleton
84
+ * portal mounted at `document.body`.
85
+ *
86
+ * - Each mount increments a module-level ref-count.
87
+ * - First mount creates the singleton portal + React root.
88
+ * - License state changes (`setLicenseKey`) re-render the portal via
89
+ * `subscribeLicense`.
90
+ * - Last unmount (ref-count → 0) tears down the portal.
91
+ *
92
+ * Use case: per-cell renderers (e.g. `DataMapCell`) where the component
93
+ * itself has no host DOM suitable for wrapper-based watermarking.
94
+ *
95
+ * SSR-safe: portal setup is skipped when `document` is undefined.
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * export function DataMapCell(info) {
100
+ * useWatermarkEnforcement(); // void — no return value
101
+ * return <span>{...}</span>;
102
+ * }
103
+ * ```
104
+ */
105
+ declare function useWatermarkEnforcement(): void;
106
+
107
+ type LicenseListener = () => void;
108
+ declare function setLicenseState(s: LicenseState): void;
109
+ /**
110
+ * Subscribe to license state changes. Listener is invoked synchronously
111
+ * after every `setLicenseState` call. Returns an unsubscribe function.
112
+ *
113
+ * Used internally by `useLicenseStatus` (via `useSyncExternalStore`) and
114
+ * by `useWatermarkEnforcement` (singleton portal re-render trigger).
115
+ */
116
+ declare function subscribeLicense(listener: LicenseListener): () => void;
117
+
118
+ export { type LicenseCheckResult, type LicenseReason, type LicenseStatus, Watermark, checkLicense, setLicenseKey, setLicenseState, subscribeLicense, useLicenseStatus, useWatermarkEnforcement };
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+
3
+ type LicenseReason = 'invalid' | 'expired' | 'domain-mismatch';
4
+ interface LicenseStatus {
5
+ valid: boolean;
6
+ reason?: LicenseReason;
7
+ expiresAt?: Date;
8
+ domain?: string;
9
+ }
10
+ interface LicenseState {
11
+ status: LicenseStatus;
12
+ rawKey: string;
13
+ setAt: number;
14
+ }
15
+ /** 만료 경고 유형. 현재는 'soon-expiring' (60일 이내) 단일 값. */
16
+ type ExpiryWarning = 'soon-expiring';
17
+ /**
18
+ * `checkLicense()` 반환 타입.
19
+ * watermarkRequired: true → Pro grid 워터마크 표시 필요.
20
+ * expiryWarning: 'soon-expiring' → 60일 이내 만료 (console.warn 발생).
21
+ */
22
+ interface LicenseCheckResult {
23
+ valid: boolean;
24
+ watermarkRequired: boolean;
25
+ expiryWarning?: ExpiryWarning;
26
+ expiresAt?: Date;
27
+ reason?: LicenseReason;
28
+ }
29
+
30
+ /**
31
+ * Pro 패키지 전역 라이선스 등록 API.
32
+ * 앱 entry(main.tsx / App.tsx)에서 1회 호출.
33
+ * @param key - Base64url(pubKey).Base64url(sig).Base64url(payload) 형식 라이선스 키
34
+ * @returns LicenseStatus — 즉시 반환 (동기 wrapper, 내부 비동기 검증 완료 후 상태 갱신)
35
+ *
36
+ * 주의: 반환값은 Promise 없이 즉시 사용 가능하도록 동기 API로 설계.
37
+ * 내부적으로 verifySignature (async) 결과를 저장. 비동기 완료 전 getLicenseState() 호출 시
38
+ * 기본값 {valid:false, reason:'invalid'} 반환 (D6).
39
+ */
40
+ declare function setLicenseKey(key: string): LicenseStatus;
41
+
42
+ /**
43
+ * 현재 라이선스 상태를 동기 검사하여 `LicenseCheckResult`를 반환한다.
44
+ *
45
+ * - valid=false 이면 `watermarkRequired=true`.
46
+ * - 유효하고 `expiresAt`까지 60일 미만이면 `expiryWarning='soon-expiring'` + `console.warn` (1회).
47
+ * - 유효하고 만료 여유가 충분하면 `{ valid: true, watermarkRequired: false }`.
48
+ */
49
+ declare function checkLicense(): LicenseCheckResult;
50
+
51
+ interface WatermarkProps {
52
+ required: boolean;
53
+ }
54
+ /**
55
+ * Pro 라이선스가 없을 때 그리드 위에 표시되는 워터마크 컴포넌트.
56
+ *
57
+ * `required=false` 이면 `null` 반환 (렌더링 없음).
58
+ */
59
+ declare function Watermark({ required }: WatermarkProps): React.ReactElement | null;
60
+
61
+ /**
62
+ * React hook returning the current license check result. Re-renders when the
63
+ * license state changes (e.g. async `setLicenseKey` resolution).
64
+ *
65
+ * Backed by `useSyncExternalStore` — no tearing under React 18 concurrent mode.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * function MyGrid() {
70
+ * const lic = useLicenseStatus();
71
+ * return (
72
+ * <div className="relative">
73
+ * <table>{ ... }</table>
74
+ * {lic.watermarkRequired && <Watermark required />}
75
+ * </div>
76
+ * );
77
+ * }
78
+ * ```
79
+ */
80
+ declare function useLicenseStatus(): LicenseCheckResult;
81
+
82
+ /**
83
+ * Void registration hook for license watermark enforcement via a singleton
84
+ * portal mounted at `document.body`.
85
+ *
86
+ * - Each mount increments a module-level ref-count.
87
+ * - First mount creates the singleton portal + React root.
88
+ * - License state changes (`setLicenseKey`) re-render the portal via
89
+ * `subscribeLicense`.
90
+ * - Last unmount (ref-count → 0) tears down the portal.
91
+ *
92
+ * Use case: per-cell renderers (e.g. `DataMapCell`) where the component
93
+ * itself has no host DOM suitable for wrapper-based watermarking.
94
+ *
95
+ * SSR-safe: portal setup is skipped when `document` is undefined.
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * export function DataMapCell(info) {
100
+ * useWatermarkEnforcement(); // void — no return value
101
+ * return <span>{...}</span>;
102
+ * }
103
+ * ```
104
+ */
105
+ declare function useWatermarkEnforcement(): void;
106
+
107
+ type LicenseListener = () => void;
108
+ declare function setLicenseState(s: LicenseState): void;
109
+ /**
110
+ * Subscribe to license state changes. Listener is invoked synchronously
111
+ * after every `setLicenseState` call. Returns an unsubscribe function.
112
+ *
113
+ * Used internally by `useLicenseStatus` (via `useSyncExternalStore`) and
114
+ * by `useWatermarkEnforcement` (singleton portal re-render trigger).
115
+ */
116
+ declare function subscribeLicense(listener: LicenseListener): () => void;
117
+
118
+ export { type LicenseCheckResult, type LicenseReason, type LicenseStatus, Watermark, checkLicense, setLicenseKey, setLicenseState, subscribeLicense, useLicenseStatus, useWatermarkEnforcement };
package/dist/index.mjs ADDED
@@ -0,0 +1,211 @@
1
+ import { useSyncExternalStore, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { createRoot } from 'react-dom/client';
4
+
5
+ // src/verifySignature.ts
6
+ function isKeyPayload(v) {
7
+ return typeof v === "object" && v !== null && typeof v["domain"] === "string" && typeof v["expiresAt"] === "number" && typeof v["tier"] === "string";
8
+ }
9
+ function base64urlToBytes(s) {
10
+ const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
11
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
12
+ const binary = atob(padded);
13
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
14
+ }
15
+ async function verifySignature(rawKey) {
16
+ const parts = rawKey.split(".");
17
+ if (parts.length !== 3) {
18
+ return { valid: false, ...{ reason: "invalid" } };
19
+ }
20
+ const [pubKeyB64, sigB64, payloadB64] = parts;
21
+ let payload;
22
+ try {
23
+ payload = JSON.parse(new TextDecoder().decode(base64urlToBytes(payloadB64)));
24
+ } catch {
25
+ return { valid: false, ...{ reason: "invalid" } };
26
+ }
27
+ if (!isKeyPayload(payload)) {
28
+ return { valid: false, ...{ reason: "invalid" } };
29
+ }
30
+ let cryptoSubtle;
31
+ try {
32
+ cryptoSubtle = crypto.subtle;
33
+ } catch {
34
+ return { valid: false, ...{ reason: "invalid" } };
35
+ }
36
+ let pubKey;
37
+ try {
38
+ pubKey = await cryptoSubtle.importKey(
39
+ "raw",
40
+ base64urlToBytes(pubKeyB64),
41
+ { name: "Ed25519" },
42
+ false,
43
+ ["verify"]
44
+ );
45
+ } catch {
46
+ return { valid: false, ...{ reason: "invalid" } };
47
+ }
48
+ const sigBytes = base64urlToBytes(sigB64);
49
+ const msgBytes = base64urlToBytes(payloadB64);
50
+ let sigOk;
51
+ try {
52
+ sigOk = await cryptoSubtle.verify("Ed25519", pubKey, sigBytes, msgBytes);
53
+ } catch {
54
+ return { valid: false, ...{ reason: "invalid" } };
55
+ }
56
+ if (!sigOk) {
57
+ return { valid: false, ...{ reason: "invalid" } };
58
+ }
59
+ const now = Date.now();
60
+ if (payload.expiresAt < now) {
61
+ return {
62
+ valid: false,
63
+ ...{ reason: "expired" },
64
+ expiresAt: new Date(payload.expiresAt),
65
+ domain: payload.domain
66
+ };
67
+ }
68
+ let hostname = null;
69
+ if (typeof window !== "undefined") {
70
+ hostname = window.location.hostname;
71
+ }
72
+ if (hostname !== null) {
73
+ const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1";
74
+ if (!isLocalhost && hostname !== payload.domain) {
75
+ return {
76
+ valid: false,
77
+ ...{ reason: "domain-mismatch" },
78
+ expiresAt: new Date(payload.expiresAt),
79
+ domain: payload.domain
80
+ };
81
+ }
82
+ }
83
+ return {
84
+ valid: true,
85
+ expiresAt: new Date(payload.expiresAt),
86
+ domain: payload.domain
87
+ };
88
+ }
89
+
90
+ // src/state.ts
91
+ var _state = null;
92
+ var _listeners = /* @__PURE__ */ new Set();
93
+ var _cachedCheck = null;
94
+ function setLicenseState(s) {
95
+ _state = s;
96
+ _cachedCheck = null;
97
+ _listeners.forEach((l) => l());
98
+ }
99
+ function getLicenseState() {
100
+ if (_state === null) {
101
+ return { valid: false, ...{ reason: "invalid" } };
102
+ }
103
+ return _state.status;
104
+ }
105
+ function getCachedCheck(compute) {
106
+ if (_cachedCheck === null) _cachedCheck = compute();
107
+ return _cachedCheck;
108
+ }
109
+ function subscribeLicense(listener) {
110
+ _listeners.add(listener);
111
+ return () => {
112
+ _listeners.delete(listener);
113
+ };
114
+ }
115
+
116
+ // src/setLicenseKey.ts
117
+ function setLicenseKey(key) {
118
+ const pending = { valid: false };
119
+ verifySignature(key).then((status) => {
120
+ setLicenseState({ status, rawKey: key, setAt: Date.now() });
121
+ }).catch(() => {
122
+ setLicenseState({
123
+ status: { valid: false, ...{ reason: "invalid" } },
124
+ rawKey: key,
125
+ setAt: Date.now()
126
+ });
127
+ });
128
+ return pending;
129
+ }
130
+
131
+ // src/checkLicense.ts
132
+ var SIXTY_DAYS_MS = 60 * 24 * 3600 * 1e3;
133
+ var warned = false;
134
+ function checkLicense() {
135
+ const status = getLicenseState();
136
+ if (!status.valid) {
137
+ const result = { valid: false, watermarkRequired: true };
138
+ if (status.reason !== void 0) result.reason = status.reason;
139
+ if (status.expiresAt !== void 0) result.expiresAt = status.expiresAt;
140
+ return result;
141
+ }
142
+ if (status.expiresAt !== void 0) {
143
+ const msLeft = status.expiresAt.getTime() - Date.now();
144
+ if (msLeft < SIXTY_DAYS_MS) {
145
+ if (!warned) {
146
+ console.warn(
147
+ `[grid-license] \uB77C\uC774\uC120\uC2A4\uAC00 ${Math.ceil(msLeft / (24 * 3600 * 1e3))}\uC77C \uD6C4 \uB9CC\uB8CC\uB429\uB2C8\uB2E4.`
148
+ );
149
+ warned = true;
150
+ }
151
+ return {
152
+ valid: true,
153
+ watermarkRequired: false,
154
+ expiryWarning: "soon-expiring",
155
+ expiresAt: status.expiresAt
156
+ };
157
+ }
158
+ }
159
+ return { valid: true, watermarkRequired: false };
160
+ }
161
+ function Watermark({ required }) {
162
+ if (!required) return null;
163
+ return /* @__PURE__ */ jsx("div", { className: "absolute top-0 right-0 opacity-40 pointer-events-none select-none text-sm font-semibold text-gray-500 p-2", children: "Unlicensed @topgrid/grid" });
164
+ }
165
+ var getSnapshot = () => getCachedCheck(checkLicense);
166
+ function useLicenseStatus() {
167
+ return useSyncExternalStore(subscribeLicense, getSnapshot, getSnapshot);
168
+ }
169
+ var _activeCount = 0;
170
+ var _portalContainer = null;
171
+ var _portalRoot = null;
172
+ var _unsubLicense = null;
173
+ function renderWatermark() {
174
+ if (_portalRoot === null || typeof document === "undefined") return;
175
+ const lic = checkLicense();
176
+ _portalRoot.render(lic.watermarkRequired ? /* @__PURE__ */ jsx(Watermark, { required: true }) : null);
177
+ }
178
+ function mountPortal() {
179
+ if (typeof document === "undefined") return;
180
+ if (_portalContainer !== null) return;
181
+ _portalContainer = document.createElement("div");
182
+ _portalContainer.setAttribute("data-tomis-watermark", "");
183
+ document.body.appendChild(_portalContainer);
184
+ _portalRoot = createRoot(_portalContainer);
185
+ renderWatermark();
186
+ _unsubLicense = subscribeLicense(renderWatermark);
187
+ }
188
+ function unmountPortal() {
189
+ if (_unsubLicense !== null) _unsubLicense();
190
+ if (_portalRoot !== null) _portalRoot.unmount();
191
+ if (_portalContainer !== null && _portalContainer.parentNode !== null) {
192
+ _portalContainer.parentNode.removeChild(_portalContainer);
193
+ }
194
+ _portalRoot = null;
195
+ _portalContainer = null;
196
+ _unsubLicense = null;
197
+ }
198
+ function useWatermarkEnforcement() {
199
+ useEffect(() => {
200
+ _activeCount += 1;
201
+ if (_activeCount === 1) mountPortal();
202
+ return () => {
203
+ _activeCount = Math.max(0, _activeCount - 1);
204
+ if (_activeCount === 0) unmountPortal();
205
+ };
206
+ }, []);
207
+ }
208
+
209
+ export { Watermark, checkLicense, setLicenseKey, setLicenseState, subscribeLicense, useLicenseStatus, useWatermarkEnforcement };
210
+ //# sourceMappingURL=index.mjs.map
211
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@topgrid/grid-license",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "SEE LICENSE IN EULA",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "description": "Pro license validation runtime",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.mjs",
17
+ "require": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "peerDependencies": {
25
+ "react": "^18.0.0 || ^19.0.0",
26
+ "react-dom": "^18.0.0 || ^19.0.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "echo TODO"
32
+ }
33
+ }