@jini-ai/admin 0.4.12 → 0.6.10
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 +70 -2
- package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
- package/dist/agent-plugins/package-file-tree.js +54 -51
- package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
- package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
- package/dist/browser/navigation.js +44 -27
- package/dist/comments/adapters/memory.js +30 -12
- package/dist/comments/conformance/comments-api.conformance.js +55 -33
- package/dist/comments/controllers/comment-queue.controller.js +13 -4
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
- package/dist/comments/react/index.d.ts +2 -1
- package/dist/comments/react/index.js +1 -1
- package/dist/consent/react/ConsentFields.d.ts +35 -0
- package/dist/consent/react/ConsentFields.js +76 -0
- package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
- package/dist/consent/react/ConsentPreviewFrame.js +14 -0
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
- package/dist/consent/react/ConsentSettingsPanel.js +6 -17
- package/dist/consent/react/index.d.ts +6 -1
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
- package/dist/consent/react/settings-panel.hooks.js +26 -0
- package/dist/core/entities/rules.js +126 -81
- package/dist/core/manifest/rules.js +10 -10
- package/dist/core/module/create-admin.js +195 -117
- package/dist/core/ports/redirects.d.ts +8 -6
- package/dist/core/ports/redirects.js +8 -6
- package/dist/core/routing/rules.js +9 -4
- package/dist/core/transport/http.js +37 -20
- package/dist/forms/adapters/memory.js +22 -11
- package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
- package/dist/forms/conformance/forms-api.conformance.js +42 -46
- package/dist/forms/field-settings.js +150 -115
- package/dist/forms/html-rules.js +45 -28
- package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
- package/dist/forms/react/components/FieldSettingsRows.js +29 -0
- package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
- package/dist/forms/react/components/FieldSettingsSection.js +5 -6
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
- package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
- package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
- package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
- package/dist/forms/react/components/FormSubmissions.js +4 -20
- package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
- package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
- package/dist/media/adapters/memory.js +31 -26
- package/dist/media/controllers/edit-media.controller.js +2 -1
- package/dist/media/controllers/library.controller.js +48 -35
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.js +29 -15
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/react/components/MediaIcons.d.ts +0 -3
- package/dist/media/react/components/MediaIcons.js +0 -9
- package/dist/media/react/components/MediaLightbox.d.ts +2 -1
- package/dist/media/react/components/MediaLightbox.js +11 -5
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
- package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
- package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
- package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
- package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
- package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
- package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
- package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
- package/dist/media/react/index.js +4 -0
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/LibraryTab.js +2 -2
- package/dist/media/rules.d.ts +7 -0
- package/dist/media/rules.js +25 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
- package/dist/react/components/RowMenu/RowMenu.js +16 -10
- package/dist/react/components/Sidebar.js +32 -26
- package/dist/react/entities/EntityEdit.js +17 -14
- package/dist/react/entities/EntityField.d.ts +5 -2
- package/dist/react/entities/EntityField.js +38 -23
- package/dist/react/entities/EntityList.js +6 -5
- package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
- package/dist/react/entities/RelationTruncationNotice.js +9 -0
- package/dist/react/entities/use-entity-edit.d.ts +4 -2
- package/dist/react/entities/use-entity-edit.js +52 -33
- package/dist/react/shell/AdminShell.js +40 -23
- package/dist/react/shell/model.d.ts +4 -1
- package/dist/react/shell/model.js +3 -2
- package/dist/react/shell/use-admin-shell-session.js +25 -26
- package/dist/redirects/adapters/memory.js +9 -2
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
- package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
- package/dist/security/adapters/http.js +25 -10
- package/dist/security/adapters/memory.js +19 -5
- package/dist/security/controllers/access-tokens.controller.js +27 -26
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +17 -9
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
- package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.js +12 -3
- package/dist/security/rules.js +45 -25
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/react/components/MediaRefField.d.ts +1 -1
- package/dist/seo/react/components/MediaRefField.js +19 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
- package/dist/seo/react/pages/SeoPage.d.ts +4 -4
- package/dist/seo/react/pages/SeoPage.js +26 -6
- package/dist/seo/rules.js +39 -10
- package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
- package/dist/source-control/controllers/source-control.controller.js +20 -8
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
- package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
- package/dist/widgets/controllers/instance-editor.controller.js +33 -24
- package/dist/widgets/controllers/library.controller.js +18 -12
- package/dist/widgets/controllers/region-editor.controller.js +6 -5
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
- package/dist/widgets/rules.d.ts +14 -2
- package/dist/widgets/rules.js +9 -6
- package/package.json +23 -14
- package/src/themes/default/admin.css +51 -0
- package/src/themes/default/chat.css +21 -0
- package/src/themes/default/manifest.json +29 -0
- package/dist/media/styles.d.ts +0 -4
- package/dist/media/styles.js +0 -5
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { START_FRESH_CONFIRMATION } from '../rules.js';
|
|
2
|
+
function customUsernameField({ patch }, { initial } = {}) {
|
|
3
|
+
const username = patch.username !== undefined ? patch.username : patch.connection?.username ?? initial?.username;
|
|
4
|
+
return username ? { username } : {};
|
|
5
|
+
}
|
|
6
|
+
function customHosts({ patch }, { initial } = {}) { return Object.freeze([...(patch.additionalHosts ?? initial?.additionalHosts ?? [])]); }
|
|
7
|
+
function customProjection({ patch }, { initial } = {}) {
|
|
8
|
+
return { category: patch.category ?? initial?.category ?? 'general', baseUrl: patch.baseUrl ?? initial?.baseUrl ?? '', additionalHosts: customHosts({ patch }, { initial }), ...customUsernameField({ patch }, { initial }) };
|
|
9
|
+
}
|
|
2
10
|
/** Fake backend: retains only read models, never credential material. */
|
|
3
11
|
export function createMemorySecurityApi({ providers = [], rows = [] }, _optional = {}) {
|
|
4
12
|
let items = rows.map(r => Object.freeze({ ...r }));
|
|
5
13
|
let sequence = 0;
|
|
6
14
|
function store(kind) {
|
|
7
|
-
function
|
|
15
|
+
function isDefaultFor({ patch, providerId }, { initial } = {}) {
|
|
16
|
+
if (kind === 'custom')
|
|
17
|
+
return false;
|
|
18
|
+
return patch.isDefault ?? initial?.isDefault ?? !items.some(r => r.kind === kind && r.providerId === providerId);
|
|
19
|
+
}
|
|
20
|
+
/** The caller resolves the label: create requires one, update falls back to the saved label. */
|
|
21
|
+
function project({ id, patch, providerId, label }, { initial } = {}) {
|
|
8
22
|
const now = new Date().toISOString();
|
|
9
|
-
return Object.freeze({ kind, id, providerId, label
|
|
10
|
-
...(kind === 'custom' ? {
|
|
23
|
+
return Object.freeze({ kind, id, providerId, label, configured: true, isDefault: isDefaultFor({ patch, providerId }, { initial }), createdAt: initial?.createdAt ?? now, updatedAt: now,
|
|
24
|
+
...(kind === 'custom' ? customProjection({ patch }, { initial }) : {}),
|
|
11
25
|
});
|
|
12
26
|
}
|
|
13
27
|
function check(label, providerId, id) { if (items.some(r => r.kind === kind && r.id !== id && (kind === 'custom' || r.providerId === providerId) && r.label.toLowerCase() === label.toLowerCase()))
|
|
@@ -21,9 +35,9 @@ export function createMemorySecurityApi({ providers = [], rows = [] }, _optional
|
|
|
21
35
|
return {
|
|
22
36
|
async list(_r, o = {}) { o.signal?.throwIfAborted(); return Object.freeze(items.filter(r => r.kind === kind)); },
|
|
23
37
|
async create(input, o = {}) { o.signal?.throwIfAborted(); if (!Object.values(input.connection).some(v => v.trim()) || !input.label.trim())
|
|
24
|
-
throw new Error('Incomplete credential'); const providerId = kind === 'custom' ? '' : input.connection.providerId ?? ''; check(input.label, providerId); return insert(project(`credential-${++sequence}`, input, providerId)); },
|
|
38
|
+
throw new Error('Incomplete credential'); const providerId = kind === 'custom' ? '' : input.connection.providerId ?? ''; check(input.label, providerId); return insert(project({ id: `credential-${++sequence}`, patch: input, providerId, label: input.label })); },
|
|
25
39
|
async update({ id, patch }, o = {}) { o.signal?.throwIfAborted(); const old = items.find(r => r.id === id && r.kind === kind); if (!old)
|
|
26
|
-
throw new Error('Credential not found');
|
|
40
|
+
throw new Error('Credential not found'); const label = patch.label ?? old.label; check(label, old.providerId, id); return insert(project({ id, patch, providerId: old.providerId, label }, { initial: old })); },
|
|
27
41
|
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) {
|
|
28
42
|
const next = items.filter(r => r.kind === kind && r.providerId === old.providerId).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
29
43
|
if (next)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createControllerStore } from '../../core/module/controller-store.js';
|
|
2
|
-
import { blankCredentialDraft, buildCredentialPatch, credentialStore, validateCredentialDraft, writePermission, credentialLabel } from '../rules.js';
|
|
2
|
+
import { blankCredentialDraft, buildCredentialPatch, credentialCategory, credentialStore, validateCredentialDraft, writePermission, credentialLabel } from '../rules.js';
|
|
3
|
+
/** An edit draft never carries the saved secret: token and extra values start blank. */
|
|
4
|
+
function draftFromRow({ row, providers }, _optional = {}) {
|
|
5
|
+
return { ...blankCredentialDraft({ kind: row.kind, providerId: row.providerId }), id: row.id, label: credentialLabel({ row, providers }), category: credentialCategory({ row }), baseUrl: row.baseUrl ?? '', additionalHosts: row.additionalHosts?.join('\n') ?? '', username: row.username ?? '' };
|
|
6
|
+
}
|
|
3
7
|
export function createAccessTokensController({ api, permissions = [] }, _optional = {}) {
|
|
4
8
|
const grants = new Set(permissions);
|
|
5
9
|
const store = createControllerStore({ initial: { rows: [], providers: [], draft: null, loading: false, saving: false, error: null, errors: {}, query: '', category: 'all' } });
|
|
@@ -43,7 +47,7 @@ export function createAccessTokensController({ api, permissions = [] }, _optiona
|
|
|
43
47
|
const k = row?.kind ?? kind ?? 'custom';
|
|
44
48
|
if (!allowed(k) || store.getSnapshot().saving)
|
|
45
49
|
return;
|
|
46
|
-
const draft = row ?
|
|
50
|
+
const draft = row ? draftFromRow({ row, providers: store.getSnapshot().providers }) : blankCredentialDraft({ kind: k, ...(providerId ? { providerId } : {}) });
|
|
47
51
|
store.set({ patch: { draft, error: null } });
|
|
48
52
|
}
|
|
49
53
|
async function save(_required, _optional = {}) {
|
|
@@ -57,44 +61,41 @@ export function createAccessTokensController({ api, permissions = [] }, _optiona
|
|
|
57
61
|
}
|
|
58
62
|
const patch = buildCredentialPatch({ draft, providers: state.providers });
|
|
59
63
|
const target = credentialStore({ api, kind: draft.kind });
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// First-create default assignment can change siblings; the server owns that invariant.
|
|
66
|
-
await load({});
|
|
67
|
-
return !store.signal.aborted;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
store.set({ patch: { error: 'Unable to save credential' } });
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
store.set({ patch: { saving: false } });
|
|
75
|
-
}
|
|
64
|
+
return mutate({ failure: 'Unable to save credential', run: async () => {
|
|
65
|
+
const row = draft.id ? await target.update({ id: draft.id, patch }, { signal: store.signal }) : await target.create(patch, { signal: store.signal });
|
|
66
|
+
// Clear accepted secrets before any later read can fail. A successful save must never leave a saved secret visible.
|
|
67
|
+
store.set({ patch: { draft: null, rows: Object.freeze([...store.getSnapshot().rows.filter(r => !(r.kind === row.kind && r.id === row.id)), row]) } });
|
|
68
|
+
} });
|
|
76
69
|
}
|
|
77
70
|
async function write({ row, remove }) {
|
|
78
71
|
if (!allowed(row.kind) || store.getSnapshot().saving)
|
|
79
72
|
return false;
|
|
73
|
+
return mutate({ failure: 'Unable to update credential', run: async () => {
|
|
74
|
+
const target = credentialStore({ api, kind: row.kind });
|
|
75
|
+
if (remove)
|
|
76
|
+
await target.remove({ id: row.id }, { signal: store.signal });
|
|
77
|
+
else
|
|
78
|
+
await target.update({ id: row.id, patch: { isDefault: true } }, { signal: store.signal });
|
|
79
|
+
} });
|
|
80
|
+
}
|
|
81
|
+
/** Shared write lifecycle: one write at a time, a fixed failure message, and a refetch after success. */
|
|
82
|
+
async function mutate({ run, failure }, _optional = {}) {
|
|
80
83
|
store.set({ patch: { saving: true, error: null } });
|
|
84
|
+
let ok = false;
|
|
81
85
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await target.remove({ id: row.id }, { signal: store.signal });
|
|
85
|
-
else
|
|
86
|
-
await target.update({ id: row.id, patch: { isDefault: true } }, { signal: store.signal });
|
|
86
|
+
await run();
|
|
87
|
+
// First-create default assignment can change siblings; the server owns that invariant.
|
|
87
88
|
// Remove/default can promote another row. Refetch rather than duplicating server promotion rules.
|
|
88
89
|
await load({});
|
|
89
|
-
|
|
90
|
+
ok = !store.signal.aborted;
|
|
90
91
|
}
|
|
91
92
|
catch {
|
|
92
|
-
store.set({ patch: { error:
|
|
93
|
-
return false;
|
|
93
|
+
store.set({ patch: { error: failure } });
|
|
94
94
|
}
|
|
95
95
|
finally {
|
|
96
96
|
store.set({ patch: { saving: false } });
|
|
97
97
|
}
|
|
98
|
+
return ok;
|
|
98
99
|
}
|
|
99
100
|
return {
|
|
100
101
|
getSnapshot: store.getSnapshot, subscribe: store.subscribe, load, begin, save,
|
|
@@ -40,6 +40,8 @@ export function createOtherCredentialsController({ api, permissions = [] }, _opt
|
|
|
40
40
|
if (store.signal.aborted)
|
|
41
41
|
return false;
|
|
42
42
|
store.set({ patch: { saving: true } });
|
|
43
|
+
// One exit after `finally`: a catch that returned left a fall-through path nothing could take.
|
|
44
|
+
let saved = false;
|
|
43
45
|
try {
|
|
44
46
|
if (remove)
|
|
45
47
|
await api.remove({ store: row.store, id: row.id }, { signal: store.signal });
|
|
@@ -49,15 +51,15 @@ export function createOtherCredentialsController({ api, permissions = [] }, _opt
|
|
|
49
51
|
delete drafts[`${row.store}:${row.id}`];
|
|
50
52
|
store.set({ patch: { drafts } });
|
|
51
53
|
await read(row.store, generation);
|
|
52
|
-
|
|
54
|
+
saved = !store.signal.aborted;
|
|
53
55
|
}
|
|
54
56
|
catch {
|
|
55
57
|
store.set({ patch: { errors: { ...store.getSnapshot().errors, [row.store]: 'Unable to update credential' } } });
|
|
56
|
-
return false;
|
|
57
58
|
}
|
|
58
59
|
finally {
|
|
59
60
|
store.set({ patch: { saving: false } });
|
|
60
61
|
}
|
|
62
|
+
return saved;
|
|
61
63
|
});
|
|
62
64
|
lane = run.then(() => undefined);
|
|
63
65
|
return run;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { createControllerStore } from '../../core/module/controller-store.js';
|
|
2
2
|
import { ROOT_KEY_PERMISSION, START_FRESH_CONFIRMATION, isRootKeyLocked } from '../rules.js';
|
|
3
|
+
/** Receivers named in a result need their new signing secret; the message tells the operator which ones. */
|
|
4
|
+
function resultMessage({ result, response }, _optional = {}) {
|
|
5
|
+
const hooks = response.affectedWebhooks;
|
|
6
|
+
return hooks?.length ? `${result} Update receivers: ${hooks.map(w => `${w.label} (${w.targetUrl})`).join(', ')}` : result;
|
|
7
|
+
}
|
|
3
8
|
export function createRootKeyController({ api, permissions = [] }, _optional = {}) {
|
|
4
9
|
const granted = permissions.includes(ROOT_KEY_PERMISSION);
|
|
5
10
|
const store = createControllerStore({ initial: { status: null, preview: null, token: '', confirm: '', loading: false, saving: false, error: null, result: null, confirming: false } });
|
|
6
11
|
let generation = 0;
|
|
7
12
|
const allowed = () => granted && !store.signal.aborted;
|
|
13
|
+
const canPreview = () => { const state = store.getSnapshot(); return allowed() && isRootKeyLocked({ status: state.status }) && !state.saving && !state.loading; };
|
|
8
14
|
async function load(_required, _optional = {}) {
|
|
9
15
|
if (!allowed())
|
|
10
16
|
return;
|
|
@@ -28,19 +34,20 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
|
|
|
28
34
|
if (!allowed() || store.getSnapshot().saving)
|
|
29
35
|
return false;
|
|
30
36
|
store.set({ patch: { saving: true, error: null } });
|
|
37
|
+
let ok = false;
|
|
31
38
|
try {
|
|
32
39
|
const response = await run();
|
|
33
|
-
store.set({ patch: { token: '', confirm: '', preview: null, confirming: false, result:
|
|
40
|
+
store.set({ patch: { token: '', confirm: '', preview: null, confirming: false, result: resultMessage({ result, response }) } });
|
|
34
41
|
await load({});
|
|
35
|
-
|
|
42
|
+
ok = !store.signal.aborted;
|
|
36
43
|
}
|
|
37
44
|
catch {
|
|
38
45
|
store.set({ patch: { error: 'Recovery refused. Check the host key configuration.' } });
|
|
39
|
-
return false;
|
|
40
46
|
}
|
|
41
47
|
finally {
|
|
42
48
|
store.set({ patch: { saving: false } });
|
|
43
49
|
}
|
|
50
|
+
return ok;
|
|
44
51
|
}
|
|
45
52
|
return {
|
|
46
53
|
getSnapshot: store.getSnapshot, subscribe: store.subscribe, load,
|
|
@@ -57,26 +64,27 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
|
|
|
57
64
|
},
|
|
58
65
|
importToken(_required, _optional = {}) { const token = store.getSnapshot().token.trim(); return token && isRootKeyLocked({ status: store.getSnapshot().status }) ? execute(() => api.importToken({ token }, { signal: store.signal }), 'Unlocked. Saved credentials work again.') : Promise.resolve(false); },
|
|
59
66
|
async preview(_required, _optional = {}) {
|
|
60
|
-
if (!
|
|
67
|
+
if (!canPreview())
|
|
61
68
|
return false;
|
|
62
69
|
const ticket = ++generation;
|
|
63
70
|
store.set({ patch: { loading: true, preview: null, confirm: '', error: null } });
|
|
71
|
+
let ok = false;
|
|
64
72
|
try {
|
|
65
73
|
const preview = await api.previewStartFresh({}, { signal: store.signal });
|
|
66
|
-
if (ticket
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
if (ticket === generation && allowed()) {
|
|
75
|
+
store.set({ patch: { preview, confirming: true } });
|
|
76
|
+
ok = true;
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
catch {
|
|
72
80
|
if (ticket === generation)
|
|
73
81
|
store.set({ patch: { error: 'Recovery preview unavailable' } });
|
|
74
|
-
return false;
|
|
75
82
|
}
|
|
76
83
|
finally {
|
|
77
84
|
if (ticket === generation)
|
|
78
85
|
store.set({ patch: { loading: false } });
|
|
79
86
|
}
|
|
87
|
+
return ok;
|
|
80
88
|
},
|
|
81
89
|
startFresh(_required, _optional = {}) { const state = store.getSnapshot(); if (!isRootKeyLocked({ status: state.status }) || !state.preview || !state.confirming || state.confirm !== START_FRESH_CONFIRMATION)
|
|
82
90
|
return Promise.resolve(false); return execute(() => api.startFresh({ confirm: state.confirm }, { signal: store.signal }), 'Done. Re-enter removed credentials.'); },
|
|
@@ -1,44 +1,11 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
2
|
import type { CredentialSummary, OtherCredentialSummary, CredentialCategory } from '../../models.js';
|
|
3
|
+
type Removal = {
|
|
4
|
+
row: CredentialSummary | OtherCredentialSummary;
|
|
5
|
+
type: 'access' | 'other';
|
|
6
|
+
};
|
|
3
7
|
export declare function useAccessTokensTab({ permissions }: TabViewProps, _optional?: {}): {
|
|
4
|
-
|
|
5
|
-
loading: boolean;
|
|
6
|
-
saving: boolean;
|
|
7
|
-
rows: {
|
|
8
|
-
row: CredentialSummary;
|
|
9
|
-
label: string;
|
|
10
|
-
canWrite: boolean;
|
|
11
|
-
edit(): void;
|
|
12
|
-
remove(): void;
|
|
13
|
-
makeDefault(): void;
|
|
14
|
-
}[];
|
|
15
|
-
otherRows: {
|
|
16
|
-
row: OtherCredentialSummary;
|
|
17
|
-
canWrite: boolean;
|
|
18
|
-
token: string;
|
|
19
|
-
onToken({ value }: {
|
|
20
|
-
value: string;
|
|
21
|
-
}): void;
|
|
22
|
-
replace(): void;
|
|
23
|
-
remove(): void;
|
|
24
|
-
}[];
|
|
25
|
-
providers: {
|
|
26
|
-
provider: import("../../models.js").CredentialProvider;
|
|
27
|
-
canWrite: boolean;
|
|
28
|
-
add(): void;
|
|
29
|
-
}[];
|
|
30
|
-
categories: readonly {
|
|
31
|
-
value: CredentialCategory | "all";
|
|
32
|
-
label: string;
|
|
33
|
-
}[];
|
|
34
|
-
query: string;
|
|
35
|
-
category: "all" | CredentialCategory;
|
|
36
|
-
total: number;
|
|
37
|
-
matched: number;
|
|
38
|
-
removal: {
|
|
39
|
-
row: CredentialSummary | OtherCredentialSummary;
|
|
40
|
-
type: "access" | "other";
|
|
41
|
-
} | null;
|
|
8
|
+
removal: Removal | null;
|
|
42
9
|
draft: import("../../models.js").CredentialDraft | null;
|
|
43
10
|
errors: string[];
|
|
44
11
|
canAddCustom: boolean;
|
|
@@ -90,5 +57,40 @@ export declare function useAccessTokensTab({ permissions }: TabViewProps, _optio
|
|
|
90
57
|
}, _optional?: {}): Promise<boolean>;
|
|
91
58
|
dispose(_required?: Record<string, never>, _optional?: {}): void;
|
|
92
59
|
} | null;
|
|
60
|
+
total: number;
|
|
61
|
+
matched: number;
|
|
62
|
+
rows: {
|
|
63
|
+
row: CredentialSummary;
|
|
64
|
+
label: string;
|
|
65
|
+
canWrite: boolean;
|
|
66
|
+
edit(): void;
|
|
67
|
+
remove(): void;
|
|
68
|
+
makeDefault(): void;
|
|
69
|
+
}[];
|
|
70
|
+
otherRows: {
|
|
71
|
+
row: OtherCredentialSummary;
|
|
72
|
+
canWrite: boolean;
|
|
73
|
+
token: string;
|
|
74
|
+
onToken({ value }: {
|
|
75
|
+
value: string;
|
|
76
|
+
}): void;
|
|
77
|
+
replace(): void;
|
|
78
|
+
remove(): void;
|
|
79
|
+
}[];
|
|
80
|
+
providers: {
|
|
81
|
+
provider: import("../../models.js").CredentialProvider;
|
|
82
|
+
canWrite: boolean;
|
|
83
|
+
add(): void;
|
|
84
|
+
}[];
|
|
85
|
+
categories: readonly {
|
|
86
|
+
value: CredentialCategory | "all";
|
|
87
|
+
label: string;
|
|
88
|
+
}[];
|
|
89
|
+
query: string;
|
|
90
|
+
category: "all" | CredentialCategory;
|
|
91
|
+
loading: boolean;
|
|
92
|
+
saving: boolean;
|
|
93
|
+
denied: boolean;
|
|
93
94
|
};
|
|
95
|
+
export {};
|
|
94
96
|
//# sourceMappingURL=AccessTokensTab.hooks.d.ts.map
|
|
@@ -4,6 +4,35 @@ import { createAccessTokensController } from '../../controllers/access-tokens.co
|
|
|
4
4
|
import { createOtherCredentialsController } from '../../controllers/other-credentials.controller.js';
|
|
5
5
|
import { CATEGORIES, credentialLabel, filterCredentials, filterOtherCredentials, otherWritePermission, writePermission } from '../../rules.js';
|
|
6
6
|
import { noOtherCredentials, useSecurityPorts } from './SecurityPorts.hooks.js';
|
|
7
|
+
function currentFilter({ snapshot: s }, _optional = {}) { return { query: s?.query ?? '', category: s?.category ?? 'all' }; }
|
|
8
|
+
function accessRows({ snapshot: s, controller, scope }, _optional = {}) {
|
|
9
|
+
if (!s)
|
|
10
|
+
return [];
|
|
11
|
+
const { permissions, query, category, setRemoval } = scope;
|
|
12
|
+
return filterCredentials({ rows: s.rows, providers: s.providers, query, category }).map(row => ({ row, label: credentialLabel({ row, providers: s.providers }), canWrite: permissions.includes(writePermission({ kind: row.kind })), edit() { controller?.begin({ row }); }, remove() { setRemoval({ row, type: 'access' }); }, makeDefault() { void controller?.makeDefault({ row }); } }));
|
|
13
|
+
}
|
|
14
|
+
function otherRowsView({ snapshot: o, controller, scope }, _optional = {}) {
|
|
15
|
+
if (!o)
|
|
16
|
+
return [];
|
|
17
|
+
const { permissions, query, category, setRemoval } = scope;
|
|
18
|
+
return filterOtherCredentials({ rows: o.rows, query, category }).map(row => ({ row, canWrite: permissions.includes(otherWritePermission({ store: row.store })), token: o.drafts[`${row.store}:${row.id}`] ?? '', onToken({ value }) { controller?.setDraft({ key: `${row.store}:${row.id}`, token: value }); }, replace() { void controller?.replace({ row }); }, remove() { setRemoval({ row, type: 'other' }); } }));
|
|
19
|
+
}
|
|
20
|
+
function providerOptions({ snapshot: s, controller, scope }, _optional = {}) {
|
|
21
|
+
const { permissions, query, category } = scope;
|
|
22
|
+
return (s?.providers ?? []).filter(p => (category === 'all' || p.category === category) && `${p.label} ${p.id}`.toLowerCase().includes(query.trim().toLowerCase())).map(provider => ({ provider, canWrite: permissions.includes(writePermission({ kind: provider.kind })), add() { controller?.begin({ kind: provider.kind, providerId: provider.id }); } }));
|
|
23
|
+
}
|
|
24
|
+
function tokenCounts({ access: s, other: o, query }, _optional = {}) {
|
|
25
|
+
// Count remains global across categories; the search count answers how many saved tokens this host holds.
|
|
26
|
+
const total = (s?.rows.length ?? 0) + (o?.rows.length ?? 0);
|
|
27
|
+
const matched = (s ? filterCredentials({ rows: s.rows, providers: s.providers, query, category: 'all' }).length : 0) + (o ? filterOtherCredentials({ rows: o.rows, query, category: 'all' }).length : 0);
|
|
28
|
+
return { total, matched };
|
|
29
|
+
}
|
|
30
|
+
function tabStatus({ access: s, other: o }, _optional = {}) {
|
|
31
|
+
return { loading: s?.loading ?? true, saving: (s?.saving ?? false) || (o?.saving ?? false) };
|
|
32
|
+
}
|
|
33
|
+
function errorList({ access: s, other: o }, _optional = {}) {
|
|
34
|
+
return [...(s?.error ? [s.error] : []), ...Object.values(s?.errors ?? {}), ...Object.values(o?.errors ?? {})];
|
|
35
|
+
}
|
|
7
36
|
export function useAccessTokensTab({ permissions = [] }, _optional = {}) {
|
|
8
37
|
const ports = useSecurityPorts({});
|
|
9
38
|
const key = permissions.join('\0');
|
|
@@ -12,15 +41,13 @@ export function useAccessTokensTab({ permissions = [] }, _optional = {}) {
|
|
|
12
41
|
const other = useController({ create: () => createOtherCredentialsController({ api: ports.otherCredentials ?? noOtherCredentials, permissions }), dependencies: [ports.otherCredentials, key] }, { start: ({ controller }) => { void controller.load({}); } });
|
|
13
42
|
const [removal, setRemoval] = useState(null);
|
|
14
43
|
const s = access.snapshot, o = other.snapshot;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return { denied: !canRead, loading: s?.loading ?? true, saving: (s?.saving ?? false) || (o?.saving ?? false), rows, otherRows, providers, categories: CATEGORIES, query, category, total, matched, removal,
|
|
23
|
-
draft: s?.draft ?? null, errors: [...(s?.error ? [s.error] : []), ...Object.values(s?.errors ?? {}), ...Object.values(o?.errors ?? {})],
|
|
44
|
+
const { query, category } = currentFilter({ snapshot: s });
|
|
45
|
+
const scope = { permissions, query, category, setRemoval };
|
|
46
|
+
const rows = accessRows({ snapshot: s, controller: access.controller, scope });
|
|
47
|
+
const otherRows = otherRowsView({ snapshot: o, controller: other.controller, scope });
|
|
48
|
+
const providers = providerOptions({ snapshot: s, controller: access.controller, scope });
|
|
49
|
+
return { denied: !canRead, ...tabStatus({ access: s, other: o }), rows, otherRows, providers, categories: CATEGORIES, query, category, ...tokenCounts({ access: s, other: o, query }), removal,
|
|
50
|
+
draft: s?.draft ?? null, errors: errorList({ access: s, other: o }),
|
|
24
51
|
canAddCustom: permissions.includes('custom-credentials.write'),
|
|
25
52
|
addCustom() { access.controller?.begin({ kind: 'custom' }); },
|
|
26
53
|
onQuery({ value }) { access.controller?.filter({ query: value }); },
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
2
|
export declare function useRootKeyTab({ permissions, params }: TabViewProps, _optional?: {}): {
|
|
3
|
-
denied: boolean;
|
|
4
|
-
loading: boolean;
|
|
5
|
-
saving: boolean;
|
|
6
|
-
status: import("../../models.js").RootKeyStatus | null | undefined;
|
|
7
|
-
error: string | null | undefined;
|
|
8
|
-
result: string | null | undefined;
|
|
9
|
-
scopeNotice: string;
|
|
10
|
-
token: string;
|
|
11
|
-
confirm: string;
|
|
12
|
-
confirming: boolean;
|
|
13
|
-
preview: import("../../models.js").RootKeyPreview | null | undefined;
|
|
14
|
-
locked: boolean;
|
|
15
|
-
canGenerate: boolean;
|
|
16
|
-
canConfirm: boolean;
|
|
17
3
|
onToken({ value }: {
|
|
18
4
|
value: string;
|
|
19
5
|
}): void;
|
|
@@ -25,5 +11,19 @@ export declare function useRootKeyTab({ permissions, params }: TabViewProps, _op
|
|
|
25
11
|
previewFresh(): void;
|
|
26
12
|
cancelFresh(): void;
|
|
27
13
|
confirmFresh(): Promise<void>;
|
|
14
|
+
locked: boolean;
|
|
15
|
+
canGenerate: boolean;
|
|
16
|
+
canConfirm: boolean;
|
|
17
|
+
token: string;
|
|
18
|
+
confirm: string;
|
|
19
|
+
confirming: boolean;
|
|
20
|
+
preview: import("../../models.js").RootKeyPreview | null | undefined;
|
|
21
|
+
scopeNotice: string;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
saving: boolean;
|
|
24
|
+
status: import("../../models.js").RootKeyStatus | null | undefined;
|
|
25
|
+
error: string | null | undefined;
|
|
26
|
+
result: string | null | undefined;
|
|
27
|
+
denied: boolean;
|
|
28
28
|
};
|
|
29
29
|
//# sourceMappingURL=RootKeyTab.hooks.d.ts.map
|
|
@@ -2,14 +2,23 @@ import { useController } from '../../../core/react/use-controller.js';
|
|
|
2
2
|
import { createRootKeyController } from '../../controllers/root-key.controller.js';
|
|
3
3
|
import { ROOT_KEY_PERMISSION, START_FRESH_CONFIRMATION, isRootKeyLocked } from '../../rules.js';
|
|
4
4
|
import { unavailableRootKey, useSecurityPorts } from './SecurityPorts.hooks.js';
|
|
5
|
+
/** Before the controller exists the tab reads as loading with nothing known yet. */
|
|
6
|
+
function rootKeyProgress({ snapshot: s }, _optional = {}) {
|
|
7
|
+
return { loading: s?.loading ?? true, saving: s?.saving ?? false, status: s?.status, error: s?.error, result: s?.result };
|
|
8
|
+
}
|
|
9
|
+
function rootKeyInputs({ snapshot: s }, _optional = {}) {
|
|
10
|
+
return { token: s?.token ?? '', confirm: s?.confirm ?? '', confirming: s?.confirming ?? false, preview: s?.preview };
|
|
11
|
+
}
|
|
12
|
+
function rootKeyFlags({ snapshot: s }, _optional = {}) {
|
|
13
|
+
return { locked: isRootKeyLocked({ status: s?.status ?? null }), canGenerate: s?.status?.state === 'missing' && !s.status.active, canConfirm: !!s?.preview && s.confirm === START_FRESH_CONFIRMATION };
|
|
14
|
+
}
|
|
5
15
|
export function useRootKeyTab({ permissions = [], params }, _optional = {}) {
|
|
6
16
|
const { rootKey } = useSecurityPorts({});
|
|
7
17
|
const key = permissions.join('\0');
|
|
8
18
|
const { controller, snapshot: s } = useController({ create: () => createRootKeyController({ api: rootKey ?? unavailableRootKey, permissions }), dependencies: [rootKey, key] }, { start: ({ controller }) => { void controller.load({}); } });
|
|
9
|
-
return { denied: !rootKey || !permissions.includes(ROOT_KEY_PERMISSION),
|
|
19
|
+
return { denied: !rootKey || !permissions.includes(ROOT_KEY_PERMISSION), ...rootKeyProgress({ snapshot: s }),
|
|
10
20
|
scopeNotice: typeof params.scopeNotice === 'string' ? params.scopeNotice : 'This key protects saved credentials. Keep its backup separate from the database.',
|
|
11
|
-
|
|
12
|
-
locked: isRootKeyLocked({ status: s?.status ?? null }), canGenerate: s?.status?.state === 'missing' && !s.status.active, canConfirm: !!s?.preview && s.confirm === START_FRESH_CONFIRMATION,
|
|
21
|
+
...rootKeyInputs({ snapshot: s }), ...rootKeyFlags({ snapshot: s }),
|
|
13
22
|
onToken({ value }) { controller?.setToken({ value }); }, onConfirmText({ value }) { controller?.setConfirm({ value }); },
|
|
14
23
|
generate() { void controller?.generate({}); }, unlock() { void controller?.importToken({}); }, previewFresh() { void controller?.preview({}); },
|
|
15
24
|
cancelFresh() { controller?.cancel({}); }, async confirmFresh() { if (!await controller?.startFresh({}))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
|
-
export declare function RootKeyTab(props: TabViewProps
|
|
2
|
+
export declare function RootKeyTab(props: TabViewProps): import("react").JSX.Element;
|
|
3
3
|
export default RootKeyTab;
|
|
4
4
|
//# sourceMappingURL=RootKeyTab.d.ts.map
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button, TextField, Notice, ConfirmDialog, Spinner } from '@jini-ai/ui-kit/react';
|
|
3
3
|
import { useRootKeyTab } from '../hooks/RootKeyTab.hooks.js';
|
|
4
|
-
|
|
4
|
+
function RootKeyMessages({ vm }) {
|
|
5
|
+
return _jsxs(_Fragment, { children: [vm.loading && _jsx(Spinner, { label: "Loading root key status\u2026" }), vm.error && _jsx(Notice, { tone: "danger", children: vm.error }), vm.result && _jsx(Notice, { tone: "success", children: vm.result })] });
|
|
6
|
+
}
|
|
7
|
+
function LockedActions({ vm }) {
|
|
8
|
+
return _jsxs(_Fragment, { children: [_jsx(TextField, { label: "Old root key", type: "password", value: vm.token, onValueChange: vm.onToken, disabled: vm.saving, attrs: { 'data-agent-private': 'true' } }), _jsx(Button, { onPress: vm.unlock, pending: vm.saving, children: "Unlock saved credentials" }), _jsx(Button, { onPress: vm.previewFresh, disabled: vm.saving || vm.loading, children: "Start fresh" })] });
|
|
9
|
+
}
|
|
10
|
+
function StartFreshBody({ vm }) {
|
|
11
|
+
return _jsxs(_Fragment, { children: [_jsx("p", { children: vm.preview?.detail }), _jsxs("p", { children: [vm.preview?.removes, " credentials will be removed."] }), vm.preview?.affectedWebhooks.map(w => _jsxs("p", { children: [w.label, " \u00B7 ", w.targetUrl] }, `${w.label}:${w.targetUrl}`)), _jsx(TextField, { label: "Type START FRESH", value: vm.confirm, onValueChange: vm.onConfirmText, disabled: vm.saving })] });
|
|
12
|
+
}
|
|
13
|
+
export function RootKeyTab(props) {
|
|
5
14
|
const vm = useRootKeyTab(props);
|
|
6
|
-
return vm.denied ? _jsx(Notice, { tone: "danger", children: "Permission denied" }) : _jsxs("section", { "aria-label": "Root key", children: [_jsx(Notice, { children: vm.scopeNotice }),
|
|
15
|
+
return vm.denied ? _jsx(Notice, { tone: "danger", children: "Permission denied" }) : _jsxs("section", { "aria-label": "Root key", children: [_jsx(Notice, { children: vm.scopeNotice }), _jsx(RootKeyMessages, { vm: vm }), vm.status && _jsxs("p", { children: [vm.status.state, " \u00B7 ", vm.status.fingerprint, " \u00B7 ", vm.status.source] }), vm.canGenerate && _jsx(Button, { onPress: vm.generate, pending: vm.saving, children: "Create root key" }), vm.locked && _jsx(LockedActions, { vm: vm }), vm.confirming && _jsx(ConfirmDialog, { open: true, title: "Start fresh", body: _jsx(StartFreshBody, { vm: vm }), consequence: "Saved credentials will be removed. Update affected webhook receivers with new signing secrets.", confirmLabel: "Start fresh", onConfirm: vm.confirmFresh, onCancel: vm.cancelFresh, pending: vm.saving, agentMayConfirm: false, tone: "danger" })] });
|
|
7
16
|
}
|
|
8
17
|
export default RootKeyTab;
|
|
9
18
|
//# sourceMappingURL=RootKeyTab.js.map
|
package/dist/security/rules.js
CHANGED
|
@@ -9,8 +9,9 @@ export const OTHER_STORES = ['site-assistant', 'admin-byok', 'media-provider', '
|
|
|
9
9
|
export function writePermission({ kind }, _optional = {}) { return kind === 'custom' ? 'custom-credentials.write' : kind === 'publish' ? 'system.publish' : 'source-control.credentials.write'; }
|
|
10
10
|
export function otherWritePermission({ store }, _optional = {}) { return store === 'site-assistant' || store === 'admin-byok' ? 'admin.assistant.manage' : 'admin.integrations.manage'; }
|
|
11
11
|
export function credentialStore({ api, kind }, _optional = {}) { return kind === 'source-control' ? api.sourceControl : kind === 'publish' ? api.publish : api.custom; }
|
|
12
|
+
function defaultCategory({ kind }, _optional = {}) { return kind === 'publish' ? 'hosting' : kind === 'source-control' ? 'source-control' : 'general'; }
|
|
12
13
|
export function blankCredentialDraft({ kind, providerId = '' }, _optional = {}) {
|
|
13
|
-
return { kind, providerId, label: '', token: '', values: {}, category:
|
|
14
|
+
return { kind, providerId, label: '', token: '', values: {}, category: defaultCategory({ kind }), baseUrl: '', additionalHosts: '', username: '' };
|
|
14
15
|
}
|
|
15
16
|
/** Legacy sentinel labels are display-only: opening this page must never mutate saved data. */
|
|
16
17
|
export function credentialLabel({ row, providers }, _optional = {}) {
|
|
@@ -18,46 +19,65 @@ export function credentialLabel({ row, providers }, _optional = {}) {
|
|
|
18
19
|
return row.label;
|
|
19
20
|
return `${providers.find(p => p.kind === row.kind && p.id === row.providerId)?.label ?? row.providerId} token`;
|
|
20
21
|
}
|
|
21
|
-
export function credentialCategory({ row }, _optional = {}) { return row.category ?? (
|
|
22
|
+
export function credentialCategory({ row }, _optional = {}) { return row.category ?? defaultCategory({ kind: row.kind }); }
|
|
22
23
|
export function filterCredentials({ rows, query, category, providers }, _optional = {}) {
|
|
23
24
|
const q = query.trim().toLowerCase();
|
|
24
25
|
return rows.filter(row => (category === 'all' || credentialCategory({ row }) === category) && [credentialLabel({ row, providers }), row.providerId, row.baseUrl ?? '', row.accountLabel ?? ''].join(' ').toLowerCase().includes(q));
|
|
25
26
|
}
|
|
26
27
|
export function filterOtherCredentials({ rows, query, category }, _optional = {}) { return rows.filter(row => (category === 'all' || row.category === category) && `${row.label} ${row.store}`.toLowerCase().includes(query.trim().toLowerCase())); }
|
|
28
|
+
function findProvider({ draft, providers }, _optional = {}) { return providers.find(p => p.kind === draft.kind && p.id === draft.providerId); }
|
|
29
|
+
function isDuplicateLabel({ draft, rows }, _optional = {}) {
|
|
30
|
+
const label = draft.label.trim().toLowerCase();
|
|
31
|
+
return rows.some(r => r.kind === draft.kind && r.id !== draft.id && (draft.kind === 'custom' || r.providerId === draft.providerId) && r.label.trim().toLowerCase() === label);
|
|
32
|
+
}
|
|
33
|
+
function customDraftError({ draft }, _optional = {}) {
|
|
34
|
+
try {
|
|
35
|
+
if (!['http:', 'https:'].includes(new URL(draft.baseUrl).protocol))
|
|
36
|
+
return 'Enter an HTTP or HTTPS URL';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 'Enter an HTTP or HTTPS URL';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function providerDraftError({ draft, providers }, _optional = {}) {
|
|
44
|
+
if (draft.id && !draft.token.trim())
|
|
45
|
+
return null;
|
|
46
|
+
const provider = findProvider({ draft, providers });
|
|
47
|
+
if (!provider)
|
|
48
|
+
return 'Provider is unavailable';
|
|
49
|
+
if (provider.fields.some(f => f.name !== provider.tokenField && f.required && !draft.values[f.name]?.trim()))
|
|
50
|
+
return 'Complete all required fields';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
27
53
|
/** A custom row is its own provider identity; its username can change without retyping its token. */
|
|
28
54
|
export function validateCredentialDraft({ draft, rows, providers }, _optional = {}) {
|
|
29
55
|
if (!draft.label.trim())
|
|
30
56
|
return 'Enter a name';
|
|
31
|
-
if (
|
|
57
|
+
if (isDuplicateLabel({ draft, rows }))
|
|
32
58
|
return 'That name is already in use';
|
|
33
59
|
if (!draft.id && !draft.token.trim())
|
|
34
60
|
return 'Enter an access token';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (provider.fields.some(f => f.name !== provider.tokenField && f.required && !draft.values[f.name]?.trim()))
|
|
49
|
-
return 'Complete all required fields';
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
61
|
+
return draft.kind === 'custom' ? customDraftError({ draft }) : providerDraftError({ draft, providers });
|
|
62
|
+
}
|
|
63
|
+
function customConnection({ draft, token }, _optional = {}) {
|
|
64
|
+
const username = draft.username.trim();
|
|
65
|
+
return { token, ...(username ? { username } : {}) };
|
|
66
|
+
}
|
|
67
|
+
function providerConnection({ draft, token, providers }, _optional = {}) {
|
|
68
|
+
const provider = findProvider({ draft, providers });
|
|
69
|
+
const extras = provider ? provider.fields.filter(f => f.name !== provider.tokenField && draft.values[f.name]?.trim()).map(f => [f.name, draft.values[f.name].trim()]) : [];
|
|
70
|
+
return { providerId: draft.providerId, [provider?.tokenField ?? 'token']: token, ...Object.fromEntries(extras) };
|
|
71
|
+
}
|
|
72
|
+
function customPatchFields({ draft }, _optional = {}) {
|
|
73
|
+
return { category: draft.category, baseUrl: draft.baseUrl.trim(), additionalHosts: draft.additionalHosts.split(/[\n,]/).map(s => s.trim()).filter(Boolean), username: draft.username.trim() || null };
|
|
52
74
|
}
|
|
53
75
|
/** Omitting connection preserves the saved secret. Never send an empty or half-blank replacement. */
|
|
54
76
|
export function buildCredentialPatch({ draft, providers }, _optional = {}) {
|
|
55
|
-
const provider = providers.find(p => p.kind === draft.kind && p.id === draft.providerId);
|
|
56
77
|
const token = draft.token.trim();
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return { label: draft.label.trim(), ...(connection ? { connection } : {}), ...(draft.kind === 'custom' ? { category: draft.category, baseUrl: draft.baseUrl.trim(), additionalHosts: draft.additionalHosts.split(/[\n,]/).map(s => s.trim()).filter(Boolean), username: draft.username.trim() || null } : {}) };
|
|
78
|
+
const custom = draft.kind === 'custom';
|
|
79
|
+
const connection = token ? (custom ? customConnection({ draft, token }) : providerConnection({ draft, token, providers })) : undefined;
|
|
80
|
+
return { label: draft.label.trim(), ...(connection ? { connection } : {}), ...(custom ? customPatchFields({ draft }) : {}) };
|
|
61
81
|
}
|
|
62
82
|
export function isRootKeyLocked({ status }, _optional = {}) { return status?.state === 'missing-with-data' || status?.state === 'mismatch'; }
|
|
63
83
|
//# sourceMappingURL=rules.js.map
|
|
@@ -27,18 +27,20 @@ export function createSeoController({ api }, { t = (key) => key } = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
async function regenerateSitemap(_required = {}, _optional = {}) {
|
|
29
29
|
store.set({ patch: { saving: true, error: null, notice: null } });
|
|
30
|
+
// One exit after `finally`: returning from both try and catch left a fall-through nothing took.
|
|
31
|
+
let accepted = false;
|
|
30
32
|
try {
|
|
31
33
|
await api.regenerateSeoSitemap({});
|
|
32
34
|
store.set({ patch: { notice: t('Sitemap regeneration accepted.') } });
|
|
33
|
-
|
|
35
|
+
accepted = true;
|
|
34
36
|
}
|
|
35
37
|
catch (error) {
|
|
36
38
|
store.set({ patch: { error: describeSeoError({ error, fallback: t('failed to regenerate sitemap') }) } });
|
|
37
|
-
return false;
|
|
38
39
|
}
|
|
39
40
|
finally {
|
|
40
41
|
store.set({ patch: { saving: false } });
|
|
41
42
|
}
|
|
43
|
+
return accepted;
|
|
42
44
|
}
|
|
43
45
|
return {
|
|
44
46
|
...store, load, save, regenerateSitemap,
|