@infuro/cms-core 1.0.14 → 1.0.16

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/dist/hooks.cjs CHANGED
@@ -30,8 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/hooks/index.ts
31
31
  var hooks_exports = {};
32
32
  __export(hooks_exports, {
33
+ CaptchaProvider: () => CaptchaProvider,
33
34
  PluginProvider: () => PluginProvider,
34
35
  useAnalytics: () => useAnalytics,
36
+ useCaptchaPayload: () => useCaptchaPayload,
35
37
  useIsMobile: () => useIsMobile,
36
38
  usePlugin: () => usePlugin
37
39
  });
@@ -84,10 +86,167 @@ function usePlugin(name) {
84
86
  if (!ctx) return void 0;
85
87
  return ctx.getPlugin(name);
86
88
  }
89
+
90
+ // src/components/CaptchaProvider.tsx
91
+ var import_react4 = require("react");
92
+ var import_jsx_runtime = require("react/jsx-runtime");
93
+ var CaptchaReactContext = (0, import_react4.createContext)(null);
94
+ function loadScript(src) {
95
+ return new Promise((resolve, reject) => {
96
+ const base = src.split("?")[0] ?? src;
97
+ const existing = Array.from(document.querySelectorAll("script[src]")).some(
98
+ (el) => el.src.startsWith(base)
99
+ );
100
+ if (existing) {
101
+ resolve();
102
+ return;
103
+ }
104
+ const s = document.createElement("script");
105
+ s.src = src;
106
+ s.async = true;
107
+ s.onload = () => resolve();
108
+ s.onerror = () => reject(new Error("Failed to load script"));
109
+ document.head.appendChild(s);
110
+ });
111
+ }
112
+ function CaptchaProvider({ children }) {
113
+ const [config, setConfig] = (0, import_react4.useState)(null);
114
+ const [selectedProvider, setSelectedProvider] = (0, import_react4.useState)(null);
115
+ const [ready, setReady] = (0, import_react4.useState)(false);
116
+ const turnstileWidgetIdRef = (0, import_react4.useRef)(null);
117
+ const turnstileContainerRef = (0, import_react4.useRef)(null);
118
+ const turnstileTokenRef = (0, import_react4.useRef)(null);
119
+ (0, import_react4.useEffect)(() => {
120
+ let cancelled = false;
121
+ fetch("/api/public/captcha-config").then((r) => r.ok ? r.json() : null).then((c) => {
122
+ if (cancelled || !c) return;
123
+ setConfig(c);
124
+ if (c.enabled && c.activeProvider) setSelectedProvider(c.activeProvider);
125
+ }).catch(() => {
126
+ }).finally(() => {
127
+ if (!cancelled) setReady(true);
128
+ });
129
+ return () => {
130
+ cancelled = true;
131
+ };
132
+ }, []);
133
+ (0, import_react4.useEffect)(() => {
134
+ if (!config?.enabled || !selectedProvider) return;
135
+ const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;
136
+ if (!siteKeyFor) return;
137
+ let cancelled = false;
138
+ (async () => {
139
+ try {
140
+ if (selectedProvider === "recaptcha_v3") {
141
+ await loadScript(
142
+ `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKeyFor)}`
143
+ );
144
+ } else {
145
+ await loadScript("https://challenges.cloudflare.com/turnstile/v0/api.js");
146
+ if (cancelled || !turnstileContainerRef.current || !window.turnstile) return;
147
+ if (turnstileWidgetIdRef.current) {
148
+ try {
149
+ window.turnstile.remove(turnstileWidgetIdRef.current);
150
+ } catch {
151
+ }
152
+ turnstileWidgetIdRef.current = null;
153
+ }
154
+ const wid = window.turnstile.render(turnstileContainerRef.current, {
155
+ sitekey: siteKeyFor,
156
+ execution: "execute",
157
+ callback: (token) => {
158
+ const p = turnstileTokenRef.current;
159
+ turnstileTokenRef.current = null;
160
+ p?.resolve(token);
161
+ },
162
+ "error-callback": () => {
163
+ const p = turnstileTokenRef.current;
164
+ turnstileTokenRef.current = null;
165
+ p?.reject(new Error("Turnstile error"));
166
+ },
167
+ "expired-callback": () => {
168
+ const p = turnstileTokenRef.current;
169
+ turnstileTokenRef.current = null;
170
+ p?.reject(new Error("Turnstile expired"));
171
+ }
172
+ });
173
+ turnstileWidgetIdRef.current = wid;
174
+ }
175
+ } catch {
176
+ }
177
+ })();
178
+ return () => {
179
+ cancelled = true;
180
+ };
181
+ }, [config, selectedProvider]);
182
+ const getCaptchaPayload = (0, import_react4.useCallback)(async () => {
183
+ if (!config?.enabled || !selectedProvider) return {};
184
+ const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;
185
+ if (!siteKeyFor) return {};
186
+ if (selectedProvider === "recaptcha_v3") {
187
+ if (!window.grecaptcha) return {};
188
+ const token2 = await new Promise((resolve, reject) => {
189
+ window.grecaptcha.ready(() => {
190
+ window.grecaptcha.execute(siteKeyFor, { action: "submit" }).then(resolve).catch(reject);
191
+ });
192
+ });
193
+ return { captchaToken: token2, captchaProvider: "recaptcha_v3" };
194
+ }
195
+ const wid = turnstileWidgetIdRef.current;
196
+ if (!wid || !window.turnstile) return {};
197
+ const token = await new Promise((resolve, reject) => {
198
+ turnstileTokenRef.current = { resolve, reject };
199
+ try {
200
+ window.turnstile.reset(wid);
201
+ window.turnstile.execute(wid);
202
+ } catch (e) {
203
+ turnstileTokenRef.current = null;
204
+ reject(e instanceof Error ? e : new Error("Turnstile execute failed"));
205
+ }
206
+ });
207
+ return { captchaToken: token, captchaProvider: "turnstile" };
208
+ }, [config, selectedProvider]);
209
+ const ctx = (0, import_react4.useMemo)(
210
+ () => ({
211
+ config,
212
+ ready,
213
+ selectedProvider,
214
+ setSelectedProvider,
215
+ getCaptchaPayload
216
+ }),
217
+ [config, ready, selectedProvider, getCaptchaPayload]
218
+ );
219
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CaptchaReactContext.Provider, { value: ctx, children: [
220
+ config?.enabled && config.multipleProviders ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-2 text-sm text-muted-foreground max-w-md", children: [
221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "mr-2", htmlFor: "captcha-provider-select", children: "Verification" }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
223
+ "select",
224
+ {
225
+ id: "captcha-provider-select",
226
+ className: "border rounded px-2 py-1 bg-background",
227
+ value: selectedProvider ?? "",
228
+ onChange: (e) => setSelectedProvider(e.target.value),
229
+ children: config.availableProviders.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: p.id, children: p.id === "turnstile" ? "Cloudflare Turnstile" : "Google reCAPTCHA v3" }, p.id))
230
+ }
231
+ )
232
+ ] }) : null,
233
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: turnstileContainerRef, className: "hidden", "aria-hidden": true }),
234
+ children
235
+ ] });
236
+ }
237
+ function useCaptchaPayload() {
238
+ const ctx = (0, import_react4.useContext)(CaptchaReactContext);
239
+ return (0, import_react4.useCallback)(async () => {
240
+ if (!ctx?.config?.enabled || !ctx.ready) return {};
241
+ return ctx.getCaptchaPayload();
242
+ }, [ctx]);
243
+ }
87
244
  // Annotate the CommonJS export names for ESM import in node:
