@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.
Files changed (151) hide show
  1. package/CHANGELOG.md +70 -2
  2. package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
  3. package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
  4. package/dist/agent-plugins/package-file-tree.js +54 -51
  5. package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
  6. package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
  7. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
  8. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
  9. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
  10. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
  11. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
  12. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
  13. package/dist/browser/navigation.js +44 -27
  14. package/dist/comments/adapters/memory.js +30 -12
  15. package/dist/comments/conformance/comments-api.conformance.js +55 -33
  16. package/dist/comments/controllers/comment-queue.controller.js +13 -4
  17. package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
  18. package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
  19. package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
  20. package/dist/comments/react/index.d.ts +2 -1
  21. package/dist/comments/react/index.js +1 -1
  22. package/dist/consent/react/ConsentFields.d.ts +35 -0
  23. package/dist/consent/react/ConsentFields.js +76 -0
  24. package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
  25. package/dist/consent/react/ConsentPreviewFrame.js +14 -0
  26. package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
  27. package/dist/consent/react/ConsentSettingsPanel.js +6 -17
  28. package/dist/consent/react/index.d.ts +6 -1
  29. package/dist/consent/react/index.js +2 -0
  30. package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
  31. package/dist/consent/react/settings-panel.hooks.js +26 -0
  32. package/dist/core/entities/rules.js +126 -81
  33. package/dist/core/manifest/rules.js +10 -10
  34. package/dist/core/module/create-admin.js +195 -117
  35. package/dist/core/ports/redirects.d.ts +8 -6
  36. package/dist/core/ports/redirects.js +8 -6
  37. package/dist/core/routing/rules.js +9 -4
  38. package/dist/core/transport/http.js +37 -20
  39. package/dist/forms/adapters/memory.js +22 -11
  40. package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
  41. package/dist/forms/conformance/forms-api.conformance.js +42 -46
  42. package/dist/forms/field-settings.js +150 -115
  43. package/dist/forms/html-rules.js +45 -28
  44. package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
  45. package/dist/forms/react/components/FieldSettingsRows.js +29 -0
  46. package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
  47. package/dist/forms/react/components/FieldSettingsSection.js +5 -6
  48. package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
  49. package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
  50. package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
  51. package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
  52. package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
  53. package/dist/forms/react/components/FormSubmissions.js +4 -20
  54. package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
  55. package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
  56. package/dist/media/adapters/memory.js +31 -26
  57. package/dist/media/controllers/edit-media.controller.js +2 -1
  58. package/dist/media/controllers/library.controller.js +48 -35
  59. package/dist/media/controllers/providers.controller.js +2 -1
  60. package/dist/media/html-attributes.js +29 -15
  61. package/dist/media/index.d.ts +1 -1
  62. package/dist/media/index.js +1 -1
  63. package/dist/media/react/components/MediaIcons.d.ts +0 -3
  64. package/dist/media/react/components/MediaIcons.js +0 -9
  65. package/dist/media/react/components/MediaLightbox.d.ts +2 -1
  66. package/dist/media/react/components/MediaLightbox.js +11 -5
  67. package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
  68. package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
  69. package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
  70. package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
  71. package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
  72. package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
  73. package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
  74. package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
  75. package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
  76. package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
  77. package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
  78. package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
  79. package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
  80. package/dist/media/react/index.js +4 -0
  81. package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
  82. package/dist/media/react/tabs/LibraryTab.js +2 -2
  83. package/dist/media/rules.d.ts +7 -0
  84. package/dist/media/rules.js +25 -2
  85. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
  86. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
  87. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
  88. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
  89. package/dist/react/components/RowMenu/RowMenu.js +16 -10
  90. package/dist/react/components/Sidebar.js +32 -26
  91. package/dist/react/entities/EntityEdit.js +17 -14
  92. package/dist/react/entities/EntityField.d.ts +5 -2
  93. package/dist/react/entities/EntityField.js +38 -23
  94. package/dist/react/entities/EntityList.js +6 -5
  95. package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
  96. package/dist/react/entities/RelationTruncationNotice.js +9 -0
  97. package/dist/react/entities/use-entity-edit.d.ts +4 -2
  98. package/dist/react/entities/use-entity-edit.js +52 -33
  99. package/dist/react/shell/AdminShell.js +40 -23
  100. package/dist/react/shell/model.d.ts +4 -1
  101. package/dist/react/shell/model.js +3 -2
  102. package/dist/react/shell/use-admin-shell-session.js +25 -26
  103. package/dist/redirects/adapters/memory.js +9 -2
  104. package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
  105. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
  106. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
  107. package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
  108. package/dist/security/adapters/http.js +25 -10
  109. package/dist/security/adapters/memory.js +19 -5
  110. package/dist/security/controllers/access-tokens.controller.js +27 -26
  111. package/dist/security/controllers/other-credentials.controller.js +4 -2
  112. package/dist/security/controllers/root-key.controller.js +17 -9
  113. package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
  114. package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
  115. package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
  116. package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
  117. package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
  118. package/dist/security/react/tabs/RootKeyTab.js +12 -3
  119. package/dist/security/rules.js +45 -25
  120. package/dist/seo/controllers/seo.controller.js +4 -2
  121. package/dist/seo/react/components/MediaRefField.d.ts +1 -1
  122. package/dist/seo/react/components/MediaRefField.js +19 -2
  123. package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
  124. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
  125. package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
  126. package/dist/seo/react/pages/SeoPage.d.ts +4 -4
  127. package/dist/seo/react/pages/SeoPage.js +26 -6
  128. package/dist/seo/rules.js +39 -10
  129. package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
  130. package/dist/source-control/controllers/source-control.controller.js +20 -8
  131. package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
  132. package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
  133. package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
  134. package/dist/widgets/controllers/instance-editor.controller.js +33 -24
  135. package/dist/widgets/controllers/library.controller.js +18 -12
  136. package/dist/widgets/controllers/region-editor.controller.js +6 -5
  137. package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
  138. package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
  139. package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
  140. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
  141. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
  142. package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
  143. package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
  144. package/dist/widgets/rules.d.ts +14 -2
  145. package/dist/widgets/rules.js +9 -6
  146. package/package.json +23 -14
  147. package/src/themes/default/admin.css +51 -0
  148. package/src/themes/default/chat.css +21 -0
  149. package/src/themes/default/manifest.json +29 -0
  150. package/dist/media/styles.d.ts +0 -4
  151. 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 project(id, patch, providerId, initial) {
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: patch.label ?? initial?.label ?? '', configured: true, isDefault: kind === 'custom' ? false : patch.isDefault ?? initial?.isDefault ?? !items.some(r => r.kind === kind && r.providerId === providerId), createdAt: initial?.createdAt ?? now, updatedAt: now,
10
- ...(kind === 'custom' ? { category: patch.category ?? initial?.category ?? 'general', baseUrl: patch.baseUrl ?? initial?.baseUrl ?? '', additionalHosts: Object.freeze([...(patch.additionalHosts ?? initial?.additionalHosts ?? [])]), ...((patch.username !== undefined ? patch.username : patch.connection?.username ?? initial?.username) ? { username: (patch.username !== undefined ? patch.username : patch.connection?.username ?? initial?.username) } : {}) } : {}),
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'); check(patch.label ?? old.label, old.providerId, id); return insert(project(id, patch, old.providerId, old)); },
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 ? { ...blankCredentialDraft({ kind: row.kind, providerId: row.providerId }), id: row.id, label: credentialLabel({ row, providers: store.getSnapshot().providers }), category: row.category ?? (row.kind === 'publish' ? 'hosting' : row.kind === 'source-control' ? 'source-control' : 'general'), baseUrl: row.baseUrl ?? '', additionalHosts: row.additionalHosts?.join('\n') ?? '', username: row.username ?? '' } : blankCredentialDraft({ kind: k, ...(providerId ? { providerId } : {}) });
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
- store.set({ patch: { saving: true, error: null } });
61
- try {
62
- const row = draft.id ? await target.update({ id: draft.id, patch }, { signal: store.signal }) : await target.create(patch, { signal: store.signal });
63
- // Clear accepted secrets before any later read can fail. A successful save must never leave a saved secret visible.
64
- store.set({ patch: { draft: null, rows: Object.freeze([...store.getSnapshot().rows.filter(r => !(r.kind === row.kind && r.id === row.id)), row]) } });
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
- const target = credentialStore({ api, kind: row.kind });
83
- if (remove)
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
- return !store.signal.aborted;
90
+ ok = !store.signal.aborted;
90
91
  }
91
92
  catch {
92
- store.set({ patch: { error: 'Unable to update credential' } });
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
- return !store.signal.aborted;
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: response.affectedWebhooks?.length ? `${result} Update receivers: ${response.affectedWebhooks.map(w => `${w.label} (${w.targetUrl})`).join(', ')}` : result } });
40
+ store.set({ patch: { token: '', confirm: '', preview: null, confirming: false, result: resultMessage({ result, response }) } });
34
41
  await load({});
