@lamalibre/create-lamaste 4.3.1 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +2 -2
  2. package/src/lib/service-config.js +6 -0
  3. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  4. package/vendor/docs/00-introduction/quickstart.md +5 -5
  5. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  6. package/vendor/docs/01-concepts/certificates.md +3 -3
  7. package/vendor/docs/01-concepts/mtls.md +2 -2
  8. package/vendor/docs/01-concepts/security-model.md +38 -1
  9. package/vendor/docs/01-concepts/tickets.md +2 -2
  10. package/vendor/docs/01-concepts/tunneling.md +4 -4
  11. package/vendor/docs/02-guides/agent-setup.md +6 -6
  12. package/vendor/docs/02-guides/certificate-management.md +2 -2
  13. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  14. package/vendor/docs/02-guides/desktop-app-setup.md +11 -11
  15. package/vendor/docs/02-guides/installation.md +3 -3
  16. package/vendor/docs/02-guides/onboarding.md +2 -2
  17. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  18. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  19. package/vendor/docs/03-architecture/installer.md +4 -4
  20. package/vendor/docs/03-architecture/management-flow.md +1 -1
  21. package/vendor/docs/03-architecture/overview.md +5 -5
  22. package/vendor/docs/03-architecture/panel-client.md +7 -7
  23. package/vendor/docs/03-architecture/panel-server.md +2 -2
  24. package/vendor/docs/03-architecture/state-management.md +2 -2
  25. package/vendor/docs/03-architecture/system-overview.md +1 -1
  26. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +394 -34
  27. package/vendor/docs/04-api-reference/app-host.md +87 -38
  28. package/vendor/docs/04-api-reference/audit-and-notifications.md +10 -10
  29. package/vendor/docs/04-api-reference/certificates.md +22 -19
  30. package/vendor/docs/04-api-reference/lifecycle-hooks.md +34 -12
  31. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  32. package/vendor/docs/04-api-reference/overview.md +137 -135
  33. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -1
  34. package/vendor/docs/04-api-reference/tunnels.md +14 -10
  35. package/vendor/docs/05-operations/backup-and-restore.md +2 -1
  36. package/vendor/docs/05-operations/uninstalling.md +1 -1
  37. package/vendor/docs/05-operations/upgrades.md +6 -6
  38. package/vendor/docs/06-reference/config-files.md +90 -85
  39. package/vendor/docs/06-reference/glossary.md +3 -3
  40. package/vendor/docs/06-reference/installer-flags.md +13 -13
  41. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  42. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  43. package/vendor/docs/gatekeeper-design.md +8 -8
  44. package/vendor/docs/identity-directory-design.md +2 -2
  45. package/vendor/docs/package.json +1 -1
  46. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  47. package/vendor/gatekeeper/package.json +2 -2
  48. package/vendor/server/bin/lamaste-server.js +4 -1
  49. package/vendor/server/package.json +4 -3
  50. package/vendor/server/src/commands/plugin-settings.js +541 -0
  51. package/vendor/server/src/commands/plugins.js +94 -27
  52. package/vendor/server/src/panel-api.js +36 -4
  53. package/vendor/server/src/plugin-approval.js +78 -0
  54. package/vendor/server/src/plugin-settings.js +699 -0
  55. package/vendor/server-ui/dist/assets/index-25wo_8av.css +1 -0
  56. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +84 -0
  57. package/vendor/server-ui/dist/index.html +2 -2
  58. package/vendor/serverd/package.json +2 -2
  59. package/vendor/serverd/src/index.js +9 -0
  60. package/vendor/serverd/src/lib/agent-presence.js +106 -0
  61. package/vendor/serverd/src/lib/hook-pipelines.js +20 -4
  62. package/vendor/serverd/src/lib/migrations/state/0012_plugin_settings_and_presence.sql +62 -0
  63. package/vendor/serverd/src/lib/mtls.js +44 -4
  64. package/vendor/serverd/src/lib/plugin-hooks.js +120 -20
  65. package/vendor/serverd/src/lib/plugin-hosting.js +35 -14
  66. package/vendor/serverd/src/lib/plugin-lifecycle.js +267 -83
  67. package/vendor/serverd/src/lib/plugin-packages.js +30 -22
  68. package/vendor/serverd/src/lib/plugin-settings.js +1101 -0
  69. package/vendor/serverd/src/lib/plugins.js +108 -33
  70. package/vendor/serverd/src/lib/registry-credential.js +37 -0
  71. package/vendor/serverd/src/lib/tunnel-hooks.js +11 -0
  72. package/vendor/serverd/src/routes/management/plugins.js +166 -19
  73. package/vendor/serverd/src/routes/management/tunnels.js +21 -1
  74. package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +0 -84
  75. package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +0 -1