88
245
  0 && (module.exports = {
246
+ CaptchaProvider,
89
247
  PluginProvider,
90
248
  useAnalytics,
249
+ useCaptchaPayload,
91
250
  useIsMobile,
92
251
  usePlugin
93
252
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/index.ts","../src/hooks/use-mobile.ts","../src/hooks/use-analytics.ts","../src/hooks/use-plugin.ts"],"sourcesContent":["export { useIsMobile } from './use-mobile';\nexport { useAnalytics } from './use-analytics';\nexport { usePlugin, PluginProvider } from './use-plugin';\n","import { useState, useEffect } from 'react';\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile(): boolean {\n const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);\n\n useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n mql.addEventListener('change', onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener('change', onChange);\n }, []);\n\n return !!isMobile;\n}\n","import { useEffect } from 'react';\n\n/**\n * Call from app with pathname from usePathname() (next/navigation).\n * Disables tracking on admin routes when pathname starts with /admin.\n */\nexport function useAnalytics(pathname: string | null): { isAdminRoute: boolean; shouldTrack: boolean } {\n const isAdminRoute = pathname != null && pathname.startsWith('/admin');\n\n useEffect(() => {\n if (isAdminRoute && typeof window !== 'undefined') {\n if (typeof (window as unknown as { gtag?: () => void }).gtag === 'function') {\n (window as unknown as { gtag: () => void }).gtag = () => {};\n }\n if (Array.isArray((window as unknown as { dataLayer?: unknown[] }).dataLayer)) {\n (window as unknown as { dataLayer: unknown[] }).dataLayer = [];\n }\n }\n }, [isAdminRoute]);\n\n return { isAdminRoute, shouldTrack: !isAdminRoute };\n}\n","import React, { createContext, useContext, type ReactNode } from 'react';\n\nconst PluginContext = createContext<{ getPlugin: <T>(name: string) => T | undefined } | null>(null);\n\nexport function PluginProvider({\n getPlugin,\n children,\n}: {\n getPlugin: <T>(name: string) => T | undefined;\n children: ReactNode;\n}) {\n return React.createElement(PluginContext.Provider, { value: { getPlugin } }, children);\n}\n\nexport function usePlugin<T = unknown>(name: string): T | undefined {\n const ctx = useContext(PluginContext);\n if (!ctx) return undefined;\n return ctx.getPlugin<T>(name);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAoC;AAEpC,IAAM,oBAAoB;AAEnB,SAAS,cAAuB;AACrC,QAAM,CAAC,UAAU,WAAW,QAAI,uBAA8B,MAAS;AAEvE,8BAAU,MAAM;AACd,UAAM,MAAM,OAAO,WAAW,eAAe,oBAAoB,CAAC,KAAK;AACvE,UAAM,WAAW,MAAM,YAAY,OAAO,aAAa,iBAAiB;AACxE,QAAI,iBAAiB,UAAU,QAAQ;AACvC,gBAAY,OAAO,aAAa,iBAAiB;AACjD,WAAO,MAAM,IAAI,oBAAoB,UAAU,QAAQ;AAAA,EACzD,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,CAAC;AACX;;;AChBA,IAAAA,gBAA0B;AAMnB,SAAS,aAAa,UAA0E;AACrG,QAAM,eAAe,YAAY,QAAQ,SAAS,WAAW,QAAQ;AAErE,+BAAU,MAAM;AACd,QAAI,gBAAgB,OAAO,WAAW,aAAa;AACjD,UAAI,OAAQ,OAA4C,SAAS,YAAY;AAC3E,QAAC,OAA2C,OAAO,MAAM;AAAA,QAAC;AAAA,MAC5D;AACA,UAAI,MAAM,QAAS,OAAgD,SAAS,GAAG;AAC7E,QAAC,OAA+C,YAAY,CAAC;AAAA,MAC/D;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO,EAAE,cAAc,aAAa,CAAC,aAAa;AACpD;;;ACrBA,IAAAC,gBAAiE;AAEjE,IAAM,oBAAgB,6BAAwE,IAAI;AAE3F,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AACF,GAGG;AACD,SAAO,cAAAC,QAAM,cAAc,cAAc,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,QAAQ;AACvF;AAEO,SAAS,UAAuB,MAA6B;AAClE,QAAM,UAAM,0BAAW,aAAa;AACpC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,UAAa,IAAI;AAC9B;","names":["import_react","import_react","React"]}
1
+ {"version":3,"sources":["../src/hooks/index.ts","../src/hooks/use-mobile.ts","../src/hooks/use-analytics.ts","../src/hooks/use-plugin.ts","../src/components/CaptchaProvider.tsx"],"sourcesContent":["export { useIsMobile } from './use-mobile';\nexport { useAnalytics } from './use-analytics';\nexport { usePlugin, PluginProvider } from './use-plugin';\nexport { CaptchaProvider, useCaptchaPayload } from '../components/CaptchaProvider';\n","import { useState, useEffect } from 'react';\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile(): boolean {\n const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);\n\n useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n mql.addEventListener('change', onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener('change', onChange);\n }, []);\n\n return !!isMobile;\n}\n","import { useEffect } from 'react';\n\n/**\n * Call from app with pathname from usePathname() (next/navigation).\n * Disables tracking on admin routes when pathname starts with /admin.\n */\nexport function useAnalytics(pathname: string | null): { isAdminRoute: boolean; shouldTrack: boolean } {\n const isAdminRoute = pathname != null && pathname.startsWith('/admin');\n\n useEffect(() => {\n if (isAdminRoute && typeof window !== 'undefined') {\n if (typeof (window as unknown as { gtag?: () => void }).gtag === 'function') {\n (window as unknown as { gtag: () => void }).gtag = () => {};\n }\n if (Array.isArray((window as unknown as { dataLayer?: unknown[] }).dataLayer)) {\n (window as unknown as { dataLayer: unknown[] }).dataLayer = [];\n }\n }\n }, [isAdminRoute]);\n\n return { isAdminRoute, shouldTrack: !isAdminRoute };\n}\n","import React, { createContext, useContext, type ReactNode } from 'react';\n\nconst PluginContext = createContext<{ getPlugin: <T>(name: string) => T | undefined } | null>(null);\n\nexport function PluginProvider({\n getPlugin,\n children,\n}: {\n getPlugin: <T>(name: string) => T | undefined;\n children: ReactNode;\n}) {\n return React.createElement(PluginContext.Provider, { value: { getPlugin } }, children);\n}\n\nexport function usePlugin<T = unknown>(name: string): T | undefined {\n const ctx = useContext(PluginContext);\n if (!ctx) return undefined;\n return ctx.getPlugin<T>(name);\n}\n","'use client';\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport type { CaptchaPublicConfig, CaptchaProviderId } from '../plugins/captcha';\n\ndeclare global {\n interface Window {\n turnstile?: {\n render: (el: HTMLElement | string, opts: Record<string, unknown>) => string;\n execute: (id: string) => void;\n reset: (id: string) => void;\n remove: (id: string) => void;\n };\n grecaptcha?: {\n ready: (fn: () => void) => void;\n execute: (siteKey: string, opts: { action: string }) => Promise<string>;\n };\n }\n}\n\ntype CaptchaCtx = {\n config: CaptchaPublicConfig | null;\n ready: boolean;\n selectedProvider: CaptchaProviderId | null;\n setSelectedProvider: (p: CaptchaProviderId) => void;\n getCaptchaPayload: () => Promise<Record<string, string>>;\n};\n\nconst CaptchaReactContext = createContext<CaptchaCtx | null>(null);\n\nfunction loadScript(src: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const base = src.split('?')[0] ?? src;\n const existing = Array.from(document.querySelectorAll('script[src]')).some((el) =>\n (el as HTMLScriptElement).src.startsWith(base)\n );\n if (existing) {\n resolve();\n return;\n }\n const s = document.createElement('script');\n s.src = src;\n s.async = true;\n s.onload = () => resolve();\n s.onerror = () => reject(new Error('Failed to load script'));\n document.head.appendChild(s);\n });\n}\n\nexport function CaptchaProvider({ children }: { children: React.ReactNode }) {\n const [config, setConfig] = useState<CaptchaPublicConfig | null>(null);\n const [selectedProvider, setSelectedProvider] = useState<CaptchaProviderId | null>(null);\n const [ready, setReady] = useState(false);\n const turnstileWidgetIdRef = useRef<string | null>(null);\n const turnstileContainerRef = useRef<HTMLDivElement | null>(null);\n const turnstileTokenRef = useRef<{\n resolve: (t: string) => void;\n reject: (e: Error) => void;\n } | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n fetch('/api/public/captcha-config')\n .then((r) => (r.ok ? r.json() : null))\n .then((c: CaptchaPublicConfig | null) => {\n if (cancelled || !c) return;\n setConfig(c);\n if (c.enabled && c.activeProvider) setSelectedProvider(c.activeProvider);\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setReady(true);\n });\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n if (!config?.enabled || !selectedProvider) return;\n const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;\n if (!siteKeyFor) return;\n\n let cancelled = false;\n (async () => {\n try {\n if (selectedProvider === 'recaptcha_v3') {\n await loadScript(\n `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKeyFor)}`\n );\n } else {\n await loadScript('https://challenges.cloudflare.com/turnstile/v0/api.js');\n if (cancelled || !turnstileContainerRef.current || !window.turnstile) return;\n if (turnstileWidgetIdRef.current) {\n try {\n window.turnstile.remove(turnstileWidgetIdRef.current);\n } catch {\n /* ignore */\n }\n turnstileWidgetIdRef.current = null;\n }\n const wid = window.turnstile.render(turnstileContainerRef.current, {\n sitekey: siteKeyFor,\n execution: 'execute',\n callback: (token: string) => {\n const p = turnstileTokenRef.current;\n turnstileTokenRef.current = null;\n p?.resolve(token);\n },\n 'error-callback': () => {\n const p = turnstileTokenRef.current;\n turnstileTokenRef.current = null;\n p?.reject(new Error('Turnstile error'));\n },\n 'expired-callback': () => {\n const p = turnstileTokenRef.current;\n turnstileTokenRef.current = null;\n p?.reject(new Error('Turnstile expired'));\n },\n });\n turnstileWidgetIdRef.current = wid;\n }\n } catch {\n /* ignore */\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [config, selectedProvider]);\n\n const getCaptchaPayload = useCallback(async (): Promise<Record<string, string>> => {\n if (!config?.enabled || !selectedProvider) return {};\n const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;\n if (!siteKeyFor) return {};\n\n if (selectedProvider === 'recaptcha_v3') {\n if (!window.grecaptcha) return {};\n const token = await new Promise<string>((resolve, reject) => {\n window.grecaptcha!.ready(() => {\n window.grecaptcha!.execute(siteKeyFor, { action: 'submit' }).then(resolve).catch(reject);\n });\n });\n return { captchaToken: token, captchaProvider: 'recaptcha_v3' };\n }\n\n const wid = turnstileWidgetIdRef.current;\n if (!wid || !window.turnstile) return {};\n const token = await new Promise<string>((resolve, reject) => {\n turnstileTokenRef.current = { resolve, reject };\n try {\n window.turnstile!.reset(wid);\n window.turnstile!.execute(wid);\n } catch (e) {\n turnstileTokenRef.current = null;\n reject(e instanceof Error ? e : new Error('Turnstile execute failed'));\n }\n });\n return { captchaToken: token, captchaProvider: 'turnstile' };\n }, [config, selectedProvider]);\n\n const ctx = useMemo<CaptchaCtx>(\n () => ({\n config,\n ready,\n selectedProvider,\n setSelectedProvider,\n getCaptchaPayload,\n }),\n [config, ready, selectedProvider, getCaptchaPayload]\n );\n\n return (\n <CaptchaReactContext.Provider value={ctx}>\n {config?.enabled && config.multipleProviders ? (\n <div className=\"mb-2 text-sm text-muted-foreground max-w-md\">\n <label className=\"mr-2\" htmlFor=\"captcha-provider-select\">\n Verification\n </label>\n <select\n id=\"captcha-provider-select\"\n className=\"border rounded px-2 py-1 bg-background\"\n value={selectedProvider ?? ''}\n onChange={(e) => setSelectedProvider(e.target.value as CaptchaProviderId)}\n >\n {config.availableProviders.map((p) => (\n <option key={p.id} value={p.id}>\n {p.id === 'turnstile' ? 'Cloudflare Turnstile' : 'Google reCAPTCHA v3'}\n </option>\n ))}\n </select>\n </div>\n ) : null}\n <div ref={turnstileContainerRef} className=\"hidden\" aria-hidden />\n {children}\n </CaptchaReactContext.Provider>\n );\n}\n\n/** Returns a function that resolves to captcha fields for JSON bodies, or {} if disabled / not ready. */\nexport function useCaptchaPayload(): () => Promise<Record<string, string>> {\n const ctx = useContext(CaptchaReactContext);\n return useCallback(async () => {\n if (!ctx?.config?.enabled || !ctx.ready) return {};\n return ctx.getCaptchaPayload();\n }, [ctx]);\n}\n"],"mappings":"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","names":["import_react","import_react","React","import_react","token"]}
package/dist/hooks.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React, { ReactNode } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
3
 
3
4
  declare function useIsMobile(): boolean;
4
5
 
@@ -19,4 +20,26 @@ declare function PluginProvider({ getPlugin, children, }: {
19
20
  } | null>>;
20
21
  declare function usePlugin<T = unknown>(name: string): T | undefined;
21
22
 
22
- export { PluginProvider, useAnalytics, useIsMobile, usePlugin };
23
+ declare global {
24
+ interface Window {
25
+ turnstile?: {
26
+ render: (el: HTMLElement | string, opts: Record<string, unknown>) => string;
27
+ execute: (id: string) => void;
28
+ reset: (id: string) => void;
29
+ remove: (id: string) => void;
30
+ };
31
+ grecaptcha?: {
32
+ ready: (fn: () => void) => void;
33
+ execute: (siteKey: string, opts: {
34
+ action: string;
35
+ }) => Promise<string>;
36
+ };
37
+ }
38
+ }
39
+ declare function CaptchaProvider({ children }: {
40
+ children: React.ReactNode;
41
+ }): react_jsx_runtime.JSX.Element;
42
+ /** Returns a function that resolves to captcha fields for JSON bodies, or {} if disabled / not ready. */
43
+ declare function useCaptchaPayload(): () => Promise<Record<string, string>>;
44
+
45
+ export { CaptchaProvider, PluginProvider, useAnalytics, useCaptchaPayload, useIsMobile, usePlugin };
package/dist/hooks.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React, { ReactNode } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
3
 
3
4
  declare function useIsMobile(): boolean;
4
5
 
@@ -19,4 +20,26 @@ declare function PluginProvider({ getPlugin, children, }: {
19
20
  } | null>>;
20
21
  declare function usePlugin<T = unknown>(name: string): T | undefined;
21
22
 
22
- export { PluginProvider, useAnalytics, useIsMobile, usePlugin };
23
+ declare global {
24
+ interface Window {
25
+ turnstile?: {
26
+ render: (el: HTMLElement | string, opts: Record<string, unknown>) => string;
27
+ execute: (id: string) => void;
28
+ reset: (id: string) => void;
29
+ remove: (id: string) => void;
30
+ };
31
+ grecaptcha?: {
32
+ ready: (fn: () => void) => void;
33
+ execute: (siteKey: string, opts: {
34
+ action: string;
35
+ }) => Promise<string>;
36
+ };
37
+ }
38
+ }
39
+ declare function CaptchaProvider({ children }: {
40
+ children: React.ReactNode;
41
+ }): react_jsx_runtime.JSX.Element;
42
+ /** Returns a function that resolves to captcha fields for JSON bodies, or {} if disabled / not ready. */
43
+ declare function useCaptchaPayload(): () => Promise<Record<string, string>>;
44
+
45
+ export { CaptchaProvider, PluginProvider, useAnalytics, useCaptchaPayload, useIsMobile, usePlugin };
package/dist/hooks.js CHANGED
@@ -45,9 +45,174 @@ function usePlugin(name) {
45
45
  if (!ctx) return void 0;
46
46
  return ctx.getPlugin(name);
47
47
  }
48
+
49
+ // src/components/CaptchaProvider.tsx
50
+ import {
51
+ createContext as createContext2,
52
+ useCallback,
53
+ useContext as useContext2,
54
+ useEffect as useEffect3,
55
+ useMemo,
56
+ useRef,
57
+ useState as useState2
58
+ } from "react";
59
+ import { jsx, jsxs } from "react/jsx-runtime";
60
+ var CaptchaReactContext = createContext2(null);
61
+ function loadScript(src) {
62
+ return new Promise((resolve, reject) => {
63
+ const base = src.split("?")[0] ?? src;
64
+ const existing = Array.from(document.querySelectorAll("script[src]")).some(
65
+ (el) => el.src.startsWith(base)
66
+ );
67
+ if (existing) {
68
+ resolve();
69
+ return;
70
+ }
71
+ const s = document.createElement("script");
72
+ s.src = src;
73
+ s.async = true;
74
+ s.onload = () => resolve();
75
+ s.onerror = () => reject(new Error("Failed to load script"));
76
+ document.head.appendChild(s);
77
+ });
78
+ }
79
+ function CaptchaProvider({ children }) {
80
+ const [config, setConfig] = useState2(null);
81
+ const [selectedProvider, setSelectedProvider] = useState2(null);
82
+ const [ready, setReady] = useState2(false);
83
+ const turnstileWidgetIdRef = useRef(null);
84
+ const turnstileContainerRef = useRef(null);
85
+ const turnstileTokenRef = useRef(null);
86
+ useEffect3(() => {
87
+ let cancelled = false;
88
+ fetch("/api/public/captcha-config").then((r) => r.ok ? r.json() : null).then((c) => {
89
+ if (cancelled || !c) return;
90
+ setConfig(c);
91
+ if (c.enabled && c.activeProvider) setSelectedProvider(c.activeProvider);
92
+ }).catch(() => {
93
+ }).finally(() => {
94
+ if (!cancelled) setReady(true);
95
+ });
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, []);
100
+ useEffect3(() => {
101
+ if (!config?.enabled || !selectedProvider) return;
102
+ const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;
103
+ if (!siteKeyFor) return;
104
+ let cancelled = false;
105
+ (async () => {
106
+ try {
107
+ if (selectedProvider === "recaptcha_v3") {
108
+ await loadScript(
109
+ `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKeyFor)}`
110
+ );
111
+ } else {
112
+ await loadScript("https://challenges.cloudflare.com/turnstile/v0/api.js");
113
+ if (cancelled || !turnstileContainerRef.current || !window.turnstile) return;
114
+ if (turnstileWidgetIdRef.current) {
115
+ try {
116
+ window.turnstile.remove(turnstileWidgetIdRef.current);
117
+ } catch {
118
+ }
119
+ turnstileWidgetIdRef.current = null;
120
+ }
121
+ const wid = window.turnstile.render(turnstileContainerRef.current, {
122
+ sitekey: siteKeyFor,
123
+ execution: "execute",
124
+ callback: (token) => {
125
+ const p = turnstileTokenRef.current;
126
+ turnstileTokenRef.current = null;
127
+ p?.resolve(token);
128
+ },
129
+ "error-callback": () => {
130
+ const p = turnstileTokenRef.current;
131
+ turnstileTokenRef.current = null;
132
+ p?.reject(new Error("Turnstile error"));
133
+ },
134
+ "expired-callback": () => {
135
+ const p = turnstileTokenRef.current;
136
+ turnstileTokenRef.current = null;
137
+ p?.reject(new Error("Turnstile expired"));
138
+ }
139
+ });
140
+ turnstileWidgetIdRef.current = wid;
141
+ }
142
+ } catch {
143
+ }
144
+ })();
145
+ return () => {
146
+ cancelled = true;
147
+ };
148
+ }, [config, selectedProvider]);
149
+ const getCaptchaPayload = useCallback(async () => {
150
+ if (!config?.enabled || !selectedProvider) return {};
151
+ const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;
152
+ if (!siteKeyFor) return {};
153
+ if (selectedProvider === "recaptcha_v3") {
154
+ if (!window.grecaptcha) return {};
155
+ const token2 = await new Promise((resolve, reject) => {
156
+ window.grecaptcha.ready(() => {
157
+ window.grecaptcha.execute(siteKeyFor, { action: "submit" }).then(resolve).catch(reject);
158
+ });
159
+ });
160
+ return { captchaToken: token2, captchaProvider: "recaptcha_v3" };
161
+ }
162
+ const wid = turnstileWidgetIdRef.current;
163
+ if (!wid || !window.turnstile) return {};
164
+ const token = await new Promise((resolve, reject) => {
165
+ turnstileTokenRef.current = { resolve, reject };
166
+ try {
167
+ window.turnstile.reset(wid);
168
+ window.turnstile.execute(wid);
169
+ } catch (e) {
170
+ turnstileTokenRef.current = null;
171
+ reject(e instanceof Error ? e : new Error("Turnstile execute failed"));
172
+ }
173
+ });
174
+ return { captchaToken: token, captchaProvider: "turnstile" };
175
+ }, [config, selectedProvider]);
176
+ const ctx = useMemo(
177
+ () => ({
178
+ config,
179
+ ready,
180
+ selectedProvider,
181
+ setSelectedProvider,
182
+ getCaptchaPayload
183
+ }),
184
+ [config, ready, selectedProvider, getCaptchaPayload]
185
+ );
186
+ return /* @__PURE__ */ jsxs(CaptchaReactContext.Provider, { value: ctx, children: [
187
+ config?.enabled && config.multipleProviders ? /* @__PURE__ */ jsxs("div", { className: "mb-2 text-sm text-muted-foreground max-w-md", children: [
188
+ /* @__PURE__ */ jsx("label", { className: "mr-2", htmlFor: "captcha-provider-select", children: "Verification" }),
189
+ /* @__PURE__ */ jsx(
190
+ "select",
191
+ {
192
+ id: "captcha-provider-select",
193
+ className: "border rounded px-2 py-1 bg-background",
194
+ value: selectedProvider ?? "",
195
+ onChange: (e) => setSelectedProvider(e.target.value),
196
+ children: config.availableProviders.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.id === "turnstile" ? "Cloudflare Turnstile" : "Google reCAPTCHA v3" }, p.id))
197
+ }
198
+ )
199
+ ] }) : null,
200
+ /* @__PURE__ */ jsx("div", { ref: turnstileContainerRef, className: "hidden", "aria-hidden": true }),
201
+ children
202
+ ] });
203
+ }
204
+ function useCaptchaPayload() {
205
+ const ctx = useContext2(CaptchaReactContext);
206
+ return useCallback(async () => {
207
+ if (!ctx?.config?.enabled || !ctx.ready) return {};
208
+ return ctx.getCaptchaPayload();
209
+ }, [ctx]);
210
+ }
48
211
  export {
212
+ CaptchaProvider,
49
213
  PluginProvider,
50
214
  useAnalytics,
215
+ useCaptchaPayload,
51
216
  useIsMobile,
52
217
  usePlugin
53
218
  };
package/dist/hooks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/use-mobile.ts","../src/hooks/use-analytics.ts","../src/hooks/use-plugin.ts"],"sourcesContent":["import { useState, useEffect } from 'react';\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile(): boolean {\n const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);\n\n useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n mql.addEventListener('change', onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener('change', onChange);\n }, []);\n\n return !!isMobile;\n}\n","import { useEffect } from 'react';\n\n/**\n * Call from app with pathname from usePathname() (next/navigation).\n * Disables tracking on admin routes when pathname starts with /admin.\n */\nexport function useAnalytics(pathname: string | null): { isAdminRoute: boolean; shouldTrack: boolean } {\n const isAdminRoute = pathname != null && pathname.startsWith('/admin');\n\n useEffect(() => {\n if (isAdminRoute && typeof window !== 'undefined') {\n if (typeof (window as unknown as { gtag?: () => void }).gtag === 'function') {\n (window as unknown as { gtag: () => void }).gtag = () => {};\n }\n if (Array.isArray((window as unknown as { dataLayer?: unknown[] }).dataLayer)) {\n (window as unknown as { dataLayer: unknown[] }).dataLayer = [];\n }\n }\n }, [isAdminRoute]);\n\n return { isAdminRoute, shouldTrack: !isAdminRoute };\n}\n","import React, { createContext, useContext, type ReactNode } from 'react';\n\nconst PluginContext = createContext<{ getPlugin: <T>(name: string) => T | undefined } | null>(null);\n\nexport function PluginProvider({\n getPlugin,\n children,\n}: {\n getPlugin: <T>(name: string) => T | undefined;\n children: ReactNode;\n}) {\n return React.createElement(PluginContext.Provider, { value: { getPlugin } }, children);\n}\n\nexport function usePlugin<T = unknown>(name: string): T | undefined {\n const ctx = useContext(PluginContext);\n if (!ctx) return undefined;\n return ctx.getPlugin<T>(name);\n}\n"],"mappings":";AAAA,SAAS,UAAU,iBAAiB;AAEpC,IAAM,oBAAoB;AAEnB,SAAS,cAAuB;AACrC,QAAM,CAAC,UAAU,WAAW,IAAI,SAA8B,MAAS;AAEvE,YAAU,MAAM;AACd,UAAM,MAAM,OAAO,WAAW,eAAe,oBAAoB,CAAC,KAAK;AACvE,UAAM,WAAW,MAAM,YAAY,OAAO,aAAa,iBAAiB;AACxE,QAAI,iBAAiB,UAAU,QAAQ;AACvC,gBAAY,OAAO,aAAa,iBAAiB;AACjD,WAAO,MAAM,IAAI,oBAAoB,UAAU,QAAQ;AAAA,EACzD,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,CAAC;AACX;;;AChBA,SAAS,aAAAA,kBAAiB;AAMnB,SAAS,aAAa,UAA0E;AACrG,QAAM,eAAe,YAAY,QAAQ,SAAS,WAAW,QAAQ;AAErE,EAAAA,WAAU,MAAM;AACd,QAAI,gBAAgB,OAAO,WAAW,aAAa;AACjD,UAAI,OAAQ,OAA4C,SAAS,YAAY;AAC3E,QAAC,OAA2C,OAAO,MAAM;AAAA,QAAC;AAAA,MAC5D;AACA,UAAI,MAAM,QAAS,OAAgD,SAAS,GAAG;AAC7E,QAAC,OAA+C,YAAY,CAAC;AAAA,MAC/D;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO,EAAE,cAAc,aAAa,CAAC,aAAa;AACpD;;;ACrBA,OAAO,SAAS,eAAe,kBAAkC;AAEjE,IAAM,gBAAgB,cAAwE,IAAI;AAE3F,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AACF,GAGG;AACD,SAAO,MAAM,cAAc,cAAc,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,QAAQ;AACvF;AAEO,SAAS,UAAuB,MAA6B;AAClE,QAAM,MAAM,WAAW,aAAa;AACpC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,UAAa,IAAI;AAC9B;","names":["useEffect"]}
1
+ {"version":3,"sources":["../src/hooks/use-mobile.ts","../src/hooks/use-analytics.ts","../src/hooks/use-plugin.ts","../src/components/CaptchaProvider.tsx"],"sourcesContent":["import { useState, useEffect } from 'react';\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile(): boolean {\n const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);\n\n useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n mql.addEventListener('change', onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener('change', onChange);\n }, []);\n\n return !!isMobile;\n}\n","import { useEffect } from 'react';\n\n/**\n * Call from app with pathname from usePathname() (next/navigation).\n * Disables tracking on admin routes when pathname starts with /admin.\n */\nexport function useAnalytics(pathname: string | null): { isAdminRoute: boolean; shouldTrack: boolean } {\n const isAdminRoute = pathname != null && pathname.startsWith('/admin');\n\n useEffect(() => {\n if (isAdminRoute && typeof window !== 'undefined') {\n if (typeof (window as unknown as { gtag?: () => void }).gtag === 'function') {\n (window as unknown as { gtag: () => void }).gtag = () => {};\n }\n if (Array.isArray((window as unknown as { dataLayer?: unknown[] }).dataLayer)) {\n (window as unknown as { dataLayer: unknown[] }).dataLayer = [];\n }\n }\n }, [isAdminRoute]);\n\n return { isAdminRoute, shouldTrack: !isAdminRoute };\n}\n","import React, { createContext, useContext, type ReactNode } from 'react';\n\nconst PluginContext = createContext<{ getPlugin: <T>(name: string) => T | undefined } | null>(null);\n\nexport function PluginProvider({\n getPlugin,\n children,\n}: {\n getPlugin: <T>(name: string) => T | undefined;\n children: ReactNode;\n}) {\n return React.createElement(PluginContext.Provider, { value: { getPlugin } }, children);\n}\n\nexport function usePlugin<T = unknown>(name: string): T | undefined {\n const ctx = useContext(PluginContext);\n if (!ctx) return undefined;\n return ctx.getPlugin<T>(name);\n}\n","'use client';\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport type { CaptchaPublicConfig, CaptchaProviderId } from '../plugins/captcha';\n\ndeclare global {\n interface Window {\n turnstile?: {\n render: (el: HTMLElement | string, opts: Record<string, unknown>) => string;\n execute: (id: string) => void;\n reset: (id: string) => void;\n remove: (id: string) => void;\n };\n grecaptcha?: {\n ready: (fn: () => void) => void;\n execute: (siteKey: string, opts: { action: string }) => Promise<string>;\n };\n }\n}\n\ntype CaptchaCtx = {\n config: CaptchaPublicConfig | null;\n ready: boolean;\n selectedProvider: CaptchaProviderId | null;\n setSelectedProvider: (p: CaptchaProviderId) => void;\n getCaptchaPayload: () => Promise<Record<string, string>>;\n};\n\nconst CaptchaReactContext = createContext<CaptchaCtx | null>(null);\n\nfunction loadScript(src: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const base = src.split('?')[0] ?? src;\n const existing = Array.from(document.querySelectorAll('script[src]')).some((el) =>\n (el as HTMLScriptElement).src.startsWith(base)\n );\n if (existing) {\n resolve();\n return;\n }\n const s = document.createElement('script');\n s.src = src;\n s.async = true;\n s.onload = () => resolve();\n s.onerror = () => reject(new Error('Failed to load script'));\n document.head.appendChild(s);\n });\n}\n\nexport function CaptchaProvider({ children }: { children: React.ReactNode }) {\n const [config, setConfig] = useState<CaptchaPublicConfig | null>(null);\n const [selectedProvider, setSelectedProvider] = useState<CaptchaProviderId | null>(null);\n const [ready, setReady] = useState(false);\n const turnstileWidgetIdRef = useRef<string | null>(null);\n const turnstileContainerRef = useRef<HTMLDivElement | null>(null);\n const turnstileTokenRef = useRef<{\n resolve: (t: string) => void;\n reject: (e: Error) => void;\n } | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n fetch('/api/public/captcha-config')\n .then((r) => (r.ok ? r.json() : null))\n .then((c: CaptchaPublicConfig | null) => {\n if (cancelled || !c) return;\n setConfig(c);\n if (c.enabled && c.activeProvider) setSelectedProvider(c.activeProvider);\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setReady(true);\n });\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n if (!config?.enabled || !selectedProvider) return;\n const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;\n if (!siteKeyFor) return;\n\n let cancelled = false;\n (async () => {\n try {\n if (selectedProvider === 'recaptcha_v3') {\n await loadScript(\n `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKeyFor)}`\n );\n } else {\n await loadScript('https://challenges.cloudflare.com/turnstile/v0/api.js');\n if (cancelled || !turnstileContainerRef.current || !window.turnstile) return;\n if (turnstileWidgetIdRef.current) {\n try {\n window.turnstile.remove(turnstileWidgetIdRef.current);\n } catch {\n /* ignore */\n }\n turnstileWidgetIdRef.current = null;\n }\n const wid = window.turnstile.render(turnstileContainerRef.current, {\n sitekey: siteKeyFor,\n execution: 'execute',\n callback: (token: string) => {\n const p = turnstileTokenRef.current;\n turnstileTokenRef.current = null;\n p?.resolve(token);\n },\n 'error-callback': () => {\n const p = turnstileTokenRef.current;\n turnstileTokenRef.current = null;\n p?.reject(new Error('Turnstile error'));\n },\n 'expired-callback': () => {\n const p = turnstileTokenRef.current;\n turnstileTokenRef.current = null;\n p?.reject(new Error('Turnstile expired'));\n },\n });\n turnstileWidgetIdRef.current = wid;\n }\n } catch {\n /* ignore */\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [config, selectedProvider]);\n\n const getCaptchaPayload = useCallback(async (): Promise<Record<string, string>> => {\n if (!config?.enabled || !selectedProvider) return {};\n const siteKeyFor = config.availableProviders.find((p) => p.id === selectedProvider)?.siteKey;\n if (!siteKeyFor) return {};\n\n if (selectedProvider === 'recaptcha_v3') {\n if (!window.grecaptcha) return {};\n const token = await new Promise<string>((resolve, reject) => {\n window.grecaptcha!.ready(() => {\n window.grecaptcha!.execute(siteKeyFor, { action: 'submit' }).then(resolve).catch(reject);\n });\n });\n return { captchaToken: token, captchaProvider: 'recaptcha_v3' };\n }\n\n const wid = turnstileWidgetIdRef.current;\n if (!wid || !window.turnstile) return {};\n const token = await new Promise<string>((resolve, reject) => {\n turnstileTokenRef.current = { resolve, reject };\n try {\n window.turnstile!.reset(wid);\n window.turnstile!.execute(wid);\n } catch (e) {\n turnstileTokenRef.current = null;\n reject(e instanceof Error ? e : new Error('Turnstile execute failed'));\n }\n });\n return { captchaToken: token, captchaProvider: 'turnstile' };\n }, [config, selectedProvider]);\n\n const ctx = useMemo<CaptchaCtx>(\n () => ({\n config,\n ready,\n selectedProvider,\n setSelectedProvider,\n getCaptchaPayload,\n }),\n [config, ready, selectedProvider, getCaptchaPayload]\n );\n\n return (\n <CaptchaReactContext.Provider value={ctx}>\n {config?.enabled && config.multipleProviders ? (\n <div className=\"mb-2 text-sm text-muted-foreground max-w-md\">\n <label className=\"mr-2\" htmlFor=\"captcha-provider-select\">\n Verification\n </label>\n <select\n id=\"captcha-provider-select\"\n className=\"border rounded px-2 py-1 bg-background\"\n value={selectedProvider ?? ''}\n onChange={(e) => setSelectedProvider(e.target.value as CaptchaProviderId)}\n >\n {config.availableProviders.map((p) => (\n <option key={p.id} value={p.id}>\n {p.id === 'turnstile' ? 'Cloudflare Turnstile' : 'Google reCAPTCHA v3'}\n </option>\n ))}\n </select>\n </div>\n ) : null}\n <div ref={turnstileContainerRef} className=\"hidden\" aria-hidden />\n {children}\n </CaptchaReactContext.Provider>\n );\n}\n\n/** Returns a function that resolves to captcha fields for JSON bodies, or {} if disabled / not ready. */\nexport function useCaptchaPayload(): () => Promise<Record<string, string>> {\n const ctx = useContext(CaptchaReactContext);\n return useCallback(async () => {\n if (!ctx?.config?.enabled || !ctx.ready) return {};\n return ctx.getCaptchaPayload();\n }, [ctx]);\n}\n"],"mappings":";AAAA,SAAS,UAAU,iBAAiB;AAEpC,IAAM,oBAAoB;AAEnB,SAAS,cAAuB;AACrC,QAAM,CAAC,UAAU,WAAW,IAAI,SAA8B,MAAS;AAEvE,YAAU,MAAM;AACd,UAAM,MAAM,OAAO,WAAW,eAAe,oBAAoB,CAAC,KAAK;AACvE,UAAM,WAAW,MAAM,YAAY,OAAO,aAAa,iBAAiB;AACxE,QAAI,iBAAiB,UAAU,QAAQ;AACvC,gBAAY,OAAO,aAAa,iBAAiB;AACjD,WAAO,MAAM,IAAI,oBAAoB,UAAU,QAAQ;AAAA,EACzD,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,CAAC;AACX;;;AChBA,SAAS,aAAAA,kBAAiB;AAMnB,SAAS,aAAa,UAA0E;AACrG,QAAM,eAAe,YAAY,QAAQ,SAAS,WAAW,QAAQ;AAErE,EAAAA,WAAU,MAAM;AACd,QAAI,gBAAgB,OAAO,WAAW,aAAa;AACjD,UAAI,OAAQ,OAA4C,SAAS,YAAY;AAC3E,QAAC,OAA2C,OAAO,MAAM;AAAA,QAAC;AAAA,MAC5D;AACA,UAAI,MAAM,QAAS,OAAgD,SAAS,GAAG;AAC7E,QAAC,OAA+C,YAAY,CAAC;AAAA,MAC/D;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO,EAAE,cAAc,aAAa,CAAC,aAAa;AACpD;;;ACrBA,OAAO,SAAS,eAAe,kBAAkC;AAEjE,IAAM,gBAAgB,cAAwE,IAAI;AAE3F,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AACF,GAGG;AACD,SAAO,MAAM,cAAc,cAAc,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,QAAQ;AACvF;AAEO,SAAS,UAAuB,MAA6B;AAClE,QAAM,MAAM,WAAW,aAAa;AACpC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,UAAa,IAAI;AAC9B;;;AChBA;AAAA,EACE,iBAAAC;AAAA,EACA;AAAA,EACA,cAAAC;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAAC;AAAA,OACK;AA6KC,SACE,KADF;AAnJR,IAAM,sBAAsBH,eAAiC,IAAI;AAEjE,SAAS,WAAW,KAA4B;AAC9C,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK;AAClC,UAAM,WAAW,MAAM,KAAK,SAAS,iBAAiB,aAAa,CAAC,EAAE;AAAA,MAAK,CAAC,OACzE,GAAyB,IAAI,WAAW,IAAI;AAAA,IAC/C;AACA,QAAI,UAAU;AACZ,cAAQ;AACR;AAAA,IACF;AACA,UAAM,IAAI,SAAS,cAAc,QAAQ;AACzC,MAAE,MAAM;AACR,MAAE,QAAQ;AACV,MAAE,SAAS,MAAM,QAAQ;AACzB,MAAE,UAAU,MAAM,OAAO,IAAI,MAAM,uBAAuB,CAAC;AAC3D,aAAS,KAAK,YAAY,CAAC;AAAA,EAC7B,CAAC;AACH;AAEO,SAAS,gBAAgB,EAAE,SAAS,GAAkC;AAC3E,QAAM,CAAC,QAAQ,SAAS,IAAIG,UAAqC,IAAI;AACrE,QAAM,CAAC,kBAAkB,mBAAmB,IAAIA,UAAmC,IAAI;AACvF,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAS,KAAK;AACxC,QAAM,uBAAuB,OAAsB,IAAI;AACvD,QAAM,wBAAwB,OAA8B,IAAI;AAChE,QAAM,oBAAoB,OAGhB,IAAI;AAEd,EAAAD,WAAU,MAAM;AACd,QAAI,YAAY;AAChB,UAAM,4BAA4B,EAC/B,KAAK,CAAC,MAAO,EAAE,KAAK,EAAE,KAAK,IAAI,IAAK,EACpC,KAAK,CAAC,MAAkC;AACvC,UAAI,aAAa,CAAC,EAAG;AACrB,gBAAU,CAAC;AACX,UAAI,EAAE,WAAW,EAAE,eAAgB,qBAAoB,EAAE,cAAc;AAAA,IACzE,CAAC,EACA,MAAM,MAAM;AAAA,IAAC,CAAC,EACd,QAAQ,MAAM;AACb,UAAI,CAAC,UAAW,UAAS,IAAI;AAAA,IAC/B,CAAC;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,QAAQ,WAAW,CAAC,iBAAkB;AAC3C,UAAM,aAAa,OAAO,mBAAmB,KAAK,CAAC,MAAM,EAAE,OAAO,gBAAgB,GAAG;AACrF,QAAI,CAAC,WAAY;AAEjB,QAAI,YAAY;AAChB,KAAC,YAAY;AACX,UAAI;AACF,YAAI,qBAAqB,gBAAgB;AACvC,gBAAM;AAAA,YACJ,kDAAkD,mBAAmB,UAAU,CAAC;AAAA,UAClF;AAAA,QACF,OAAO;AACL,gBAAM,WAAW,uDAAuD;AACxE,cAAI,aAAa,CAAC,sBAAsB,WAAW,CAAC,OAAO,UAAW;AACtE,cAAI,qBAAqB,SAAS;AAChC,gBAAI;AACF,qBAAO,UAAU,OAAO,qBAAqB,OAAO;AAAA,YACtD,QAAQ;AAAA,YAER;AACA,iCAAqB,UAAU;AAAA,UACjC;AACA,gBAAM,MAAM,OAAO,UAAU,OAAO,sBAAsB,SAAS;AAAA,YACjE,SAAS;AAAA,YACT,WAAW;AAAA,YACX,UAAU,CAAC,UAAkB;AAC3B,oBAAM,IAAI,kBAAkB;AAC5B,gCAAkB,UAAU;AAC5B,iBAAG,QAAQ,KAAK;AAAA,YAClB;AAAA,YACA,kBAAkB,MAAM;AACtB,oBAAM,IAAI,kBAAkB;AAC5B,gCAAkB,UAAU;AAC5B,iBAAG,OAAO,IAAI,MAAM,iBAAiB,CAAC;AAAA,YACxC;AAAA,YACA,oBAAoB,MAAM;AACxB,oBAAM,IAAI,kBAAkB;AAC5B,gCAAkB,UAAU;AAC5B,iBAAG,OAAO,IAAI,MAAM,mBAAmB,CAAC;AAAA,YAC1C;AAAA,UACF,CAAC;AACD,+BAAqB,UAAU;AAAA,QACjC;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAE7B,QAAM,oBAAoB,YAAY,YAA6C;AACjF,QAAI,CAAC,QAAQ,WAAW,CAAC,iBAAkB,QAAO,CAAC;AACnD,UAAM,aAAa,OAAO,mBAAmB,KAAK,CAAC,MAAM,EAAE,OAAO,gBAAgB,GAAG;AACrF,QAAI,CAAC,WAAY,QAAO,CAAC;AAEzB,QAAI,qBAAqB,gBAAgB;AACvC,UAAI,CAAC,OAAO,WAAY,QAAO,CAAC;AAChC,YAAME,SAAQ,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC3D,eAAO,WAAY,MAAM,MAAM;AAC7B,iBAAO,WAAY,QAAQ,YAAY,EAAE,QAAQ,SAAS,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM;AAAA,QACzF,CAAC;AAAA,MACH,CAAC;AACD,aAAO,EAAE,cAAcA,QAAO,iBAAiB,eAAe;AAAA,IAChE;AAEA,UAAM,MAAM,qBAAqB;AACjC,QAAI,CAAC,OAAO,CAAC,OAAO,UAAW,QAAO,CAAC;AACvC,UAAM,QAAQ,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC3D,wBAAkB,UAAU,EAAE,SAAS,OAAO;AAC9C,UAAI;AACF,eAAO,UAAW,MAAM,GAAG;AAC3B,eAAO,UAAW,QAAQ,GAAG;AAAA,MAC/B,SAAS,GAAG;AACV,0BAAkB,UAAU;AAC5B,eAAO,aAAa,QAAQ,IAAI,IAAI,MAAM,0BAA0B,CAAC;AAAA,MACvE;AAAA,IACF,CAAC;AACD,WAAO,EAAE,cAAc,OAAO,iBAAiB,YAAY;AAAA,EAC7D,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAE7B,QAAM,MAAM;AAAA,IACV,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,OAAO,kBAAkB,iBAAiB;AAAA,EACrD;AAEA,SACE,qBAAC,oBAAoB,UAApB,EAA6B,OAAO,KAClC;AAAA,YAAQ,WAAW,OAAO,oBACzB,qBAAC,SAAI,WAAU,+CACb;AAAA,0BAAC,WAAM,WAAU,QAAO,SAAQ,2BAA0B,0BAE1D;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,WAAU;AAAA,UACV,OAAO,oBAAoB;AAAA,UAC3B,UAAU,CAAC,MAAM,oBAAoB,EAAE,OAAO,KAA0B;AAAA,UAEvE,iBAAO,mBAAmB,IAAI,CAAC,MAC9B,oBAAC,YAAkB,OAAO,EAAE,IACzB,YAAE,OAAO,cAAc,yBAAyB,yBADtC,EAAE,EAEf,CACD;AAAA;AAAA,MACH;AAAA,OACF,IACE;AAAA,IACJ,oBAAC,SAAI,KAAK,uBAAuB,WAAU,UAAS,eAAW,MAAC;AAAA,IAC/D;AAAA,KACH;AAEJ;AAGO,SAAS,oBAA2D;AACzE,QAAM,MAAMH,YAAW,mBAAmB;AAC1C,SAAO,YAAY,YAAY;AAC7B,QAAI,CAAC,KAAK,QAAQ,WAAW,CAAC,IAAI,MAAO,QAAO,CAAC;AACjD,WAAO,IAAI,kBAAkB;AAAA,EAC/B,GAAG,CAAC,GAAG,CAAC;AACV;","names":["useEffect","createContext","useContext","useEffect","useState","token"]}
@@ -26,6 +26,8 @@ interface OrderPlacedLineItem {
26
26
  unitPrice: number | string;
27
27
  lineTotal: number | string;
28
28
  sku?: string | null;
29
+ tax?: number | string | null;
30
+ hsn?: string | null;
29
31
  }