35
- return !store.signal.aborted;
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 (!allowed() || !isRootKeyLocked({ status: store.getSnapshot().status }) || store.getSnapshot().saving || store.getSnapshot().loading)
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 !== generation || !allowed())
67
- return false;
68
- store.set({ patch: { preview, confirming: true } });
69
- return true;
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
- denied: boolean;
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 query = s?.query ?? '', category = s?.category ?? 'all';
16
- const rows = s ? 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() { access.controller?.begin({ row }); }, remove() { setRemoval({ row, type: 'access' }); }, makeDefault() { void access.controller?.makeDefault({ row }); } })) : [];
17
- const otherRows = o ? 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 }) { other.controller?.setDraft({ key: `${row.store}:${row.id}`, token: value }); }, replace() { void other.controller?.replace({ row }); }, remove() { setRemoval({ row, type: 'other' }); } })) : [];
18
- const providers = (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() { access.controller?.begin({ kind: provider.kind, providerId: provider.id }); } }));
19
- // Count remains global across categories; the search count answers how many saved tokens this host holds.
20
- const total = (s?.rows.length ?? 0) + (o?.rows.length ?? 0);
21
- 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);
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), loading: s?.loading ?? true, saving: s?.saving ?? false, status: s?.status, error: s?.error, result: s?.result,
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
- token: s?.token ?? '', confirm: s?.confirm ?? '', confirming: s?.confirming ?? false, preview: s?.preview,
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, _optional?: {}): import("react").JSX.Element;
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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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
- export function RootKeyTab(props, _optional = {}) {
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 }), 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 }), 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 && _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" })] }), vm.confirming && _jsx(ConfirmDialog, { open: true, title: "Start fresh", body: _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 })] }), 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" })] });
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
@@ -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: kind === 'publish' ? 'hosting' : kind === 'source-control' ? 'source-control' : 'general', baseUrl: '', additionalHosts: '', username: '' };
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 ?? (row.kind === 'publish' ? 'hosting' : row.kind === 'source-control' ? 'source-control' : 'general'); }
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 (rows.some(r => r.kind === draft.kind && r.id !== draft.id && (draft.kind === 'custom' || r.providerId === draft.providerId) && r.label.trim().toLowerCase() === draft.label.trim().toLowerCase()))
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
- if (draft.kind === 'custom') {
36
- try {
37
- if (!['http:', 'https:'].includes(new URL(draft.baseUrl).protocol))
38
- return 'Enter an HTTP or HTTPS URL';
39
- }
40
- catch {
41
- return 'Enter an HTTP or HTTPS URL';
42
- }
43
- }
44
- else if (!draft.id || draft.token.trim()) {
45
- const provider = providers.find(p => p.kind === draft.kind && p.id === draft.providerId);
46
- if (!provider)
47
- return 'Provider is unavailable';
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 connection = token ? draft.kind === 'custom'
58
- ? { token, ...(draft.username.trim() ? { username: draft.username.trim() } : {}) }
59
- : { providerId: draft.providerId, [provider?.tokenField ?? 'token']: token, ...Object.fromEntries((provider?.fields ?? []).filter(f => f.name !== provider?.tokenField && draft.values[f.name]?.trim()).map(f => [f.name, draft.values[f.name].trim()])) } : undefined;
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
- return true;
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,