@@ -27,8 +27,9 @@
27
27
  * hooks <name> The plugin's lifecycle hooks and what needs approval: the relay
28
28
  * hook set (stages, routes, fail-open hooks, effects), the agent
29
29
  * half Lamaste installs, the approval digest; agent hooks
30
- * approve-hooks <name> --digest <sha256>
31
- * Approve with the approval digest `hooks` shows (or a pending update's)
30
+ * approve-hooks <name> --digest <sha256> [--version <v>]
31
+ * Approve with the approval digest `hooks` shows; a pending update with
32
+ * its version too (`--version` alone when it has no digest)
32
33
  * auto-approve <name> on|off
33
34
  * Approve what this plugin's later versions bring automatically (audited)
34
35
  * update <name> [--version <v>]
@@ -45,6 +46,12 @@
45
46
  * followed to its end
46
47
  * forget-data <name> --agent <label>
47
48
  * Drop only the relay's record of it; the data stays on the agent
49
+ * settings <name> An agent-hosted plugin's settings: each field's value (a secret
50
+ * only as set or not), whether it still needs configuration,
51
+ * where the values come from, the hosting
52
+ * configure <name> [--set key=value]... [--clear key]... [--secrets-stdin]
53
+ * Change them (kept by its agent half); in a terminal without
54
+ * flags, field by field — a secret never on the command line
48
55
  *
49
56
  * Every subcommand goes through the running panel (`/api/plugins/*`, admin
50
57
  * certificate): the panel owns the plugin registry (SQLite `state.db`), runs
@@ -61,6 +68,14 @@ import { AGENT_LABEL_REGEX } from '@lamalibre/lamaste';
61
68
  import { PANEL_SERVICE } from '../config.js';
62
69
  import { panelRequest } from '../panel-api.js';
63
70
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
71
+ import { configureCmd, settingsCmd } from './plugin-settings.js';
72
+ import { settingsDiffLines, terminalText } from '../plugin-settings.js';
73
+ import {
74
+ APPROVE_HOOKS_USAGE,
75
+ approveHooksCommand,
76
+ parseApproveArgs,
77
+ pendingUpdateReasons,
78
+ } from '../plugin-approval.js';
64
79
 
65
80
  /** npm runs inside the panel for install and uninstall (its own limit is 120 s). */
66
81
  const NPM_CALL_TIMEOUT_MS = 180_000;
@@ -115,6 +130,10 @@ export async function runPlugins(args, { json }) {
115
130
  return purgeDataCmd(args.slice(1), { json });
116
131
  case 'forget-data':
117
132
  return forgetDataCmd(args.slice(1), { json });
133
+ case 'settings':
134
+ return settingsCmd(args.slice(1), { json });
135
+ case 'configure':
136
+ return configureCmd(args.slice(1), { json });
118
137
  default:
119
138
  printPluginUsage();
120
139
  process.exit(sub ? 1 : 0);
@@ -148,8 +167,9 @@ ${b('Subcommands:')}
148
167
  ${c('rotate-hosting-secret <name>')}
149
168
  New installation secret (the agent fetches it on its next sync)
150
169
  ${c('hooks <name>')} Lifecycle hooks and what needs approval: relay hooks, agent half
151
- ${c('approve-hooks <name> --digest <sha256>')}
152
- Approve with the approval digest "hooks" showed (or a pending update's)
170
+ ${c('approve-hooks <name> --digest <sha256> [--version <v>]')}
171
+ Approve with the approval digest "hooks" showed; a pending update
172
+ with its version too (--version alone when it has no digest)
153
173
  ${c('auto-approve <name> on|off')}
154
174
  Approve what this plugin's later versions bring automatically
155
175
  ${c('update <name> [--version <v>]')}
@@ -164,6 +184,13 @@ ${b('Subcommands:')}
164
184
  Delete it on the agent — its data directory and account (a push)
165
185
  ${c('forget-data <name> --agent <label>')}
166
186
  Drop only the relay's record; the data stays on the agent
187
+ ${c('settings <name>')} Settings of an agent-hosted plugin: values (secrets only as set or
188
+ not), what is missing, where they come from, the hosting
189
+ ${c('configure <name> [--set key=value]... [--clear key]... [--secrets-stdin]')}
190
+ Change them: --set parses by the field's type (never a secret),
191
+ --clear puts a field back to its default or clears a secret,
192
+ --secrets-stdin reads {"key": "value" | null} from stdin;
193
+ in a terminal without flags, asked field by field
167
194
  `);
168
195
  }
169
196
 
@@ -732,11 +759,14 @@ function printAgentHalf(agentHalf, title) {
732
759
 
733
760
  /**
734
761
  * Print what a version brings that needs approval — the approval digest
735
- * (the one value `approve-hooks` takes), its relay hook set and the agent
736
- * half Lamaste installs.
762
+ * (what `approve-hooks` takes; none for an update that brings neither
763
+ * relay hooks nor an agent half Lamaste installs), its relay hook set and
764
+ * the agent half Lamaste installs.
737
765
  */
738
766
  function printApprovalContents({ digest, relay, agentHalf }) {
739
- console.log(` approval digest: ${chalk.cyan(digest)}`);
767
+ console.log(
768
+ ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs')}`,
769
+ );
740
770
  if (relay) {
741
771
  console.log('');
742
772
  printRelayHookSet(relay, 'Relay hooks — run in a sandbox on the relay');
@@ -747,9 +777,43 @@ function printApprovalContents({ digest, relay, agentHalf }) {
747
777
  }
748
778
  }
749
779
 
750
- /** The command that approves `digest` for plugin `name`. */
751
- function approveCommand(name, digest) {
752
- return chalk.cyan(`lamaste-server plugins approve-hooks ${name} --digest ${digest}`);
780
+ /**
781
+ * Print an update that waits for approval under `title`: why it waits
782
+ * (`approvalRequired` — a secret turned plain in red), what it brings
783
+ * (its digest, relay hook set and agent half), how it changes the settings
784
+ * schema (outside the digest) — fields added and removed with their types,
785
+ * retyped ones, newly required ones, and a warning for secrets that become
786
+ * plain values — what auto-approve could not cover beyond the reasons, and
787
+ * the command that approves it.
788
+ */
789
+ function printPendingUpdate(name, update, title) {
790
+ console.log('');
791
+ console.log(chalk.bold(` ${title}`));
792
+ const { required, notCovered } = pendingUpdateReasons(update);
793
+ for (const line of required) {
794
+ console.log(line.warning ? chalk.red(` - ${line.text}`) : ` - ${line.text}`);
795
+ }
796
+ console.log('');
797
+ printApprovalContents(update);
798
+ const lines = settingsDiffLines(update.settings);
799
+ if (lines.length > 0) {
800
+ console.log('');
801
+ console.log(chalk.bold(' Settings — what this version changes (not part of the digest)'));
802
+ for (const line of lines) {
803
+ console.log(` ${line.warning ? chalk.red(line.text) : line.text}`);
804
+ }
805
+ }
806
+ if (notCovered.length > 0) {
807
+ console.log('');
808
+ console.log(chalk.bold(' Auto-approve does not cover it'));
809
+ for (const reason of notCovered) console.log(chalk.yellow(` ${reason}`));
810
+ }
811
+ console.log(`\n Approve and update with: ${approveCommand(name, update)}`);
812
+ }
813
+
814
+ /** The command that approves the installed version (`{ digest }`) or a pending update (`{ digest, version }`). */
815
+ function approveCommand(name, target) {
816
+ return chalk.cyan(approveHooksCommand(name, target));
753
817
  }
754
818
 
755
819
  /**
@@ -794,7 +858,7 @@ async function hooksCmd(name, { json }) {
794
858
  `\n auto-approve for later versions: ${hooks.autoApprove ? chalk.yellow('on') : 'off'}`,
795
859
  );
796
860
  if (approval.state !== 'approved') {
797
- console.log(`\n Approve with: ${approveCommand(name, approval.digest)}`);
861
+ console.log(`\n Approve with: ${approveCommand(name, { digest: approval.digest })}`);
798
862
  }
799
863
  }
800
864
  if (hooks.agent) {
@@ -805,10 +869,11 @@ async function hooksCmd(name, { json }) {
805
869
  }
806
870
  if (hooks.pendingUpdate) {
807
871
  const update = hooks.pendingUpdate;
808
- console.log('');
809
- console.log(chalk.bold(` Update to ${update.version} — waits for approval`));
810
- printApprovalContents(update);
811
- console.log(`\n Approve and update with: ${approveCommand(name, update.digest)}`);
872
+ printPendingUpdate(
873
+ name,
874
+ update,
875
+ `Update to ${terminalText(update.version)} — waits for your approval:`,
876
+ );
812
877
  }
813
878
  console.log('');
814
879
  }
@@ -819,18 +884,16 @@ async function hooksCmd(name, { json }) {
819
884
  * @param {{ json: boolean }} options
820
885
  */
821
886
  async function approveHooksCmd(name, rest, { json }) {
822
- const idx = rest.indexOf('--digest');
823
- const digest = idx !== -1 ? rest[idx + 1] : undefined;
824
- if (!name || !digest || !/^[a-f0-9]{64}$/.test(digest)) {
825
- fail(
826
- 'Usage: lamaste-server plugins approve-hooks <name> --digest <sha256> (the approval digest `hooks <name>` shows)',
827
- json,
828
- );
829
- }
887
+ if (!name) fail(APPROVE_HOOKS_USAGE, json);
888
+ const args = parseApproveArgs(rest);
889
+ if (args.error) fail(args.error, json);
890
+ const { digest, version } = args;
830
891
  let result;
831
892
  try {
893
+ // A version names the pending update; its digest, when it has one, goes with it.
832
894
  result = await panelRequest('POST', `/api/plugins/${encodeURIComponent(name)}/hooks/approve`, {
833
895
  digest,
896
+ ...(version ? { version } : {}),
834
897
  });
835
898
  } catch (err) {
836
899
  // Approving a pending update starts it: its agent half is a push.
@@ -899,10 +962,12 @@ async function updateCmd(name, rest, { json }) {
899
962
  return;
900
963
  }
901
964
  const update = result.pendingUpdate;
965
+ printPendingUpdate(
966
+ name,
967
+ update,
968
+ `${name} ${terminalText(update.version)} waits for your approval:`,
969
+ );
902
970
  console.log('');
903
- console.log(chalk.bold(` ${name} ${update.version} waits for approval`));
904
- printApprovalContents(update);
905
- console.log(`\n Approve and update with: ${approveCommand(name, update.digest)}\n`);
906
971
  return;
907
972
  }
908
973
  if (!json) console.log(`\n Updating ${chalk.cyan(name)}`);
@@ -1111,7 +1176,9 @@ async function installPluginCmd(packageName, { json }) {
1111
1176
  agentHalf: result.hooks.agentHalf,
1112
1177
  });
1113
1178
  if (approval.state === 'pending') {
1114
- console.log(`\n Approve with: ${approveCommand(plugin.name, approval.digest)}`);
1179
+ console.log(
1180
+ `\n Approve with: ${approveCommand(plugin.name, { digest: approval.digest })}`,
1181
+ );
1115
1182
  }
1116
1183
  console.log('');
1117
1184
  }
@@ -22,6 +22,7 @@ import { mkdtemp, open, readFile, rm } from 'node:fs/promises';
22
22
  import { tmpdir } from 'node:os';
23
23
  import { join } from 'node:path';
24
24
  import { execa } from 'execa';
25
+ import { SETTINGS_KEY_RE } from '@lamalibre/lamaste';
25
26
  import { PKI_DIR, readConfig } from './config.js';
26
27
  import { PANEL_LOOPBACK, assertPanelListener, readPanelIdentity } from './panel-identity.js';
27
28
 
@@ -244,12 +245,31 @@ function detailsText(details) {
244
245
  return [...new Set(found)].join('; ');
245
246
  }
246
247
 
248
+ /**
249
+ * The per-field messages of a refused settings change (`errors`: a setting
250
+ * key, or `_` for the change as a whole, to a message), or null when the
251
+ * panel sent none. Only string messages under a key a setting can have are
252
+ * kept — the panel's and the agent half's messages never contain a value.
253
+ *
254
+ * @param {unknown} errors
255
+ * @returns {Record<string, string> | null}
256
+ */
257
+ function settingsErrors(errors) {
258
+ if (errors === null || typeof errors !== 'object' || Array.isArray(errors)) return null;
259
+ const kept = Object.entries(errors).filter(
260
+ ([key, message]) => (key === '_' || SETTINGS_KEY_RE.test(key)) && typeof message === 'string',
261
+ );
262
+ return kept.length > 0 ? Object.fromEntries(kept) : null;
263
+ }
264
+
247
265
  /**
248
266
  * Split curl's output (`<payload>\n<status>`) and turn a non-2xx status
249
267
  * into an error carrying the panel's `error` (and `details`) in its message,
250
268
  * the status as `statusCode`, and what a caller acts on beyond the message
251
- * when the panel sent it: `code` (e.g. `agent_not_eligible`), `reasons` and
252
- * a running operation's `operationId`. Pure, exported for tests.
269
+ * when the panel sent it: `code` (e.g. `agent_not_eligible`), `reasons`, a
270
+ * running operation's `operationId`, and for a refused settings change its
271
+ * per-field `errors`, the current `revision` (`revision_mismatch`) and the
272
+ * `hosting` (`hosting_offline`). Pure, exported for tests.
253
273
  *
254
274
  * @param {string} stdout
255
275
  * @param {string} method
@@ -282,6 +302,17 @@ export function parsePanelResponse(stdout, method, path) {
282
302
  err.reasons = parsed.reasons.filter((r) => typeof r === 'string');
283
303
  }
284
304
  if (typeof parsed.operationId === 'string') err.operationId = parsed.operationId;
305
+ const errors = settingsErrors(parsed.errors);
306
+ if (errors) err.errors = errors;
307
+ if (Number.isSafeInteger(parsed.revision) && parsed.revision >= 0) {
308
+ err.revision = parsed.revision;
309
+ }
310
+ if (
311
+ parsed.hosting === null ||
312
+ (typeof parsed.hosting === 'object' && !Array.isArray(parsed.hosting))
313
+ ) {
314
+ err.hosting = parsed.hosting;
315
+ }
285
316
  throw err;
286
317
  }
287
318
 
@@ -337,8 +368,9 @@ export async function curlPanelRequest({
337
368
  * client certificate (mTLS); neither the P12 password nor the body is ever
338
369
  * in curl's arguments. Resolves with the JSON body; rejects with an Error
339
370
  * (`statusCode`, the panel's `error` in the message, a refusal's `code`,
340
- * `reasons` and `operationId`) for any non-2xx answer, and with one that
341
- * says why when the request could not be made.
371
+ * `reasons` and `operationId`, a refused settings change's `errors`,
372
+ * `revision` and `hosting`) for any non-2xx answer, and with one that says
373
+ * why when the request could not be made.
342
374
  *
343
375
  * @param {string} method
344
376
  * @param {string} path e.g. '/api/tunnels'
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `lamaste-server plugins hooks|update|approve-hooks`: why an update waits
3
+ * for approval, the command that approves it, and the arguments that
4
+ * command takes. Pure — no output — so they are tested as they are.
5
+ */
6
+
7
+ import { terminalText } from './plugin-settings.js';
8
+
9
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
10
+ const VERSION_RE = /^[0-9A-Za-z.+-]{1,64}$/;
11
+ /** The reason an update turns secret settings into plain ones. */
12
+ const SETTINGS_SECRET_TO_PLAIN = 'settings.secret-to-plain';
13
+
14
+ /** How `approve-hooks` is called. */
15
+ export const APPROVE_HOOKS_USAGE =
16
+ 'Usage: lamaste-server plugins approve-hooks <name> --digest <sha256> [--version <v>] (what `hooks <name>` shows)';
17
+
18
+ /**
19
+ * Why a pending update waits, as the terminal shows it: each reason of
20
+ * `approvalRequired` (`[{ code, reason }]`; a secret turned plain is a
21
+ * warning), and the sentences of `autoApproveWithheld` that do not repeat
22
+ * one of them — what auto-approve, when on, could not cover beyond that.
23
+ * Every text as {@link terminalText}.
24
+ *
25
+ * @param {{ approvalRequired?: unknown, autoApproveWithheld?: unknown }} update
26
+ * @returns {{ required: Array<{ text: string, warning: boolean }>, notCovered: string[] }}
27
+ */
28
+ export function pendingUpdateReasons(update) {
29
+ const required = (Array.isArray(update?.approvalRequired) ? update.approvalRequired : [])
30
+ .filter((entry) => entry !== null && typeof entry === 'object')
31
+ .map((entry) => ({
32
+ text: terminalText(entry.reason),
33
+ warning: entry.code === SETTINGS_SECRET_TO_PLAIN,
34
+ }));
35
+ const said = new Set(required.map((line) => line.text));
36
+ const notCovered = (Array.isArray(update?.autoApproveWithheld) ? update.autoApproveWithheld : [])
37
+ .map((reason) => terminalText(reason))
38
+ .filter((text) => !said.has(text));
39
+ return { required, notCovered };
40
+ }
41
+
42
+ /**
43
+ * The command that approves what plugin `name` brings: the installed
44
+ * version by its digest (`{ digest }`), or a pending update by its version
45
+ * and — when it has one — its digest (`{ digest, version }`).
46
+ *
47
+ * @param {string} name
48
+ * @param {{ digest?: string | null, version?: string | null }} target
49
+ */
50
+ export function approveHooksCommand(name, { digest = null, version = null }) {
51
+ return [
52
+ `lamaste-server plugins approve-hooks ${terminalText(name)}`,
53
+ ...(digest ? [`--digest ${terminalText(digest)}`] : []),
54
+ ...(version ? [`--version ${terminalText(version)}`] : []),
55
+ ].join(' ');
56
+ }
57
+
58
+ /**
59
+ * The arguments of `approve-hooks <name>` after the name: `--digest
60
+ * <sha256>` (the installed version's, or a pending update's) and
61
+ * `--version <v>` (a pending update's) — at least one; `--version` alone
62
+ * approves an update that brings neither relay hooks nor an agent half.
63
+ *
64
+ * @param {string[]} rest
65
+ * @returns {{ digest: string | null, version: string | null } | { error: string }}
66
+ */
67
+ export function parseApproveArgs(rest) {
68
+ const valueOf = (flag) => {
69
+ const idx = rest.indexOf(flag);
70
+ return idx === -1 ? null : (rest[idx + 1] ?? '');
71
+ };
72
+ const digest = valueOf('--digest');
73
+ const version = valueOf('--version');
74
+ if (digest === null && version === null) return { error: APPROVE_HOOKS_USAGE };
75
+ if (digest !== null && !DIGEST_RE.test(digest)) return { error: APPROVE_HOOKS_USAGE };
76
+ if (version !== null && !VERSION_RE.test(version)) return { error: APPROVE_HOOKS_USAGE };
77
+ return { digest, version };
78
+ }