30
32
  /** Merge branding + email plugin settings for layout (email overrides when set). */
31
33
  declare function mergeEmailLayoutCompanyDetails(branding: Record<string, string>, emailSettings: Record<string, string>): CompanyDetails;
@@ -162,6 +164,11 @@ interface DashboardStatsConfig extends CmsHandlersBase {
162
164
  requirePermission?: (req: Request, permission: string) => Promise<Response | null>;
163
165
  }
164
166
  declare function createDashboardStatsHandler(config: DashboardStatsConfig): (req: Request) => Promise<Response>;
167
+ interface EcommerceAnalyticsConfig extends CmsHandlersBase {
168
+ dataSource: DataSource;
169
+ entityMap: EntityMap;
170
+ }
171
+ declare function createEcommerceAnalyticsHandler(config: EcommerceAnalyticsConfig): (req: Request) => Promise<Response>;
165
172
  interface AnalyticsHandlerConfig extends CmsHandlersBase {
166
173
  getAnalyticsData?: (days: number) => Promise<unknown>;
167
174
  getPropertyId?: () => ({
@@ -187,8 +194,13 @@ interface UploadHandlerConfig extends CmsHandlersBase {
187
194
  localUploadDir?: string;
188
195
  allowedTypes?: string[];
189
196
  maxSizeBytes?: number;
197
+ /** Resolve folder chain for `parentId` (merged from createCmsApiHandler when omitted). */
198
+ dataSource?: DataSource;
199
+ entityMap?: EntityMap;
190
200
  }
191
201
  declare function createUploadHandler(config: UploadHandlerConfig): (req: Request) => Promise<Response>;
202
+ /** POST /api/media/extract/:id — expand a zip media row into folders/files under the zip’s parent folder. */
203
+ declare function createMediaZipExtractHandler(config: UploadHandlerConfig): (_req: Request, zipMediaId: string) => Promise<Response>;
192
204
  interface BlogBySlugConfig extends CmsHandlersBase {
193
205
  dataSource: DataSource;
194
206
  entityMap: EntityMap;
@@ -297,7 +309,7 @@ interface ChatApiConfig extends CmsHandlersBase {
297
309
  }
298
310
 
299
311
  /**
300
- * Single CMS API handler: dashboard, analytics, upload, blog/form by slug, users API, user auth, CRUD. Mount once (e.g. app/api/[[...path]]/route.ts).
312
+ * Single CMS API handler: dashboard, analytics, upload, media zip extract, blog/form by slug, users API, user auth, CRUD. Mount once (e.g. app/api/[[...path]]/route.ts).
301
313
  */
302
314
 
303
315
  /** CMS instance with getPlugin; when provided, analytics and userAuth.sendEmail can be resolved from plugins when not passed. */
@@ -322,6 +334,8 @@ interface CmsApiHandlerConfig {
322
334
  userAuth?: UserAuthApiConfig;
323
335
  /** GET /api/dashboard/stats */
324
336
  dashboard?: DashboardStatsConfig;
337
+ /** GET /api/dashboard/ecommerce */
338
+ ecommerceAnalytics?: EcommerceAnalyticsConfig;
325
339
  /** GET /api/analytics. If omitted and getCms is set, uses getCms().getPlugin('analytics'). */
326
340
  analytics?: AnalyticsHandlerConfig;
327
341
  /** POST /api/upload (S3 or local) */
@@ -390,4 +404,4 @@ declare function createStorefrontApiHandler(config: StorefrontApiConfig): {
390
404
  handle(method: string, path: string[], req: Request): Promise<Response>;
391
405
  };
392
406
 
393
- export { type AnalyticsHandlerConfig as A, type BlogBySlugConfig as B, type CompanyDetails as C, type DashboardStatsConfig as D, type EmailTemplateResult as E, type ForgotPasswordConfig as F, type GetPublicSettingsGroupConfig as G, createFormBySlugHandler as H, type InviteAcceptConfig as I, createInviteAcceptHandler as J, createSetPasswordHandler as K, createSettingsApiHandlers as L, createStorefrontApiHandler as M, createUploadHandler as N, type OrderPlacedLineItem as O, createUserAuthApiRouter as P, createUserAvatarHandler as Q, createUserProfileHandler as R, type StorageService as S, type TemplateContext as T, type UploadHandlerConfig as U, createUsersApiHandlers as V, getCompanyDetailsFromSettings as W, getPublicSettingsGroup as X, mergeEmailLayoutCompanyDetails as Y, type EmailTemplateName as a, type EntityMap as b, type AuthHandlersConfig as c, type ChangePasswordConfig as d, type CmsApiHandlerConfig as e, type CmsGetter as f, type CrudHandlerOptions as g, type FormBySlugConfig as h, type GetPublicSettingsGroupDataSource as i, type SetPasswordConfig as j, type SettingsApiConfig as k, type SocialLinkItem as l, type StorefrontApiConfig as m, type StorefrontOtpFlags as n, type UserAuthApiConfig as o, type UserAvatarConfig as p, type UserProfileConfig as q, type UsersApiConfig as r, createAnalyticsHandlers as s, createBlogBySlugHandler as t, createChangePasswordHandler as u, createCmsApiHandler as v, createCrudByIdHandler as w, createCrudHandler as x, createDashboardStatsHandler as y, createForgotPasswordHandler as z };
407
+ export { mergeEmailLayoutCompanyDetails as $, type AnalyticsHandlerConfig as A, type BlogBySlugConfig as B, type CompanyDetails as C, type DashboardStatsConfig as D, type EmailTemplateResult as E, type ForgotPasswordConfig as F, type GetPublicSettingsGroupConfig as G, createEcommerceAnalyticsHandler as H, type InviteAcceptConfig as I, createForgotPasswordHandler as J, createFormBySlugHandler as K, createInviteAcceptHandler as L, createMediaZipExtractHandler as M, createSetPasswordHandler as N, type OrderPlacedLineItem as O, createSettingsApiHandlers as P, createStorefrontApiHandler as Q, createUploadHandler as R, type StorageService as S, type TemplateContext as T, type UploadHandlerConfig as U, createUserAuthApiRouter as V, createUserAvatarHandler as W, createUserProfileHandler as X, createUsersApiHandlers as Y, getCompanyDetailsFromSettings as Z, getPublicSettingsGroup as _, type EmailTemplateName as a, type EntityMap as b, type AuthHandlersConfig as c, type ChangePasswordConfig as d, type CmsApiHandlerConfig as e, type CmsGetter as f, type CrudHandlerOptions as g, type EcommerceAnalyticsConfig as h, type FormBySlugConfig as i, type GetPublicSettingsGroupDataSource as j, type SetPasswordConfig as k, type SettingsApiConfig as l, type SocialLinkItem as m, type StorefrontApiConfig as n, type StorefrontOtpFlags as o, type UserAuthApiConfig as p, type UserAvatarConfig as q, type UserProfileConfig as r, type UsersApiConfig as s, createAnalyticsHandlers as t, createBlogBySlugHandler as u, createChangePasswordHandler as v, createCmsApiHandler as w, createCrudByIdHandler as x, createCrudHandler as y, createDashboardStatsHandler as z };
@@ -26,6 +26,8 @@ interface OrderPlacedLineItem {
26
26
  unitPrice: number | string;
27
27
  lineTotal: number | string;
28
28
  sku?: string | null;
29
+ tax?: number | string | null;
30
+ hsn?: string | null;
29
31
  }
30
32
  /** Merge branding + email plugin settings for layout (email overrides when set). */
31
33
  declare function mergeEmailLayoutCompanyDetails(branding: Record<string, string>, emailSettings: Record<string, string>): CompanyDetails;
@@ -162,6 +164,11 @@ interface DashboardStatsConfig extends CmsHandlersBase {
162
164
  requirePermission?: (req: Request, permission: string) => Promise<Response | null>;
163
165
  }
164
166
  declare function createDashboardStatsHandler(config: DashboardStatsConfig): (req: Request) => Promise<Response>;
167
+ interface EcommerceAnalyticsConfig extends CmsHandlersBase {
168
+ dataSource: DataSource;
169
+ entityMap: EntityMap;
170
+ }
171
+ declare function createEcommerceAnalyticsHandler(config: EcommerceAnalyticsConfig): (req: Request) => Promise<Response>;
165
172
  interface AnalyticsHandlerConfig extends CmsHandlersBase {
166
173
  getAnalyticsData?: (days: number) => Promise<unknown>;
167
174
  getPropertyId?: () => ({
@@ -187,8 +194,13 @@ interface UploadHandlerConfig extends CmsHandlersBase {
187
194
  localUploadDir?: string;
188
195
  allowedTypes?: string[];
189
196
  maxSizeBytes?: number;
197
+ /** Resolve folder chain for `parentId` (merged from createCmsApiHandler when omitted). */
198
+ dataSource?: DataSource;
199
+ entityMap?: EntityMap;
190
200
  }
191
201
  declare function createUploadHandler(config: UploadHandlerConfig): (req: Request) => Promise<Response>;
202
+ /** POST /api/media/extract/:id — expand a zip media row into folders/files under the zip’s parent folder. */
203
+ declare function createMediaZipExtractHandler(config: UploadHandlerConfig): (_req: Request, zipMediaId: string) => Promise<Response>;
192
204
  interface BlogBySlugConfig extends CmsHandlersBase {
193
205
  dataSource: DataSource;
194
206
  entityMap: EntityMap;
@@ -297,7 +309,7 @@ interface ChatApiConfig extends CmsHandlersBase {
297
309
  }
298
310
 
299
311
  /**
300
- * Single CMS API handler: dashboard, analytics, upload, blog/form by slug, users API, user auth, CRUD. Mount once (e.g. app/api/[[...path]]/route.ts).
312
+ * Single CMS API handler: dashboard, analytics, upload, media zip extract, blog/form by slug, users API, user auth, CRUD. Mount once (e.g. app/api/[[...path]]/route.ts).
301
313
  */
302
314
 
303
315
  /** CMS instance with getPlugin; when provided, analytics and userAuth.sendEmail can be resolved from plugins when not passed. */
@@ -322,6 +334,8 @@ interface CmsApiHandlerConfig {
322
334
  userAuth?: UserAuthApiConfig;
323
335
  /** GET /api/dashboard/stats */
324
336
  dashboard?: DashboardStatsConfig;
337
+ /** GET /api/dashboard/ecommerce */
338
+ ecommerceAnalytics?: EcommerceAnalyticsConfig;
325
339
  /** GET /api/analytics. If omitted and getCms is set, uses getCms().getPlugin('analytics'). */
326
340
  analytics?: AnalyticsHandlerConfig;
327
341
  /** POST /api/upload (S3 or local) */
@@ -390,4 +404,4 @@ declare function createStorefrontApiHandler(config: StorefrontApiConfig): {
390
404
  handle(method: string, path: string[], req: Request): Promise<Response>;
391
405
  };
392
406
 
393
- export { type AnalyticsHandlerConfig as A, type BlogBySlugConfig as B, type CompanyDetails as C, type DashboardStatsConfig as D, type EmailTemplateResult as E, type ForgotPasswordConfig as F, type GetPublicSettingsGroupConfig as G, createFormBySlugHandler as H, type InviteAcceptConfig as I, createInviteAcceptHandler as J, createSetPasswordHandler as K, createSettingsApiHandlers as L, createStorefrontApiHandler as M, createUploadHandler as N, type OrderPlacedLineItem as O, createUserAuthApiRouter as P, createUserAvatarHandler as Q, createUserProfileHandler as R, type StorageService as S, type TemplateContext as T, type UploadHandlerConfig as U, createUsersApiHandlers as V, getCompanyDetailsFromSettings as W, getPublicSettingsGroup as X, mergeEmailLayoutCompanyDetails as Y, type EmailTemplateName as a, type EntityMap as b, type AuthHandlersConfig as c, type ChangePasswordConfig as d, type CmsApiHandlerConfig as e, type CmsGetter as f, type CrudHandlerOptions as g, type FormBySlugConfig as h, type GetPublicSettingsGroupDataSource as i, type SetPasswordConfig as j, type SettingsApiConfig as k, type SocialLinkItem as l, type StorefrontApiConfig as m, type StorefrontOtpFlags as n, type UserAuthApiConfig as o, type UserAvatarConfig as p, type UserProfileConfig as q, type UsersApiConfig as r, createAnalyticsHandlers as s, createBlogBySlugHandler as t, createChangePasswordHandler as u, createCmsApiHandler as v, createCrudByIdHandler as w, createCrudHandler as x, createDashboardStatsHandler as y, createForgotPasswordHandler as z };
407
+ export { mergeEmailLayoutCompanyDetails as $, type AnalyticsHandlerConfig as A, type BlogBySlugConfig as B, type CompanyDetails as C, type DashboardStatsConfig as D, type EmailTemplateResult as E, type ForgotPasswordConfig as F, type GetPublicSettingsGroupConfig as G, createEcommerceAnalyticsHandler as H, type InviteAcceptConfig as I, createForgotPasswordHandler as J, createFormBySlugHandler as K, createInviteAcceptHandler as L, createMediaZipExtractHandler as M, createSetPasswordHandler as N, type OrderPlacedLineItem as O, createSettingsApiHandlers as P, createStorefrontApiHandler as Q, createUploadHandler as R, type StorageService as S, type TemplateContext as T, type UploadHandlerConfig as U, createUserAuthApiRouter as V, createUserAvatarHandler as W, createUserProfileHandler as X, createUsersApiHandlers as Y, getCompanyDetailsFromSettings as Z, getPublicSettingsGroup as _, type EmailTemplateName as a, type EntityMap as b, type AuthHandlersConfig as c, type ChangePasswordConfig as d, type CmsApiHandlerConfig as e, type CmsGetter as f, type CrudHandlerOptions as g, type EcommerceAnalyticsConfig as h, type FormBySlugConfig as i, type GetPublicSettingsGroupDataSource as j, type SetPasswordConfig as k, type SettingsApiConfig as l, type SocialLinkItem as m, type StorefrontApiConfig as n, type StorefrontOtpFlags as o, type UserAuthApiConfig as p, type UserAvatarConfig as q, type UserProfileConfig as r, type UsersApiConfig as s, createAnalyticsHandlers as t, createBlogBySlugHandler as u, createChangePasswordHandler as v, createCmsApiHandler as w, createCrudByIdHandler as x, createCrudHandler as y, createDashboardStatsHandler as z };