@jini-ai/admin 0.6.15-rc.1 → 0.6.15
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/agent-plugins/adapters/memory.js +9 -3
- package/dist/agent-plugins/conformance/agent-plugins-api.conformance.js +1 -1
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +14 -5
- package/dist/agent-plugins/package-file-tree.js +4 -6
- package/dist/agent-plugins/react/components/AgentPluginInspector.js +5 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +10 -4
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +8 -5
- package/dist/agent-plugins/rules.js +3 -1
- package/dist/comments/adapters/http.js +2 -1
- package/dist/comments/adapters/memory.js +3 -1
- package/dist/comments/conformance/comments-api.conformance.js +3 -2
- package/dist/comments/controllers/comment-queue.controller.d.ts +1 -1
- package/dist/comments/controllers/comment-queue.controller.js +4 -2
- package/dist/core/entities/rules.js +13 -20
- package/dist/core/module/create-admin.js +2 -1
- package/dist/core/omit-keys.d.ts +7 -0
- package/dist/core/omit-keys.js +12 -0
- package/dist/core/ports/forms.d.ts +5 -3
- package/dist/core/transport/query-suffix.d.ts +3 -0
- package/dist/core/transport/query-suffix.js +5 -0
- package/dist/forms/adapters/http.js +3 -2
- package/dist/forms/adapters/memory.js +3 -2
- package/dist/forms/controllers/field-attributes-dialog.controller.js +17 -3
- package/dist/forms/controllers/form-fields-editor.controller.js +1 -2
- package/dist/forms/controllers/form-submission-detail.controller.js +1 -2
- package/dist/forms/controllers/form-submissions.controller.js +1 -2
- package/dist/forms/controllers/forms-list.controller.js +1 -2
- package/dist/forms/field-settings.js +15 -4
- package/dist/forms/html-rules.js +10 -13
- package/dist/forms/react/hooks/forms-view.hooks.d.ts +2 -2
- package/dist/forms/react/hooks/forms-view.hooks.js +9 -1
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +1 -3
- package/dist/forms/react/hooks/use-form-editor.hooks.js +8 -9
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -4
- package/dist/forms/react/hooks/use-form-submissions.hooks.js +1 -5
- package/dist/forms/react/hooks/use-forms-list.hooks.js +1 -2
- package/dist/forms/rules.js +7 -2
- package/dist/media/adapters/memory.js +5 -3
- package/dist/media/controllers/library.controller.js +4 -2
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.d.ts +1 -8
- package/dist/media/html-attributes.js +6 -161
- package/dist/media/react/components/MediaCard.js +11 -1
- package/dist/media/react/hooks/EditMediaPanel.hooks.js +18 -7
- package/dist/media/react/hooks/LibraryTab.hooks.js +3 -2
- package/dist/media/react/hooks/ProvidersTab.hooks.js +19 -9
- package/dist/media/rules.js +6 -3
- package/dist/playground/adapters/memory.js +26 -7
- package/dist/playground/conformance/playground-api.conformance.js +6 -3
- package/dist/playground/react/hooks/PlaygroundPorts.hooks.js +6 -2
- package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +1 -1
- package/dist/react/components/ConfirmButton/ConfirmButton.js +2 -5
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +2 -2
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +3 -1
- package/dist/react/components/RowMenu/RowMenu.hooks.js +4 -8
- package/dist/react/components/Sidebar.js +1 -1
- package/dist/react/entities/EntityField.js +1 -0
- package/dist/react/entities/contribution.js +2 -1
- package/dist/react/entities/types.js +1 -1
- package/dist/react/overlays.js +2 -1
- package/dist/react/shell/AdminShell.hooks.js +1 -1
- package/dist/react/shell/use-admin-shell-session.js +2 -1
- package/dist/react/shell/use-admin-theme.js +6 -6
- package/dist/redirects/adapters/memory.js +16 -13
- package/dist/security/adapters/http.js +10 -11
- package/dist/security/adapters/memory.js +50 -15
- package/dist/security/conformance/security-api.conformance.js +6 -3
- package/dist/security/controllers/access-tokens.controller.js +7 -3
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +12 -5
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +8 -3
- package/dist/security/react/hooks/SecurityPorts.hooks.js +6 -2
- package/dist/security/rules.js +19 -5
- package/dist/seo/adapters/memory.js +5 -2
- package/dist/seo/controllers/seo-entry.controller.js +2 -1
- package/dist/seo/controllers/sitemap.controller.js +2 -1
- package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +1 -1
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -21
- package/dist/seo/react/hooks/SeoPage.hooks.js +0 -8
- package/dist/seo/react/hooks/use-seo.hooks.js +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +1 -1
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +9 -1
- package/dist/seo/react/pages/SeoPage.js +17 -3
- package/dist/source-control/adapters/memory.js +30 -14
- package/dist/source-control/controllers/source-control.controller.js +5 -2
- package/dist/source-control/react/components/SourceControlProviderRow.js +6 -1
- package/dist/source-control/react/hooks/SourceControlPorts.hooks.js +6 -2
- package/dist/source-control/rules.js +9 -3
- package/dist/widgets/adapters/http.js +2 -1
- package/dist/widgets/adapters/memory.js +4 -2
- package/dist/widgets/conformance/widgets-api.conformance.js +5 -2
- package/dist/widgets/controllers/instance-editor.controller.js +3 -2
- package/dist/widgets/controllers/library.controller.js +2 -1
- package/dist/widgets/controllers/region-editor.controller.js +2 -1
- package/dist/widgets/controllers/regions.controller.js +2 -1
- package/dist/widgets/react/components/WidgetInstanceNotices.d.ts +1 -1
- package/dist/widgets/react/components/WidgetInstanceNotices.js +2 -0
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +5 -2
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +5 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +1 -1
- package/dist/widgets/react/pages/WidgetRegionEditor.js +1 -1
- package/dist/widgets/react/pages/WidgetsLibrary.js +1 -1
- package/dist/widgets/rules.js +5 -1
- package/package.json +13 -13
|
@@ -7,6 +7,7 @@ import { settleFromCache, useSharedMediaLibrary } from './MediaPage.hooks.js';
|
|
|
7
7
|
import { useMediaPorts } from './MediaPorts.hooks.js';
|
|
8
8
|
import { mediaMessagesEn } from '../../messages.en.js';
|
|
9
9
|
import { canRestoreMedia } from '../../rules.js';
|
|
10
|
+
import { omitKeys } from '../../../core/omit-keys.js';
|
|
10
11
|
const EMPTY_LABELS = {
|
|
11
12
|
images: 'No images yet. Upload an image to see it here.',
|
|
12
13
|
videos: 'No videos yet. Upload a video to see it here.',
|
|
@@ -52,7 +53,7 @@ function purgeDialogView({ snapshot }, _optional = {}) {
|
|
|
52
53
|
function queryHandlers({ controller }, _optional = {}) {
|
|
53
54
|
return {
|
|
54
55
|
onStatus: ({ value }) => {
|
|
55
|
-
const
|
|
56
|
+
const query = omitKeys(controller?.getSnapshot().query ?? {}, ['status']);
|
|
56
57
|
void controller?.setQuery({ query: { ...query, ...(value === 'active' || value === 'trashed' ? { status: value } : {}) } });
|
|
57
58
|
},
|
|
58
59
|
onSearch: ({ value: search }) => {
|
|
@@ -183,7 +184,7 @@ export function useLibraryTab({ params, permissions = [] }, _optional = {}) {
|
|
|
183
184
|
const item = controller?.getSnapshot().pendingPurge;
|
|
184
185
|
// The controller refuses to purge without a pending item, so there is nothing to announce.
|
|
185
186
|
if (!can.canPurge || !controller || !item)
|
|
186
|
-
return;
|
|
187
|
+
return undefined;
|
|
187
188
|
const index = items.findIndex(row => row.id === item.id);
|
|
188
189
|
return controller.confirmPurge({ confirmed: true }).then(saved => {
|
|
189
190
|
if (saved) {
|
|
@@ -34,8 +34,10 @@ async function saveProviderSettings({ port, item, entry, signal }, _optional = {
|
|
|
34
34
|
await port.saveSettings({ id: item.id, ...next }, { signal });
|
|
35
35
|
}
|
|
36
36
|
async function saveProvider({ port, item, signal }, { entry } = {}) {
|
|
37
|
-
if (!entry)
|
|
38
|
-
|
|
37
|
+
if (!entry) {
|
|
38
|
+
await removeIfStored({ port, item, signal });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
39
41
|
await saveProviderSettings({ port, item, entry, signal });
|
|
40
42
|
if (entry.apiKey?.trim())
|
|
41
43
|
await port.saveCredential({ id: item.id, credential: entry.apiKey }, { signal });
|
|
@@ -57,17 +59,21 @@ async function saveSequentially({ port, providers, lastRead, signal }, _optional
|
|
|
57
59
|
function rowStatus({ entry, pending }, _optional = {}) {
|
|
58
60
|
const saved = isMarkerOnlyEntry(entry);
|
|
59
61
|
const mask = saved ? maskedKeyLabel(entry) : null;
|
|
62
|
+
const unsavedLabel = pending ? 'Unsaved' : null;
|
|
60
63
|
return {
|
|
61
64
|
mask,
|
|
62
|
-
statusLabel: saved ? `Saved (${mask})` :
|
|
65
|
+
statusLabel: saved ? `Saved (${mask})` : unsavedLabel,
|
|
63
66
|
statusClassName: saved ? 'jini-field-status-badge jini-field-status-badge-success' : 'jini-field-status-badge',
|
|
64
67
|
};
|
|
65
68
|
}
|
|
69
|
+
/** Names the provider default an empty endpoint falls back to, when it has one. */
|
|
70
|
+
function defaultBaseUrlHint({ entry, item }) {
|
|
71
|
+
return !entry.baseUrl?.trim() && item.defaultBaseUrl ? `Uses ${item.defaultBaseUrl} by default.` : null;
|
|
72
|
+
}
|
|
66
73
|
function baseUrlHint({ entry, item }, _optional = {}) {
|
|
67
74
|
const invalid = isProviderBaseUrlInvalid(entry);
|
|
68
75
|
return {
|
|
69
|
-
invalid, hint: invalid ? 'Enter an absolute http:// or https:// URL that is not a private or internal address.'
|
|
70
|
-
: !entry.baseUrl?.trim() && item.defaultBaseUrl ? `Uses ${item.defaultBaseUrl} by default.` : null,
|
|
76
|
+
invalid, hint: invalid ? 'Enter an absolute http:// or https:// URL that is not a private or internal address.' : defaultBaseUrlHint({ entry, item }),
|
|
71
77
|
hintClassName: invalid ? 'jini-field-hint jini-hint-error' : 'jini-field-hint',
|
|
72
78
|
hintRole: invalid ? 'alert' : undefined,
|
|
73
79
|
ariaInvalid: invalid || undefined,
|
|
@@ -177,10 +183,14 @@ export function useProvidersTab(_required = {}, _optional = {}) {
|
|
|
177
183
|
const catalog = mediaProviders?.catalog ?? discoveredCatalog;
|
|
178
184
|
const pinnedIds = mediaProviders?.pinnedProviderIds ?? [];
|
|
179
185
|
const ordered = sortProvidersByConfigured(catalog, editor.providers, pinnedIds);
|
|
180
|
-
const toggleKey = (id) => setVisibleKeys(current => {
|
|
181
|
-
next
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
const toggleKey = (id) => setVisibleKeys(current => {
|
|
187
|
+
const next = new Set(current);
|
|
188
|
+
if (next.has(id))
|
|
189
|
+
next.delete(id);
|
|
190
|
+
else
|
|
191
|
+
next.add(id);
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
184
194
|
const rows = ordered.map((item, index) => providerRow({ item, entry: editor.providers[item.id] ?? {}, editor, visible: visibleKeys.has(item.id), toggle: () => toggleKey(item.id) }, { divider: index > 0 && !pinnedIds.includes(item.id) && pinnedIds.includes(ordered[index - 1].id), disabled: !mediaProviders }));
|
|
185
195
|
return { rows, ...editorView({ editor, port: mediaProviders }) };
|
|
186
196
|
}
|
package/dist/media/rules.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimChars } from '@jini-ai/core/text';
|
|
1
2
|
/** Falls back to "all" for an absent or unrecognized ?tab= value — don't trust a raw
|
|
2
3
|
* query value: a stale link or a typo must not blank the panel. */
|
|
3
4
|
export function resolveActiveTab({ tabId }, _optional = {}) {
|
|
@@ -138,11 +139,13 @@ export function mediaEmbedSnippet({ slug }, _optional = {}) {
|
|
|
138
139
|
export function mediaRowHandles({ media }, _optional = {}) {
|
|
139
140
|
const used = new Set();
|
|
140
141
|
return media.map((item, index) => {
|
|
141
|
-
const slug = item.id.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
142
|
+
const slug = trimChars({ text: item.id.toLowerCase().replace(/[^a-z0-9]+/g, '-'), chars: '-' });
|
|
142
143
|
const preferred = `media-item-${slug || index + 1}`;
|
|
143
144
|
let handle = preferred, suffix = 2;
|
|
144
|
-
while (used.has(handle))
|
|
145
|
-
handle = `${preferred}-${suffix
|
|
145
|
+
while (used.has(handle)) {
|
|
146
|
+
handle = `${preferred}-${suffix}`;
|
|
147
|
+
suffix += 1;
|
|
148
|
+
}
|
|
146
149
|
used.add(handle);
|
|
147
150
|
return handle;
|
|
148
151
|
});
|
|
@@ -18,13 +18,32 @@ export function createMemoryPlaygroundTargets(_required, { onListenerError } = {
|
|
|
18
18
|
}
|
|
19
19
|
return {
|
|
20
20
|
getSnapshot: (_required = {}, _optional = {}) => snapshot,
|
|
21
|
-
subscribe({ listener }, _optional = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
subscribe({ listener }, _optional = {}) {
|
|
22
|
+
if (!disposed)
|
|
23
|
+
listeners.add(listener);
|
|
24
|
+
return () => listeners.delete(listener);
|
|
25
|
+
},
|
|
26
|
+
register({ target }, _optional = {}) {
|
|
27
|
+
if (disposed)
|
|
28
|
+
throw new Error('Playground target scope disposed');
|
|
29
|
+
const ticket = {};
|
|
30
|
+
owner = ticket;
|
|
31
|
+
publish(target);
|
|
32
|
+
return { release(_required = {}, _optional = {}) {
|
|
33
|
+
if (owner !== ticket)
|
|
34
|
+
return;
|
|
35
|
+
owner = null;
|
|
36
|
+
publish(null);
|
|
37
|
+
} };
|
|
38
|
+
},
|
|
39
|
+
dispose(_required = {}, _optional = {}) {
|
|
40
|
+
if (disposed)
|
|
41
|
+
return;
|
|
42
|
+
disposed = true;
|
|
43
|
+
owner = null;
|
|
44
|
+
publish(null);
|
|
45
|
+
listeners.clear();
|
|
46
|
+
},
|
|
28
47
|
};
|
|
29
48
|
}
|
|
30
49
|
//# sourceMappingURL=memory.js.map
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** Run with an isolated bus; this temporarily registers and releases test targets. */
|
|
2
2
|
export function runPlaygroundApiConformance({ targets }, _optional = {}) {
|
|
3
3
|
const checks = [];
|
|
4
|
-
const assert = (ok, name) => {
|
|
5
|
-
|
|
4
|
+
const assert = (ok, name) => {
|
|
5
|
+
if (!ok)
|
|
6
|
+
throw new Error(`Playground conformance: ${name}`);
|
|
7
|
+
checks.push(name);
|
|
8
|
+
};
|
|
6
9
|
const a = {}, b = {};
|
|
7
10
|
let count = 0;
|
|
8
|
-
const stop = targets.subscribe({ listener: () => count
|
|
11
|
+
const stop = targets.subscribe({ listener: () => { count += 1; } });
|
|
9
12
|
const first = targets.register({ target: a });
|
|
10
13
|
let second;
|
|
11
14
|
try {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
2
|
import { AdminConfigError } from '../../../core/module/index.js';
|
|
3
3
|
export const PlaygroundPortsContext = createContext(null);
|
|
4
|
-
export function usePlaygroundPorts(_required = {}, _optional = {}) {
|
|
5
|
-
|
|
4
|
+
export function usePlaygroundPorts(_required = {}, _optional = {}) {
|
|
5
|
+
const ports = useContext(PlaygroundPortsContext);
|
|
6
|
+
if (!ports)
|
|
7
|
+
throw new AdminConfigError({ issues: ['React scope unavailable: playground'] });
|
|
8
|
+
return ports;
|
|
9
|
+
}
|
|
6
10
|
//# sourceMappingURL=PlaygroundPorts.hooks.js.map
|
|
@@ -29,11 +29,8 @@ const DEFAULT_PENDING_LABEL = '…';
|
|
|
29
29
|
*/
|
|
30
30
|
export function ConfirmButton({ useConfirmButton: useConfirmButtonState = useConfirmButton, ...props }) {
|
|
31
31
|
const { confirming, buttonRef, handleClick, handleBlur } = useConfirmButtonState({ onConfirm: props.onConfirm, document }, { pending: props.pending, disabled: props.disabled });
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
: confirming
|
|
35
|
-
? props.confirmLabel
|
|
36
|
-
: props.label;
|
|
32
|
+
const idleLabel = confirming ? props.confirmLabel : props.label;
|
|
33
|
+
const label = props.pending ? (props.pendingLabel ?? DEFAULT_PENDING_LABEL) : idleLabel;
|
|
37
34
|
const className = [props.className, props.destructive ? 'btn-danger' : null].filter(Boolean).join(' ') || undefined;
|
|
38
35
|
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: buttonRef, type: "button", className: className, "data-armed": confirming ? 'true' : undefined, disabled: props.disabled || props.pending, onClick: handleClick, onBlur: handleBlur, "aria-label": props.ariaLabel, children: label }), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: confirming ? `Press "${props.confirmLabel}" to confirm, or press Escape to cancel.` : '' })] }));
|
|
39
36
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { agentHandle } from '@jini-ai/agentic';
|
|
2
|
+
import { omitKeys } from '../../../core/omit-keys.js';
|
|
2
3
|
/**
|
|
3
4
|
* The agent-attribute port `ConfirmDialog` hands UI-kit's confirm controller. UI-kit names selects
|
|
4
5
|
* separately; agentic publishes them as fields and still owns handle validation. Absent role/label
|
|
@@ -18,7 +19,6 @@ export const confirmDialogAgentAttrs = (required, optional = {}) => agentHandle(
|
|
|
18
19
|
* @complexity O(k) in the number of attributes.
|
|
19
20
|
*/
|
|
20
21
|
export function withoutAccessibleName({ attrs }, _optional = {}) {
|
|
21
|
-
|
|
22
|
-
return rest;
|
|
22
|
+
return omitKeys(attrs ?? {}, ['aria-label']);
|
|
23
23
|
}
|
|
24
24
|
//# sourceMappingURL=confirm-dialog-attrs.js.map
|
|
@@ -154,7 +154,9 @@ function parseEmbedConfig(raw) {
|
|
|
154
154
|
}
|
|
155
155
|
function galleryIdentityLabel(config) {
|
|
156
156
|
const count = Array.isArray(config.images) ? config.images.length : 0;
|
|
157
|
-
|
|
157
|
+
if (count === 0)
|
|
158
|
+
return 'no images set';
|
|
159
|
+
return count === 1 ? '1 image' : `${count} images`;
|
|
158
160
|
}
|
|
159
161
|
export function InteractiveHtmlEditor(props) {
|
|
160
162
|
return (_jsx(HtmlEditor, { ...props, isProtectedElement: (el) => isProtectedEmbedElement({ el }), describeEmbedPlaceholder: (el) => describeEmbedPlaceholder({ el }) }));
|
|
@@ -121,14 +121,10 @@ export function useRowMenu({ itemCount, window, document }) {
|
|
|
121
121
|
openAt(0);
|
|
122
122
|
}
|
|
123
123
|
function onTriggerKeyDown(e) {
|
|
124
|
-
if (e.key
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
else if (e.key === 'ArrowUp') {
|
|
129
|
-
e.preventDefault();
|
|
130
|
-
openAt(itemCount - 1);
|
|
131
|
-
}
|
|
124
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')
|
|
125
|
+
return;
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
openAt(e.key === 'ArrowDown' ? 0 : itemCount - 1);
|
|
132
128
|
}
|
|
133
129
|
function onMenuKeyDown(e) {
|
|
134
130
|
switch (e.key) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { DEFAULT_ADMIN_BASE, adminHref } from '../../core/routing/rules.js';
|
|
5
5
|
import { useNavSections } from '../hooks/use-nav-sections.js';
|
|
@@ -43,6 +43,7 @@ export function EntityField(props) {
|
|
|
43
43
|
case 'integer':
|
|
44
44
|
case 'real': return _jsx(NumberInput, { ...props });
|
|
45
45
|
case 'text': return _jsx("input", { ...commonProps(props), value: typeof value === 'string' ? value : '', onChange: (e) => onChange({ value: blankToUndefined(e.target.value) }) });
|
|
46
|
+
default: return null;
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
//# sourceMappingURL=EntityField.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { trimCharsEnd } from '@jini-ai/core/text';
|
|
2
3
|
import { EntityIndex } from './EntityIndex.js';
|
|
3
4
|
import { EntityList } from './EntityList.js';
|
|
4
5
|
import { EntityDetail } from './EntityDetail.js';
|
|
@@ -18,7 +19,7 @@ export function createEntityRoutes({ adminBase, panelId, navigate }) {
|
|
|
18
19
|
throw new Error('Entity panelId must be one URL segment');
|
|
19
20
|
if (!adminBase.startsWith('/') || adminBase.startsWith('//') || /[?#]/.test(adminBase))
|
|
20
21
|
throw new Error('Entity adminBase must be an absolute pathname');
|
|
21
|
-
const base =
|
|
22
|
+
const base = trimCharsEnd({ text: adminBase, chars: '/' });
|
|
22
23
|
function routePath({ entity, id, view }) {
|
|
23
24
|
const root = `/${panelId}`;
|
|
24
25
|
if (entity === undefined)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Stable source templates let host dictionaries translate copy before values are inserted. */
|
|
2
2
|
export function entityText({ translate, key, values }) {
|
|
3
|
-
return translate({ key: key }).replace(/\{(
|
|
3
|
+
return translate({ key: key }).replace(/\{(\w+)\}/g, (match, name) => Object.hasOwn(values, name) ? String(values[name]) : match);
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
6
|
* Adapts the host's object-arg {@link EntityTranslate} to the positional `(key) => string` form the
|
package/dist/react/overlays.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createControllerStore } from '../core/module/controller-store.js';
|
|
|
4
4
|
export function createOverlayController(_required = {}, _optional = {}) {
|
|
5
5
|
const store = createControllerStore({ initial: { entries: [] } });
|
|
6
6
|
let sequence = 0, cancelActive = null;
|
|
7
|
+
const nextSequence = () => { sequence += 1; return sequence; };
|
|
7
8
|
return {
|
|
8
9
|
getSnapshot: store.getSnapshot,
|
|
9
10
|
subscribe: store.subscribe,
|
|
@@ -28,7 +29,7 @@ export function createOverlayController(_required = {}, _optional = {}) {
|
|
|
28
29
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
29
30
|
store.set({
|
|
30
31
|
patch: {
|
|
31
|
-
entries: [{ id:
|
|
32
|
+
entries: [{ id: nextSequence(), render: () => render({ resolve: finish, cancel }) }],
|
|
32
33
|
},
|
|
33
34
|
});
|
|
34
35
|
});
|
|
@@ -39,7 +39,8 @@ export function useAdminShellSession(args) {
|
|
|
39
39
|
return;
|
|
40
40
|
if (operation === 'logout')
|
|
41
41
|
loggingOut.current = owner;
|
|
42
|
-
|
|
42
|
+
generation.current += 1;
|
|
43
|
+
const request = generation.current;
|
|
43
44
|
const isCurrent = () => active.current === owner && generation.current === request;
|
|
44
45
|
setOwned({ context, port, state: { status: 'checking' } });
|
|
45
46
|
// settleOperation never rejects, so the logout latch below always clears.
|
|
@@ -4,7 +4,9 @@ import { applyAdminTheme, resolveColorScheme } from '@jini-ai/ui/theme';
|
|
|
4
4
|
export function useAdminTheme({ userId, workspace }, { theme, colorScheme, environment, preferenceStore, onColorSchemeChange } = {}) {
|
|
5
5
|
const scope = JSON.stringify([userId, Object.entries(workspace).sort(([a], [b]) => a.localeCompare(b))]);
|
|
6
6
|
const [saved, setSaved] = useState(null);
|
|
7
|
-
|
|
7
|
+
// Only the setter is used: bumping the revision re-renders, which re-reads the live media
|
|
8
|
+
// snapshot below; explicit choices never query the OS.
|
|
9
|
+
const [, setMediaRevision] = useState(0);
|
|
8
10
|
const preference = colorScheme ?? (saved?.scope === scope ? saved.preference : 'system');
|
|
9
11
|
useEffect(() => {
|
|
10
12
|
if (!userId || !preferenceStore) {
|
|
@@ -16,25 +18,23 @@ export function useAdminTheme({ userId, workspace }, { theme, colorScheme, envir
|
|
|
16
18
|
}, [userId, workspace, scope, preferenceStore]);
|
|
17
19
|
useEffect(() => {
|
|
18
20
|
if (!theme)
|
|
19
|
-
return;
|
|
21
|
+
return undefined;
|
|
20
22
|
if (!environment)
|
|
21
23
|
throw new TypeError('AdminShell theme requires a themeEnvironment port');
|
|
22
24
|
return applyAdminTheme({ theme }, environment);
|
|
23
25
|
}, [theme, environment]);
|
|
24
26
|
useEffect(() => {
|
|
25
27
|
if (!environment || preference !== 'system')
|
|
26
|
-
return;
|
|
28
|
+
return undefined;
|
|
27
29
|
const media = environment.matchMedia('(prefers-color-scheme: dark)');
|
|
28
30
|
const changed = () => setMediaRevision((revision) => revision + 1);
|
|
29
31
|
media.addEventListener?.('change', changed);
|
|
30
32
|
return () => media.removeEventListener?.('change', changed);
|
|
31
33
|
}, [environment, preference]);
|
|
32
|
-
// A revision refreshes the live media snapshot; explicit choices never query the OS.
|
|
33
|
-
void mediaRevision;
|
|
34
34
|
const resolved = environment ? resolveColorScheme({ preference }, { matchMedia: environment.matchMedia }) : 'light';
|
|
35
35
|
useEffect(() => {
|
|
36
36
|
if (!environment)
|
|
37
|
-
return;
|
|
37
|
+
return undefined;
|
|
38
38
|
const previous = environment.target.getAttribute('data-color-scheme');
|
|
39
39
|
environment.target.setAttribute('data-color-scheme', resolved);
|
|
40
40
|
return () => {
|
|
@@ -15,15 +15,22 @@ export function createMemoryRedirectsApi({ redirects = [], hits = {} }, { valida
|
|
|
15
15
|
throw new AdminApiError({ message: `redirect '${id}' was not found`, status: 404 }, { code: 'REDIRECT_NOT_FOUND' });
|
|
16
16
|
return row;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
/** The core port documents reference-host rejection of a regex `matchType`. A supplied
|
|
19
|
+
* validator owns alternate hosts and sees the whole candidate instead. */
|
|
20
|
+
function validateOrRejectRegex(candidate, matchType) {
|
|
21
|
+
if (validate) {
|
|
22
|
+
validate(candidate);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (matchType === 'regex')
|
|
23
26
|
throw new AdminApiError({ message: 'regex match type is not supported', status: 400 }, { code: 'REDIRECT_VALIDATION_ERROR' });
|
|
27
|
+
}
|
|
28
|
+
function create(input, source) {
|
|
29
|
+
validateOrRejectRegex(input, input.matchType);
|
|
24
30
|
let id;
|
|
25
31
|
do {
|
|
26
|
-
id = `memory-redirect-${nextId
|
|
32
|
+
id = `memory-redirect-${nextId}`;
|
|
33
|
+
nextId += 1;
|
|
27
34
|
} while (rows.has(id));
|
|
28
35
|
const row = Object.freeze({ ...input, id, status: 'active', override: input.override ?? false,
|
|
29
36
|
priority: input.priority ?? 0, source, sourceEntryId: null, fromPathAtCapture: null, toPathAtCapture: null,
|
|
@@ -34,20 +41,16 @@ export function createMemoryRedirectsApi({ redirects = [], hits = {} }, { valida
|
|
|
34
41
|
}
|
|
35
42
|
return {
|
|
36
43
|
async listRedirects(_required, filter = {}) {
|
|
44
|
+
const matches = (wanted, actual) => wanted === undefined || actual === wanted;
|
|
37
45
|
return Object.freeze([...rows.values()].filter(row => !trashed.has(row.id) &&
|
|
38
|
-
(filter.status
|
|
39
|
-
(filter.source === undefined || row.source === filter.source) &&
|
|
40
|
-
(filter.matchType === undefined || row.matchType === filter.matchType)));
|
|
46
|
+
matches(filter.status, row.status) && matches(filter.source, row.source) && matches(filter.matchType, row.matchType)));
|
|
41
47
|
},
|
|
42
48
|
async getRedirect({ id }) { return get(id); },
|
|
43
49
|
async createRedirect(input, options = {}) { return create({ ...input, ...options }, 'manual'); },
|
|
44
50
|
async updateRedirect({ id }, patch = {}) {
|
|
45
51
|
const before = get(id);
|
|
46
52
|
const next = { ...before, ...patch };
|
|
47
|
-
|
|
48
|
-
validate(next);
|
|
49
|
-
else if (patch.matchType === 'regex')
|
|
50
|
-
throw new AdminApiError({ message: 'regex match type is not supported', status: 400 }, { code: 'REDIRECT_VALIDATION_ERROR' });
|
|
53
|
+
validateOrRejectRegex(next, patch.matchType);
|
|
51
54
|
const row = Object.freeze({ ...next, version: before.version + 1 });
|
|
52
55
|
rows.set(id, row);
|
|
53
56
|
if (patch.status === 'active')
|
|
@@ -40,15 +40,14 @@ export function createHttpSecurityApi({ transport, workspacePath }, _optional =
|
|
|
40
40
|
function mediaEntry({ entry }, { apiKey } = {}) {
|
|
41
41
|
return { ...(entry.baseUrl !== undefined ? { baseUrl: entry.baseUrl } : {}), ...(entry.model !== undefined ? { model: entry.model } : {}), ...(apiKey !== undefined ? { apiKey } : {}) };
|
|
42
42
|
}
|
|
43
|
+
const MEDIA_PROVIDERS_PATH = '/media/providers';
|
|
43
44
|
/** Replacing the ENTIRE map requires sibling settings unchanged. Blank apiKey preserves their stored keys. */
|
|
44
45
|
function rebuildMedia(current, id, token) {
|
|
45
46
|
const next = {};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
next[key] = mediaEntry({ entry }, { apiKey: token });
|
|
51
|
-
}
|
|
47
|
+
// The replaced entry is dropped when no token is given; every sibling is echoed unchanged.
|
|
48
|
+
const kept = Object.entries(current).filter(([key]) => key !== id || token !== undefined);
|
|
49
|
+
for (const [key, entry] of kept)
|
|
50
|
+
next[key] = mediaEntry({ entry }, key === id && token !== undefined ? { apiKey: token } : {});
|
|
52
51
|
return next;
|
|
53
52
|
}
|
|
54
53
|
export function createHttpOtherCredentials({ transport, workspacePath }, { mediaLabels = {} } = {}) {
|
|
@@ -62,7 +61,7 @@ export function createHttpOtherCredentials({ transport, workspacePath }, { media
|
|
|
62
61
|
return r.data.isSet ? [{ store, id: store, label: singles[store].label, category: 'ai', configured: true, supportsReplace: true, updatedAt: r.data.updatedAt }] : [];
|
|
63
62
|
}
|
|
64
63
|
if (store === 'media-provider') {
|
|
65
|
-
const map = await request(
|
|
64
|
+
const map = await request(MEDIA_PROVIDERS_PATH, 'GET', options);
|
|
66
65
|
return Object.entries(map).filter(([, entry]) => entry.apiKeyConfigured).map(([id]) => ({ store, id, label: mediaLabels[id] ?? id, category: 'media', configured: true, supportsReplace: true }));
|
|
67
66
|
}
|
|
68
67
|
const { servers } = await request('/mcp-servers', 'GET', options);
|
|
@@ -74,16 +73,16 @@ export function createHttpOtherCredentials({ transport, workspacePath }, { media
|
|
|
74
73
|
if (store === 'external-mcp')
|
|
75
74
|
throw new Error('Edit the full server configuration on its own screen');
|
|
76
75
|
if (store === 'media-provider') {
|
|
77
|
-
const current = await request(
|
|
78
|
-
await request(
|
|
76
|
+
const current = await request(MEDIA_PROVIDERS_PATH, 'GET', options);
|
|
77
|
+
await request(MEDIA_PROVIDERS_PATH, 'PUT', options, rebuildMedia(current, id, token));
|
|
79
78
|
}
|
|
80
79
|
else
|
|
81
80
|
await request(singles[store].path, 'PUT', options, { apiKey: token });
|
|
82
81
|
},
|
|
83
82
|
async remove({ store, id }, options = {}) {
|
|
84
83
|
if (store === 'media-provider') {
|
|
85
|
-
const current = await request(
|
|
86
|
-
await request(
|
|
84
|
+
const current = await request(MEDIA_PROVIDERS_PATH, 'GET', options);
|
|
85
|
+
await request(MEDIA_PROVIDERS_PATH, 'PUT', options, rebuildMedia(current, id));
|
|
87
86
|
}
|
|
88
87
|
else if (store === 'external-mcp')
|
|
89
88
|
await request(`/mcp-servers/${encodeURIComponent(id)}`, 'DELETE', options);
|
|
@@ -11,6 +11,7 @@ function customProjection({ patch }, { initial } = {}) {
|
|
|
11
11
|
export function createMemorySecurityApi({ providers = [], rows = [] }, _optional = {}) {
|
|
12
12
|
let items = rows.map(r => Object.freeze({ ...r }));
|
|
13
13
|
let sequence = 0;
|
|
14
|
+
const nextSequence = () => { sequence += 1; return sequence; };
|
|
14
15
|
function store(kind) {
|
|
15
16
|
function isDefaultFor({ patch, providerId }, { initial } = {}) {
|
|
16
17
|
if (kind === 'custom')
|
|
@@ -24,8 +25,11 @@ export function createMemorySecurityApi({ providers = [], rows = [] }, _optional
|
|
|
24
25
|
...(kind === 'custom' ? customProjection({ patch }, { initial }) : {}),
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
|
-
function check(label, providerId, id) {
|
|
28
|
-
|
|
28
|
+
function check(label, providerId, id) {
|
|
29
|
+
const sameScope = (r) => r.kind === kind && r.id !== id && (kind === 'custom' || r.providerId === providerId);
|
|
30
|
+
if (items.some(r => sameScope(r) && r.label.toLowerCase() === label.toLowerCase()))
|
|
31
|
+
throw new Error('Duplicate label');
|
|
32
|
+
}
|
|
29
33
|
function insert(row) {
|
|
30
34
|
if (row.isDefault)
|
|
31
35
|
items = items.map(r => r.kind === kind && r.providerId === row.providerId ? Object.freeze({ ...r, isDefault: false }) : r);
|
|
@@ -34,10 +38,23 @@ export function createMemorySecurityApi({ providers = [], rows = [] }, _optional
|
|
|
34
38
|
}
|
|
35
39
|
return {
|
|
36
40
|
async list(_r, o = {}) { o.signal?.throwIfAborted(); return Object.freeze(items.filter(r => r.kind === kind)); },
|
|
37
|
-
async create(input, o = {}) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
async create(input, o = {}) {
|
|
42
|
+
o.signal?.throwIfAborted();
|
|
43
|
+
if (!Object.values(input.connection).some(v => v.trim()) || !input.label.trim())
|
|
44
|
+
throw new Error('Incomplete credential');
|
|
45
|
+
const providerId = kind === 'custom' ? '' : input.connection.providerId ?? '';
|
|
46
|
+
check(input.label, providerId);
|
|
47
|
+
return insert(project({ id: `credential-${nextSequence()}`, patch: input, providerId, label: input.label }));
|
|
48
|
+
},
|
|
49
|
+
async update({ id, patch }, o = {}) {
|
|
50
|
+
o.signal?.throwIfAborted();
|
|
51
|
+
const old = items.find(r => r.id === id && r.kind === kind);
|
|
52
|
+
if (!old)
|
|
53
|
+
throw new Error('Credential not found');
|
|
54
|
+
const label = patch.label ?? old.label;
|
|
55
|
+
check(label, old.providerId, id);
|
|
56
|
+
return insert(project({ id, patch, providerId: old.providerId, label }, { initial: old }));
|
|
57
|
+
},
|
|
41
58
|
async remove({ id }, o = {}) { o.signal?.throwIfAborted(); const old = items.find(r => r.id === id && r.kind === kind); items = items.filter(r => !(r.id === id && r.kind === kind)); if (old?.isDefault) {
|
|
42
59
|
const next = items.filter(r => r.kind === kind && r.providerId === old.providerId).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
43
60
|
if (next)
|
|
@@ -51,8 +68,13 @@ export function createMemoryOtherCredentials({ rows = [] }, _optional = {}) {
|
|
|
51
68
|
let items = rows.map(row => Object.freeze({ ...row }));
|
|
52
69
|
return {
|
|
53
70
|
async list({ store }, o = {}) { o.signal?.throwIfAborted(); return items.filter(r => r.store === store); },
|
|
54
|
-
async replace({ store, id, token }, o = {}) {
|
|
55
|
-
|
|
71
|
+
async replace({ store, id, token }, o = {}) {
|
|
72
|
+
o.signal?.throwIfAborted();
|
|
73
|
+
const row = items.find(r => r.store === store && r.id === id);
|
|
74
|
+
if (!row?.supportsReplace || !token.trim())
|
|
75
|
+
throw new Error('Replacement unavailable');
|
|
76
|
+
items = items.map(r => r === row ? Object.freeze({ ...r, configured: true, updatedAt: new Date().toISOString() }) : r);
|
|
77
|
+
},
|
|
56
78
|
async remove({ store, id }, o = {}) { o.signal?.throwIfAborted(); items = items.filter(r => r.store !== store || r.id !== id); },
|
|
57
79
|
};
|
|
58
80
|
}
|
|
@@ -61,14 +83,27 @@ export function createMemoryRootKey({ status = { active: false, source: 'none',
|
|
|
61
83
|
function activate(outcome) { current = Object.freeze({ ...current, active: true, state: 'active', source: 'file', fingerprint: 'memory-fingerprint' }); return { outcome, fingerprint: 'memory-fingerprint', keyFilePath: current.keyFilePath, runtimeMode: current.runtimeMode }; }
|
|
62
84
|
return {
|
|
63
85
|
async status(_r, o = {}) { o.signal?.throwIfAborted(); return current; },
|
|
64
|
-
async generate(_r, o = {}) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
async generate(_r, o = {}) {
|
|
87
|
+
o.signal?.throwIfAborted();
|
|
88
|
+
if (current.active)
|
|
89
|
+
return { ...activate('already-active') };
|
|
90
|
+
if (current.state !== 'missing')
|
|
91
|
+
throw new Error('Key dependent data');
|
|
92
|
+
return activate('created');
|
|
93
|
+
},
|
|
94
|
+
async importToken({ token }, o = {}) {
|
|
95
|
+
o.signal?.throwIfAborted();
|
|
96
|
+
if (!/^[0-9a-f]{64}$/i.test(token))
|
|
97
|
+
throw new Error('Invalid token');
|
|
98
|
+
return { ...activate('unlocked'), resealed: 0 };
|
|
99
|
+
},
|
|
69
100
|
async previewStartFresh(_r, o = {}) { o.signal?.throwIfAborted(); return preview; },
|
|
70
|
-
async startFresh({ confirm }, o = {}) {
|
|
71
|
-
|
|
101
|
+
async startFresh({ confirm }, o = {}) {
|
|
102
|
+
o.signal?.throwIfAborted();
|
|
103
|
+
if (confirm !== START_FRESH_CONFIRMATION)
|
|
104
|
+
throw new Error('Confirmation required');
|
|
105
|
+
return { ...activate('started-fresh'), discarded: preview.removes, kept: 0, affectedWebhooks: preview.affectedWebhooks, restorePointId: 'memory-restore-point' };
|
|
106
|
+
},
|
|
72
107
|
};
|
|
73
108
|
}
|
|
74
109
|
//# sourceMappingURL=memory.js.map
|
|
@@ -3,9 +3,12 @@ import { credentialStore } from '../rules.js';
|
|
|
3
3
|
export async function runSecurityApiConformance({ createApi, provider }, _optional = {}) {
|
|
4
4
|
const api = await createApi({}), target = credentialStore({ api, kind: provider.kind });
|
|
5
5
|
let passed = 0;
|
|
6
|
-
function check(value, message) {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
function check(value, message) {
|
|
7
|
+
if (!value)
|
|
8
|
+
throw new Error(`Security contract: ${message}`);
|
|
9
|
+
passed++;
|
|
10
|
+
}
|
|
11
|
+
const suffix = `${Date.now()}-${globalThis.crypto.randomUUID()}`;
|
|
9
12
|
const secret = `contract-secret-${suffix}`;
|
|
10
13
|
const connection = { providerId: provider.id, [provider.tokenField]: secret, ...Object.fromEntries(provider.fields.filter(f => f.name !== provider.tokenField).map(f => [f.name, 'test-value'])) };
|
|
11
14
|
const ids = [];
|
|
@@ -12,7 +12,8 @@ export function createAccessTokensController({ api, permissions = [] }, _optiona
|
|
|
12
12
|
async function load(_required, _optional = {}) {
|
|
13
13
|
if (store.signal.aborted || !grants.has('security.read'))
|
|
14
14
|
return;
|
|
15
|
-
|
|
15
|
+
generation += 1;
|
|
16
|
+
const ticket = generation;
|
|
16
17
|
store.set({ patch: { loading: true } });
|
|
17
18
|
// Independent stores retain their own successful results; one unavailable store cannot hide the rest.
|
|
18
19
|
const kinds = ['publish', 'source-control', 'custom'];
|
|
@@ -105,8 +106,11 @@ export function createAccessTokensController({ api, permissions = [] }, _optiona
|
|
|
105
106
|
store.set({ patch: { draft: null, error: null } }); },
|
|
106
107
|
filter({ query, category }, _optional = {}) { store.set({ patch: { ...(query !== undefined ? { query } : {}), ...(category ? { category } : {}) } }); },
|
|
107
108
|
remove({ row }, _optional = {}) { return write({ row, remove: true }); },
|
|
108
|
-
makeDefault({ row }, _optional = {}) {
|
|
109
|
-
|
|
109
|
+
makeDefault({ row }, _optional = {}) {
|
|
110
|
+
if (row.kind === 'custom')
|
|
111
|
+
return Promise.resolve(false);
|
|
112
|
+
return write({ row, remove: false });
|
|
113
|
+
},
|
|
110
114
|
dispose(_required = {}, _optional = {}) { generation++; store.dispose(); },
|
|
111
115
|
};
|
|
112
116
|
}
|