@jini-ai/admin 0.6.11 → 0.6.15-rc.1
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/CHANGELOG.md +18 -0
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +1 -0
- package/dist/browser/telemetry.d.ts +45 -0
- package/dist/browser/telemetry.js +108 -0
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +5 -15
- package/dist/consent/react/ConsentFields.d.ts +7 -3
- package/dist/consent/react/ConsentFields.js +17 -5
- package/dist/consent/react/ConsentSettingsPanel.d.ts +4 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +4 -0
- package/dist/consent/react/settings-panel.hooks.js +2 -0
- package/dist/forms/forms.module.d.ts +4 -0
- package/dist/forms/messages.en.d.ts +4 -0
- package/dist/forms/messages.en.js +4 -0
- package/dist/forms/react/components/FormEditorFieldsBody.js +1 -1
- package/dist/forms/react/index.d.ts +12 -0
- package/package.json +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fddd084: Allow consent-enabled static HTML exports with native reopening controls and denied visitor defaults. Keep reopened preferences at the banner position and show dynamic-code help through the shared info-tip.
|
|
8
|
+
- 0728914: Engine guard clean-up (`pnpm guard` 106 -> 0): layer, subpath-isolation and module-state findings fixed by moving
|
|
9
|
+
shared contracts to their owners instead of reaching across domains. Additive public API: `@jini-ai/core/filesystem`
|
|
10
|
+
(guarded-reader contracts), `useFetchQueryScope` from `@jini-ai/ui` fetch-query (per-provider query scope),
|
|
11
|
+
`@jini-ai/chat/react` re-exports of the accepted-input, run-state and agent-plugin chat-state helpers. The Claude
|
|
12
|
+
filesystem probe cache in `@jini-ai/agent-runtime` is instance-owned. No behaviour change intended; host-facing
|
|
13
|
+
breaks are listed in `owner-store-to-server` and `guard-host-ports`.
|
|
14
|
+
|
|
15
|
+
## 0.6.12
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- consent settings: per-site `'unsafe-eval'` toggle.
|
|
20
|
+
|
|
3
21
|
## 0.6.11
|
|
4
22
|
|
|
5
23
|
## 0.6.10
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { NAVIGATION_EVENT, installInternalLinkInterceptor, navigate, readRoutePath, subscribeToRoute, } from './navigation.js';
|
|
8
8
|
export { createAdminShellNavigation } from './shell-navigation.js';
|
|
9
|
+
export { createBrowserTelemetry, installBrowserErrorListeners, type BrowserTelemetry, type BrowserTelemetryDeps, type BrowserTelemetryPayload } from './telemetry.js';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/browser/index.js
CHANGED
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { NAVIGATION_EVENT, installInternalLinkInterceptor, navigate, readRoutePath, subscribeToRoute, } from './navigation.js';
|
|
8
8
|
export { createAdminShellNavigation } from './shell-navigation.js';
|
|
9
|
+
export { createBrowserTelemetry, installBrowserErrorListeners } from './telemetry.js';
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Browser telemetry over host-bound transport, timers and session state. Route names stay with the host. */
|
|
2
|
+
export type BrowserTelemetryPayload = {
|
|
3
|
+
type: "error";
|
|
4
|
+
message: string;
|
|
5
|
+
stack: string;
|
|
6
|
+
componentStack?: string | undefined;
|
|
7
|
+
route: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "pageview";
|
|
10
|
+
route: string;
|
|
11
|
+
};
|
|
12
|
+
export interface BrowserTelemetryDeps {
|
|
13
|
+
send(payload: BrowserTelemetryPayload): Promise<unknown>;
|
|
14
|
+
nowMs(): number;
|
|
15
|
+
setTimer(fn: () => void, ms: number): unknown;
|
|
16
|
+
clearTimer(handle: unknown): void;
|
|
17
|
+
}
|
|
18
|
+
export interface BrowserTelemetry {
|
|
19
|
+
/** Only a signed-in session can post; before sign-in everything is dropped. */
|
|
20
|
+
setSignedIn(signedIn: boolean): void;
|
|
21
|
+
/** The current route template; errors carry it, and a change sends a (debounced) page view. */
|
|
22
|
+
setRoute(route: string): void;
|
|
23
|
+
reportError(input: {
|
|
24
|
+
error: unknown;
|
|
25
|
+
componentStack?: string | undefined;
|
|
26
|
+
}): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds the telemetry engine over injected I/O so its dedupe, cap and debounce are testable.
|
|
30
|
+
* @complexity O(1) amortized per call; O(f) memory for at most {@link MAX_FINGERPRINTS} fingerprints.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createBrowserTelemetry(deps: BrowserTelemetryDeps, { maxErrorsPerMinute, pageviewDelayMs, initialRoute }?: {
|
|
33
|
+
maxErrorsPerMinute?: number | undefined;
|
|
34
|
+
pageviewDelayMs?: number | undefined;
|
|
35
|
+
initialRoute?: string | undefined;
|
|
36
|
+
}): BrowserTelemetry;
|
|
37
|
+
/**
|
|
38
|
+
* Window-level errors the React root never sees (event handlers, timers, promises).
|
|
39
|
+
* @returns Uninstall.
|
|
40
|
+
*/
|
|
41
|
+
export declare function installBrowserErrorListeners({ target, telemetry }: {
|
|
42
|
+
target: Pick<Window, "addEventListener" | "removeEventListener">;
|
|
43
|
+
telemetry: Pick<BrowserTelemetry, "reportError">;
|
|
44
|
+
}, _options?: {}): () => void;
|
|
45
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Mirrors the server's caps (`browser-telemetry.ts`): longer text is cut here, not refused there. */
|
|
2
|
+
const CAPS = { message: 1_000, stack: 8_000, componentStack: 4_000, route: 200 };
|
|
3
|
+
const WINDOW_MS = 60_000;
|
|
4
|
+
const DEDUPE_MS = 5 * 60_000;
|
|
5
|
+
const MAX_FINGERPRINTS = 200;
|
|
6
|
+
/** Message of an Error or a thrown string; any other thrown value is named by type only, never dumped. */
|
|
7
|
+
function messageOf(error) {
|
|
8
|
+
if (error instanceof Error)
|
|
9
|
+
return error.message || error.name;
|
|
10
|
+
if (typeof error === "string")
|
|
11
|
+
return error;
|
|
12
|
+
return `Non-Error thrown (${error === null ? "null" : typeof error})`;
|
|
13
|
+
}
|
|
14
|
+
function errorPayload({ error, componentStack, route }) {
|
|
15
|
+
const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : "";
|
|
16
|
+
const cs = componentStack ? componentStack.slice(0, CAPS.componentStack) : "";
|
|
17
|
+
return { type: "error", message: messageOf(error).slice(0, CAPS.message), stack: stack.slice(0, CAPS.stack), ...(cs ? { componentStack: cs } : {}), route };
|
|
18
|
+
}
|
|
19
|
+
/** Message + first stack frame: the same bug from the same place is one report, not hundreds. */
|
|
20
|
+
function fingerprintOf(payload) {
|
|
21
|
+
const frame = payload.stack.split("\n").find((line) => /\bat\b|@/.test(line)) ?? "";
|
|
22
|
+
return `${payload.message}|${frame.trim()}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Builds the telemetry engine over injected I/O so its dedupe, cap and debounce are testable.
|
|
26
|
+
* @complexity O(1) amortized per call; O(f) memory for at most {@link MAX_FINGERPRINTS} fingerprints.
|
|
27
|
+
*/
|
|
28
|
+
export function createBrowserTelemetry(deps, { maxErrorsPerMinute = 10, pageviewDelayMs = 1_000, initialRoute = "" } = {}) {
|
|
29
|
+
let signedIn = false;
|
|
30
|
+
let route = initialRoute;
|
|
31
|
+
let lastPageview = null;
|
|
32
|
+
let timer = null;
|
|
33
|
+
let sentAt = [];
|
|
34
|
+
const seen = new Map();
|
|
35
|
+
const post = (payload) => {
|
|
36
|
+
try {
|
|
37
|
+
void deps.send(payload).catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
catch { /* never throws */ }
|
|
40
|
+
};
|
|
41
|
+
const underCap = (now) => {
|
|
42
|
+
sentAt = sentAt.filter((at) => now - at < WINDOW_MS);
|
|
43
|
+
return sentAt.length < maxErrorsPerMinute;
|
|
44
|
+
};
|
|
45
|
+
const isRepeat = (fingerprint, now) => {
|
|
46
|
+
const last = seen.get(fingerprint);
|
|
47
|
+
if (last !== undefined && now - last < DEDUPE_MS)
|
|
48
|
+
return true;
|
|
49
|
+
seen.delete(fingerprint);
|
|
50
|
+
if (seen.size >= MAX_FINGERPRINTS)
|
|
51
|
+
seen.delete(seen.keys().next().value);
|
|
52
|
+
seen.set(fingerprint, now);
|
|
53
|
+
return false;
|
|
54
|
+
};
|
|
55
|
+
const flushPageview = () => {
|
|
56
|
+
timer = null;
|
|
57
|
+
if (!signedIn || route === lastPageview)
|
|
58
|
+
return;
|
|
59
|
+
lastPageview = route;
|
|
60
|
+
post({ type: "pageview", route });
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
setSignedIn(next) { signedIn = next; if (!next)
|
|
64
|
+
lastPageview = null; },
|
|
65
|
+
setRoute(next) {
|
|
66
|
+
route = next.slice(0, CAPS.route);
|
|
67
|
+
try {
|
|
68
|
+
if (timer !== null)
|
|
69
|
+
deps.clearTimer(timer);
|
|
70
|
+
timer = deps.setTimer(flushPageview, pageviewDelayMs);
|
|
71
|
+
}
|
|
72
|
+
catch { /* never throws */ }
|
|
73
|
+
},
|
|
74
|
+
reportError({ error, componentStack }) {
|
|
75
|
+
try {
|
|
76
|
+
if (!signedIn)
|
|
77
|
+
return;
|
|
78
|
+
const payload = errorPayload({ error, componentStack, route });
|
|
79
|
+
const now = deps.nowMs();
|
|
80
|
+
if (isRepeat(fingerprintOf(payload), now) || !underCap(now))
|
|
81
|
+
return;
|
|
82
|
+
sentAt.push(now);
|
|
83
|
+
post(payload);
|
|
84
|
+
}
|
|
85
|
+
catch { /* never throws */ }
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Window-level errors the React root never sees (event handlers, timers, promises).
|
|
91
|
+
* @returns Uninstall.
|
|
92
|
+
*/
|
|
93
|
+
export function installBrowserErrorListeners({ target, telemetry }, _options = {}) {
|
|
94
|
+
const onError = (event) => {
|
|
95
|
+
// A cross-origin "Script error." carries nothing usable; skip it rather than report noise.
|
|
96
|
+
if (event.error === undefined || event.error === null)
|
|
97
|
+
return;
|
|
98
|
+
telemetry.reportError({ error: event.error });
|
|
99
|
+
};
|
|
100
|
+
const onRejection = (event) => telemetry.reportError({ error: event.reason });
|
|
101
|
+
target.addEventListener("error", onError);
|
|
102
|
+
target.addEventListener("unhandledrejection", onRejection);
|
|
103
|
+
return () => {
|
|
104
|
+
target.removeEventListener("error", onError);
|
|
105
|
+
target.removeEventListener("unhandledrejection", onRejection);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, createElement, useContext, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { useCachedLoader, useFetchQuery, useInvalidate } from '@jini-ai/ui/fetch-query';
|
|
2
|
+
import { useCachedLoader, useFetchQuery, useInvalidate, useFetchQueryScope } from '@jini-ai/ui/fetch-query';
|
|
3
3
|
import { AdminConfigError } from '../../../core/module/create-admin.js';
|
|
4
4
|
import { useController } from '../../../core/react/use-controller.js';
|
|
5
5
|
import { createCommentQueueController } from '../../controllers/comment-queue.controller.js';
|
|
@@ -18,19 +18,6 @@ const CommentQueueScopeContext = createContext(null);
|
|
|
18
18
|
export function CommentQueueScopeProvider({ scope, children }) {
|
|
19
19
|
return createElement(CommentQueueScopeContext.Provider, { value: scope }, children);
|
|
20
20
|
}
|
|
21
|
-
// Weak API keys preserve sharing between consumers without retaining APIs from previous sessions.
|
|
22
|
-
// Explicit workspace/principal keys below also isolate hosts that retain one API across sessions.
|
|
23
|
-
const queueScopes = new WeakMap();
|
|
24
|
-
let nextQueueScope = 0;
|
|
25
|
-
/** Allocate the API portion of a namespace; workspace/principal identity is keyed separately. */
|
|
26
|
-
function queueScope({ api }, _optional = {}) {
|
|
27
|
-
let scope = queueScopes.get(api);
|
|
28
|
-
if (scope === undefined) {
|
|
29
|
-
scope = ++nextQueueScope;
|
|
30
|
-
queueScopes.set(api, scope);
|
|
31
|
-
}
|
|
32
|
-
return scope;
|
|
33
|
-
}
|
|
34
21
|
/** Preserve the hook's loading defaults while no controller from the current scope is attached. */
|
|
35
22
|
function queueDisplay({ snapshot }, _optional = {}) {
|
|
36
23
|
const { status, items, nextCursor, error, loadingMore, pendingPurge } = snapshot ?? BEFORE_FIRST_SNAPSHOT;
|
|
@@ -73,7 +60,10 @@ function useCursorPageLoader({ api, queueRoot, status, cursor }, _optional = {})
|
|
|
73
60
|
*/
|
|
74
61
|
export function useCommentQueue({ api, scope, permissions = NO_GRANTS }, { t = englishComments, events } = {}) {
|
|
75
62
|
const invalidate = useInvalidate();
|
|
76
|
-
|
|
63
|
+
// Weak API keys preserve sharing between consumers without retaining APIs from previous sessions.
|
|
64
|
+
// Explicit workspace/principal keys below also isolate hosts that retain one API across sessions.
|
|
65
|
+
const apiScope = useFetchQueryScope({ owner: api });
|
|
66
|
+
const queueRoot = useMemo(() => [...KEYS.queueRoot, apiScope, scope.workspaceId, scope.principalId], [apiScope, scope.workspaceId, scope.principalId]);
|
|
77
67
|
const invalidateQueue = useCallback(() => invalidate({ key: queueRoot }), [invalidate, queueRoot]);
|
|
78
68
|
const loadPageRef = useRef(null);
|
|
79
69
|
const { controller: attachedController, snapshot: attachedSnapshot } = useController({
|
|
@@ -20,9 +20,13 @@ export interface ConsentFieldsProps {
|
|
|
20
20
|
/** Host max lengths per text field; the defaults are the engine's loosest bounds. */
|
|
21
21
|
limits?: Readonly<Partial<Record<TextKey | 'policyVersion', number>>> | undefined;
|
|
22
22
|
}
|
|
23
|
-
/** Banner copy and its links.
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
/** Banner copy and its links. Optional host modes report only a mode patch; the host's independent
|
|
24
|
+
* enabled switch stays authoritative. Text is always plain text.
|
|
25
|
+
* @example <ConsentBannerFields value={value} onChange={onChange} bannerModes={['jini', 'headless', 'off']} /> */
|
|
26
|
+
export declare function ConsentBannerFields({ value, onChange, disabled, problems, dictionary, limits, bannerModes }: ConsentFieldsProps & {
|
|
27
|
+
bannerModes?: readonly NonNullable<ConsentSettingsView['bannerMode']>[] | undefined;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
/** On/off, script policy, region behavior and the version that re-asks visitors when it changes. */
|
|
26
30
|
export declare function ConsentRulesFields({ value, onChange, disabled, problems, dictionary, limits, regionModes }: ConsentFieldsProps & {
|
|
27
31
|
regionModes?: readonly ConsentSettingsView['regionMode'][];
|
|
28
32
|
}): import("react").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from 'react';
|
|
3
3
|
import { defaultAdminTheme } from '@jini-ai/ui/theme';
|
|
4
|
+
import { InfoTip } from '@jini-ai/ui/admin-widgets';
|
|
4
5
|
import { pixelInputValue, problemsFor, useConsentSettingsPanel, usePolicyVersionInput } from './settings-panel.hooks.js';
|
|
5
6
|
const BANNER_FIELDS = [
|
|
6
7
|
{ key: 'title', label: 'Banner title' }, { key: 'message', label: 'Banner message', multiline: true },
|
|
@@ -8,6 +9,7 @@ const BANNER_FIELDS = [
|
|
|
8
9
|
{ key: 'preferencesLabel', label: 'Preferences label' }, { key: 'privacyPolicyUrl', label: 'Privacy policy page URL' },
|
|
9
10
|
{ key: 'secondaryLinkLabel', label: 'Second link text' }, { key: 'secondaryLinkUrl', label: 'Second link URL' },
|
|
10
11
|
];
|
|
12
|
+
const BANNER_MODE_LABELS = { jini: 'Jini banner', headless: 'My own', off: 'None' };
|
|
11
13
|
const REGION_LABELS = {
|
|
12
14
|
auto: 'Automatic (unknown region: opt-in)', 'opt-in': 'Opt-in everywhere (EU / UK)', 'opt-out': 'Opt-out everywhere (California)',
|
|
13
15
|
};
|
|
@@ -38,21 +40,31 @@ function Field({ label, field, problems, children }) {
|
|
|
38
40
|
const attributes = own.length ? { id, 'aria-invalid': true, 'aria-describedby': `${id}-problems` } : { id };
|
|
39
41
|
return _jsxs("div", { className: "jini-consent-settings-field", children: [_jsx("label", { htmlFor: id, className: "jini-field-label", children: label }), children(attributes), _jsx(FieldProblems, { id: `${id}-problems`, problems: own })] });
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
+
/** Host-supplied banner modes. Native radios provide keyboard selection; the hint keeps its space
|
|
44
|
+
* while hidden, and the parent fieldset supplies the disabled state. */
|
|
45
|
+
function BannerModeFields({ value, onChange, dictionary, modes }) {
|
|
46
|
+
const id = useId();
|
|
47
|
+
const t = translator(dictionary);
|
|
48
|
+
const handlers = useConsentSettingsPanel({ value, onChange });
|
|
49
|
+
return _jsxs("fieldset", { className: "jini-consent-banner-modes", "aria-describedby": value.bannerMode === 'headless' ? `${id}-mode-hint` : undefined, children: [_jsx("legend", { children: t('Banner mode') }), _jsx("div", { className: "jini-consent-banner-options", children: modes.map(mode => _jsxs("label", { children: [_jsx("input", { type: "radio", name: `${id}-banner-mode`, value: mode, checked: (value.bannerMode ?? 'jini') === mode, onChange: handlers.bannerMode({ mode }) }), _jsx("span", { children: t(BANNER_MODE_LABELS[mode]) })] }, mode)) }), _jsx("p", { id: `${id}-mode-hint`, className: "jini-hint jini-consent-banner-mode-hint", style: { visibility: value.bannerMode === 'headless' ? 'visible' : 'hidden' }, children: t('Your banner calls window.jiniConsent.set(...)') })] });
|
|
50
|
+
}
|
|
51
|
+
/** Banner copy and its links. Optional host modes report only a mode patch; the host's independent
|
|
52
|
+
* enabled switch stays authoritative. Text is always plain text.
|
|
53
|
+
* @example <ConsentBannerFields value={value} onChange={onChange} bannerModes={['jini', 'headless', 'off']} /> */
|
|
54
|
+
export function ConsentBannerFields({ value, onChange, disabled = false, problems, dictionary, limits = {}, bannerModes }) {
|
|
43
55
|
const t = translator(dictionary);
|
|
44
56
|
const handlers = useConsentSettingsPanel({ value, onChange });
|
|
45
|
-
return _jsxs("fieldset", { disabled: disabled, className: "jini-consent-settings-fields", children: [_jsx("legend", { children: t('Banner text') }), BANNER_FIELDS.map(({ key, label, multiline }) => _jsx(Field, { label: t(label), field: key, problems: problems, children: aria => multiline
|
|
57
|
+
return _jsxs("fieldset", { disabled: disabled, className: "jini-consent-settings-fields", children: [_jsx("legend", { children: t('Banner text') }), bannerModes && _jsx(BannerModeFields, { value: value, onChange: onChange, dictionary: dictionary, modes: bannerModes }), BANNER_FIELDS.map(({ key, label, multiline }) => _jsx(Field, { label: t(label), field: key, problems: problems, children: aria => multiline
|
|
46
58
|
? _jsx("textarea", { className: "jini-input", value: value[key] ?? '', maxLength: limits[key] ?? 2000, rows: 3, onChange: handlers.text(key), ...aria })
|
|
47
59
|
: _jsx("input", { className: "jini-input", type: "text", value: value[key] ?? '', maxLength: limits[key] ?? 2000, onChange: handlers.text(key), ...aria }) }, key)), _jsx("p", { className: "jini-hint", children: t('Link a published privacy page, such as /privacy. The second link is optional.') })] });
|
|
48
60
|
}
|
|
49
|
-
/** On/off, region behavior and the
|
|
61
|
+
/** On/off, script policy, region behavior and the version that re-asks visitors when it changes. */
|
|
50
62
|
export function ConsentRulesFields({ value, onChange, disabled = false, problems, dictionary, limits = {}, regionModes = ['auto', 'opt-in', 'opt-out'] }) {
|
|
51
63
|
const id = useId();
|
|
52
64
|
const t = translator(dictionary);
|
|
53
65
|
const handlers = useConsentSettingsPanel({ value, onChange });
|
|
54
66
|
const modes = regionModes.includes(value.regionMode) ? regionModes : [value.regionMode, ...regionModes];
|
|
55
|
-
return _jsxs("fieldset", { disabled: disabled, className: "jini-consent-settings-fields", children: [_jsx("legend", { children: t('Rules') }), _jsxs("button", { type: "button", className: value.enabled ? 'jini-toggle-row on' : 'jini-toggle-row', role: "switch", "aria-checked": value.enabled, "aria-labelledby": `${id}-enabled`, "aria-describedby": `${id}-hint`, onClick: handlers.toggle, children: [_jsxs("span", { className: "jini-toggle-row-text", children: [_jsx("span", { id: `${id}-enabled`, className: "jini-toggle-row-label", children: t('Enable cookie consent on this site') }), _jsx("span", { id: `${id}-hint`, className: "jini-toggle-row-hint", children: t('Applies to published site pages.') })] }), _jsx("span", { className: "jini-toggle-row-switch", "aria-hidden": "true" })] }), _jsx(Field, { label: t('Region mode'), field: "regionMode", problems: problems, children: aria => _jsx("select", { className: "jini-input", value: value.regionMode, onChange: handlers.region, ...aria, children: modes.map(mode => _jsx("option", { value: mode, children: t(REGION_LABELS[mode]) }, mode)) }) }), _jsx("p", { className: "jini-hint", children: t('California permits optional categories until opt-out. Global Privacy Control always disables analytics and marketing.') }), _jsx(Field, { label: t('Policy version'), field: "policyVersion", problems: problems, children: aria => _jsx(PolicyVersionInput, { value: value.policyVersion, onChange: onChange, maxLength: limits.policyVersion ?? 128, aria: aria }) }), _jsx("p", { className: "jini-hint", children: t('Change the policy version to ask visitors to choose again.') })] });
|
|
67
|
+
return _jsxs("fieldset", { disabled: disabled, className: "jini-consent-settings-fields", children: [_jsx("legend", { children: t('Rules') }), _jsxs("button", { type: "button", className: value.enabled ? 'jini-toggle-row on' : 'jini-toggle-row', role: "switch", "aria-checked": value.enabled, "aria-labelledby": `${id}-enabled`, "aria-describedby": `${id}-hint`, onClick: handlers.toggle, children: [_jsxs("span", { className: "jini-toggle-row-text", children: [_jsx("span", { id: `${id}-enabled`, className: "jini-toggle-row-label", children: t('Enable cookie consent on this site') }), _jsx("span", { id: `${id}-hint`, className: "jini-toggle-row-hint", children: t('Applies to published site pages.') })] }), _jsx("span", { className: "jini-toggle-row-switch", "aria-hidden": "true" })] }), value.enabled && _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsxs("button", { type: "button", className: value.allowUnsafeEval === true ? 'jini-toggle-row on' : 'jini-toggle-row', role: "switch", "aria-checked": value.allowUnsafeEval === true, "aria-labelledby": `${id}-eval`, "aria-describedby": `${id}-eval-hint`, onClick: handlers.toggleUnsafeEval, children: [_jsx("span", { className: "jini-toggle-row-text", children: _jsx("span", { id: `${id}-eval`, className: "jini-toggle-row-label", children: t('Allow dynamic code (unsafe-eval)') }) }), _jsx("span", { className: "jini-toggle-row-switch", "aria-hidden": "true" })] }), _jsx(InfoTip, { triggerLabel: t('About dynamic code'), descriptionId: `${id}-eval-hint`, label: t("Some themes need this. It weakens the site's script protection. Optional scripts still wait for consent.") })] }), _jsx(Field, { label: t('Region mode'), field: "regionMode", problems: problems, children: aria => _jsx("select", { className: "jini-input", value: value.regionMode, onChange: handlers.region, ...aria, children: modes.map(mode => _jsx("option", { value: mode, children: t(REGION_LABELS[mode]) }, mode)) }) }), _jsx("p", { className: "jini-hint", children: t('California permits optional categories until opt-out. Global Privacy Control always disables analytics and marketing.') }), _jsx(Field, { label: t('Policy version'), field: "policyVersion", problems: problems, children: aria => _jsx(PolicyVersionInput, { value: value.policyVersion, onChange: onChange, maxLength: limits.policyVersion ?? 128, aria: aria }) }), _jsx("p", { className: "jini-hint", children: t('Change the policy version to ask visitors to choose again.') })] });
|
|
56
68
|
}
|
|
57
69
|
/** Placement, bounded appearance tokens and allowlisted custom CSS. The engine checks contrast and
|
|
58
70
|
* CSS safety; its problems arrive on the `appearance` field and show under the custom CSS. */
|
|
@@ -12,6 +12,10 @@ export interface ConsentAppearanceView {
|
|
|
12
12
|
/** The host's consent configuration. The look fields are optional so older hosts stay valid. */
|
|
13
13
|
export interface ConsentSettingsView {
|
|
14
14
|
enabled: boolean;
|
|
15
|
+
/** Optional for older hosts; omitted means the dynamic code exception is off. */
|
|
16
|
+
allowUnsafeEval?: boolean;
|
|
17
|
+
/** Optional for older hosts; omitted uses the Jini banner when consent is enabled. */
|
|
18
|
+
bannerMode?: 'jini' | 'headless' | 'off';
|
|
15
19
|
regionMode: 'auto' | 'opt-in' | 'opt-out';
|
|
16
20
|
policyVersion: string;
|
|
17
21
|
privacyPolicyUrl: string;
|
|
@@ -3,6 +3,10 @@ import type { ConsentAppearanceView, ConsentFieldProblem, ConsentSettingsPanelPr
|
|
|
3
3
|
/** Keep value conversions and toggle behavior out of the view. React owns event signatures. */
|
|
4
4
|
export declare function useConsentSettingsPanel({ value, onChange }: Pick<ConsentSettingsPanelProps, 'value' | 'onChange'>, _options?: {}): {
|
|
5
5
|
toggle: () => void;
|
|
6
|
+
toggleUnsafeEval: () => void;
|
|
7
|
+
bannerMode: ({ mode }: {
|
|
8
|
+
mode: NonNullable<ConsentSettingsView["bannerMode"]>;
|
|
9
|
+
}, _optional?: {}) => () => void;
|
|
6
10
|
region: (event: ChangeEvent<HTMLSelectElement>) => void;
|
|
7
11
|
placement: (event: ChangeEvent<HTMLSelectElement>) => void;
|
|
8
12
|
text: (key: keyof ConsentSettingsView) => (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
@@ -3,6 +3,8 @@ import { useEffect, useState } from 'react';
|
|
|
3
3
|
export function useConsentSettingsPanel({ value, onChange }, _options = {}) {
|
|
4
4
|
return {
|
|
5
5
|
toggle: () => onChange({ patch: { enabled: !value.enabled } }),
|
|
6
|
+
toggleUnsafeEval: () => onChange({ patch: { allowUnsafeEval: value.allowUnsafeEval !== true } }),
|
|
7
|
+
bannerMode: ({ mode }, _optional = {}) => () => onChange({ patch: { bannerMode: mode } }),
|
|
6
8
|
region: (event) => onChange({ patch: { regionMode: event.target.value } }),
|
|
7
9
|
placement: (event) => onChange({ patch: { placement: event.target.value } }),
|
|
8
10
|
text: (key) => (event) => onChange({ patch: { [key]: event.target.value } }),
|
|
@@ -9,6 +9,10 @@ export declare const formsModule: {
|
|
|
9
9
|
readonly formsNavigation: import("../core/module/token.js").AdminPortToken<import("./ports.js").FormsNavigationPort, "admin.forms.navigation">;
|
|
10
10
|
};
|
|
11
11
|
readonly messages: {
|
|
12
|
+
readonly "Basic spam protection is on.": "Basic spam protection is on.";
|
|
13
|
+
readonly "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.": "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.";
|
|
14
|
+
readonly "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.": "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.";
|
|
15
|
+
readonly "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.": "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.";
|
|
12
16
|
readonly "Created / Updated": "Created / Updated";
|
|
13
17
|
readonly "Created {created} \u00B7 Updated {updated}": "Created {created} · Updated {updated}";
|
|
14
18
|
readonly "Created {date}": "Created {date}";
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { FormsTranslator } from "./models.js";
|
|
2
2
|
export declare const formsMessagesEn: {
|
|
3
|
+
readonly "Basic spam protection is on.": "Basic spam protection is on.";
|
|
4
|
+
readonly "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.": "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.";
|
|
5
|
+
readonly "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.": "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.";
|
|
6
|
+
readonly "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.": "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.";
|
|
3
7
|
readonly "Created / Updated": "Created / Updated";
|
|
4
8
|
readonly "Created {created} \u00B7 Updated {updated}": "Created {created} · Updated {updated}";
|
|
5
9
|
readonly "Created {date}": "Created {date}";
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export const formsMessagesEn = {
|
|
2
|
+
"Basic spam protection is on.": "Basic spam protection is on.",
|
|
3
|
+
"Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.": "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.",
|
|
4
|
+
"Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.": "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.",
|
|
5
|
+
"Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.": "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.",
|
|
2
6
|
"Created / Updated": "Created / Updated",
|
|
3
7
|
"Created {created} · Updated {updated}": "Created {created} · Updated {updated}",
|
|
4
8
|
"Created {date}": "Created {date}",
|
|
@@ -8,6 +8,6 @@ export function FormEditorFieldsBody(props) {
|
|
|
8
8
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "field-group", children: _jsxs("div", { className: "field-row", children: [_jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-name", children: t("Name") }), _jsx("input", { id: "form-name", value: name, onChange: handlers.onName, ...agentHandle({ handle: "form-editor-name" }, { role: "field", label: "This form's display name" }) })] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-slug", children: t("Slug") }), _jsx("input", { id: "form-slug", value: slug, disabled: !isNew, onChange: handlers.onSlug, ...agentHandle({ handle: "form-editor-slug" }, {
|
|
9
9
|
role: "field",
|
|
10
10
|
label: "This form's URL slug — only editable while creating a new form",
|
|
11
|
-
}) })] })] }) }), _jsx("div", { className: "field-group", children: _jsx(FormEditorModeSwitch, { props: props, handlers: handlers }) }), _jsx(FormEditorActions, { props: props })] }));
|
|
11
|
+
}) })] })] }) }), _jsx("div", { className: "field-group", children: _jsx(FormEditorModeSwitch, { props: props, handlers: handlers }) }), _jsxs("div", { className: "field-group", children: [_jsx("p", { children: _jsx("strong", { children: t("Basic spam protection is on.") }) }), _jsx("p", { className: "muted", children: t("Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.") }), _jsx("p", { className: "muted", children: t("Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.") }), _jsx("p", { className: "muted", children: t("Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.") })] }), _jsx(FormEditorActions, { props: props })] }));
|
|
12
12
|
}
|
|
13
13
|
//# sourceMappingURL=FormEditorFieldsBody.js.map
|
|
@@ -25,6 +25,10 @@ export declare function forms(_required: Record<string, never>, options?: FormsR
|
|
|
25
25
|
readonly formsNavigation: import("../../core/module/token.js").AdminPortToken<import("../ports.js").FormsNavigationPort, "admin.forms.navigation">;
|
|
26
26
|
};
|
|
27
27
|
messages: {
|
|
28
|
+
readonly "Basic spam protection is on.": "Basic spam protection is on.";
|
|
29
|
+
readonly "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.": "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.";
|
|
30
|
+
readonly "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.": "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.";
|
|
31
|
+
readonly "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.": "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.";
|
|
28
32
|
readonly "Created / Updated": "Created / Updated";
|
|
29
33
|
readonly "Created {created} \u00B7 Updated {updated}": "Created {created} · Updated {updated}";
|
|
30
34
|
readonly "Created {date}": "Created {date}";
|
|
@@ -243,6 +247,10 @@ export declare function forms(_required: Record<string, never>, options?: FormsR
|
|
|
243
247
|
readonly formsNavigation: import("../../core/module/token.js").AdminPortToken<import("../ports.js").FormsNavigationPort, "admin.forms.navigation">;
|
|
244
248
|
};
|
|
245
249
|
readonly messages: {
|
|
250
|
+
readonly "Basic spam protection is on.": "Basic spam protection is on.";
|
|
251
|
+
readonly "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.": "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.";
|
|
252
|
+
readonly "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.": "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.";
|
|
253
|
+
readonly "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.": "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.";
|
|
246
254
|
readonly "Created / Updated": "Created / Updated";
|
|
247
255
|
readonly "Created {created} \u00B7 Updated {updated}": "Created {created} · Updated {updated}";
|
|
248
256
|
readonly "Created {date}": "Created {date}";
|
|
@@ -465,6 +473,10 @@ export declare function forms(_required: Record<string, never>, options?: FormsR
|
|
|
465
473
|
readonly formsNavigation: import("../../core/module/token.js").AdminPortToken<import("../ports.js").FormsNavigationPort, "admin.forms.navigation">;
|
|
466
474
|
};
|
|
467
475
|
readonly messages: {
|
|
476
|
+
readonly "Basic spam protection is on.": "Basic spam protection is on.";
|
|
477
|
+
readonly "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.": "Hidden-field checks, submission timing when available, duplicate suppression, and rate limits.";
|
|
478
|
+
readonly "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.": "Live server defaults: wait 1 second; up to 5 submissions per minute per IP address and form.";
|
|
479
|
+
readonly "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.": "Exported forms use their destination's protection; timing and duplicate suppression may be unavailable. Custom embeds need the generated hidden controls for hidden-field checks.";
|
|
468
480
|
readonly "Created / Updated": "Created / Updated";
|
|
469
481
|
readonly "Created {created} \u00B7 Updated {updated}": "Created {created} · Updated {updated}";
|
|
470
482
|
readonly "Created {date}": "Created {date}";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/admin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15-rc.1",
|
|
4
4
|
"description": "Composable admin surface for Jini-hosted products: framework-free contracts, panel registry, and route model in `/core`; React shell and panels in `/react`. Import only the layers you need.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -448,12 +448,12 @@
|
|
|
448
448
|
"peerDependencies": {
|
|
449
449
|
"react": "^18.3.0 || ^19.0.0",
|
|
450
450
|
"react-dom": "^18.3.0 || ^19.0.0",
|
|
451
|
-
"@jini-ai/agentic": "^0.6.
|
|
452
|
-
"@jini-ai/ui": "^0.6.
|
|
453
|
-
"@jini-ai/ui-kit": "^0.6.
|
|
454
|
-
"@jini-ai/agent-plugins": "^0.6.
|
|
455
|
-
"@jini-ai/cms": "^0.6.
|
|
456
|
-
"@jini-ai/core": "^0.6.
|
|
451
|
+
"@jini-ai/agentic": "^0.6.15-rc.1",
|
|
452
|
+
"@jini-ai/ui": "^0.6.15-rc.1",
|
|
453
|
+
"@jini-ai/ui-kit": "^0.6.15-rc.1",
|
|
454
|
+
"@jini-ai/agent-plugins": "^0.6.15-rc.1",
|
|
455
|
+
"@jini-ai/cms": "^0.6.15-rc.1",
|
|
456
|
+
"@jini-ai/core": "^0.6.15-rc.1"
|
|
457
457
|
},
|
|
458
458
|
"peerDependenciesMeta": {
|
|
459
459
|
"react": {
|
|
@@ -491,12 +491,12 @@
|
|
|
491
491
|
"react": "^19.2.0",
|
|
492
492
|
"react-dom": "^19.2.0",
|
|
493
493
|
"@testing-library/user-event": "^14.6.1",
|
|
494
|
-
"@jini-ai/
|
|
495
|
-
"@jini-ai/
|
|
496
|
-
"@jini-ai/
|
|
497
|
-
"@jini-ai/
|
|
498
|
-
"@jini-ai/
|
|
499
|
-
"@jini-ai/
|
|
494
|
+
"@jini-ai/ui": "^0.6.15-rc.1",
|
|
495
|
+
"@jini-ai/ui-kit": "^0.6.15-rc.1",
|
|
496
|
+
"@jini-ai/core": "^0.6.15-rc.1",
|
|
497
|
+
"@jini-ai/agentic": "^0.6.15-rc.1",
|
|
498
|
+
"@jini-ai/cms": "^0.6.15-rc.1",
|
|
499
|
+
"@jini-ai/agent-plugins": "^0.6.15-rc.1"
|
|
500
500
|
},
|
|
501
501
|
"typesVersions": {
|
|
502
502
|
"*": {
|