@bsb/config-vault 9.6.10 → 9.6.12
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/README.md +4 -2
- package/lib/plugins/service-config-vault/http-server.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/http-server.js +368 -10
- package/lib/plugins/service-config-vault/http-server.js.map +1 -1
- package/lib/plugins/service-config-vault/store.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/store.js +6 -2
- package/lib/plugins/service-config-vault/store.js.map +1 -1
- package/lib/plugins/service-config-vault/vault.d.ts +17 -0
- package/lib/plugins/service-config-vault/vault.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/vault.js +230 -0
- package/lib/plugins/service-config-vault/vault.js.map +1 -1
- package/lib/schemas/config-vault.json +1 -1
- package/lib/schemas/config-vault.plugin.json +1 -1
- package/lib/schemas/service-config-vault.json +1 -1
- package/lib/schemas/service-config-vault.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ Keep the value stable. If the key changes, Vault cannot decrypt configs already
|
|
|
52
52
|
|
|
53
53
|
On first startup, Vault logs a one-time setup code. Open `/setup`, enter the code, create the admin user, and confirm the password. Vault generates the TOTP enrollment secret and authenticator URI after the user is created.
|
|
54
54
|
|
|
55
|
+
Vault has exactly one admin user. Treat that as part of the security model, not a missing team-management feature.
|
|
56
|
+
|
|
55
57
|
On the first login, Vault verifies password and TOTP, then checks whether the admin has a registered passkey. If no passkey exists, Vault sends the admin through browser passkey enrollment and then forces a fresh login.
|
|
56
58
|
|
|
57
59
|
After enrollment, every admin login requires password, TOTP, and a browser passkey assertion. Passkeys require HTTPS in browsers unless you are using localhost for local development, and `publicUrl` must match the external URL used to open Vault.
|
|
@@ -60,7 +62,7 @@ After enrollment, every admin login requires password, TOTP, and a browser passk
|
|
|
60
62
|
|
|
61
63
|
Vault has pages for Overview, Applications, Deployments, Plugins, and Profile. Deployment profiles own config drafts, publishing, and container key create/rotate flows.
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
Vault stores profile config internally as the profile body:
|
|
64
66
|
|
|
65
67
|
```json
|
|
66
68
|
{
|
|
@@ -70,4 +72,4 @@ When editing a profile config, enter only the profile body:
|
|
|
70
72
|
}
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
Vault wraps
|
|
75
|
+
The admin UI builds that body from plugin catalog entries and generated config schemas. Add a plugin, enable or disable it, then fill out the schema-derived fields instead of editing JSON. Vault validates those fields server-side, applies defaults, strips unknown keys, and rejects invalid values before encrypting drafts. Vault wraps the body under the profile name internally. Container keys are generated from the deployment profile page and the UI shows the BSB container env vars once on creation or rotation.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-config-vault/http-server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,UAAU,CAAC;IAChB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,gBAAgB;IAI/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-config-vault/http-server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,UAAU,CAAC;IAChB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,gBAAgB;IAI/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoVtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAUb,WAAW;YAUX,gBAAgB;IAY9B,OAAO,CAAC,IAAI;CAGb"}
|
|
@@ -185,6 +185,31 @@ export class VaultHttpServer {
|
|
|
185
185
|
const body = await readBody(event);
|
|
186
186
|
return this.options.vault.publishDraft(user.userId, String(body.profileId ?? ''));
|
|
187
187
|
}));
|
|
188
|
+
app.use('/api/profile-plugins/delete', defineEventHandler(async (event) => {
|
|
189
|
+
const user = await this.requireUser(event);
|
|
190
|
+
const body = await readBody(event);
|
|
191
|
+
await this.options.vault.removeProfilePlugin(user.userId, {
|
|
192
|
+
profileId: String(body.profileId ?? ''),
|
|
193
|
+
section: parseConfigSection(body.section),
|
|
194
|
+
name: String(body.name ?? ''),
|
|
195
|
+
});
|
|
196
|
+
return { success: true };
|
|
197
|
+
}));
|
|
198
|
+
app.use('/api/profile-plugins', defineEventHandler(async (event) => {
|
|
199
|
+
const user = await this.requireUser(event);
|
|
200
|
+
const body = await readBody(event);
|
|
201
|
+
await this.options.vault.upsertProfilePlugin(user.userId, {
|
|
202
|
+
profileId: String(body.profileId ?? ''),
|
|
203
|
+
section: parseConfigSection(body.section),
|
|
204
|
+
name: String(body.name ?? ''),
|
|
205
|
+
plugin: String(body.plugin ?? ''),
|
|
206
|
+
packageName: stringOrUndefined(body.packageName) ?? null,
|
|
207
|
+
version: stringOrUndefined(body.version) ?? null,
|
|
208
|
+
enabled: body.enabled === true || body.enabled === 'true' || body.enabled === 'on',
|
|
209
|
+
config: parseJsonObject(body.config) ?? {},
|
|
210
|
+
});
|
|
211
|
+
return { success: true };
|
|
212
|
+
}));
|
|
188
213
|
app.use('/api/runtime-keys/rotate', defineEventHandler(async (event) => {
|
|
189
214
|
const user = await this.requireUser(event);
|
|
190
215
|
const body = await readBody(event);
|
|
@@ -343,6 +368,8 @@ function html(title, body, active, authenticated) {
|
|
|
343
368
|
.muted{color:var(--muted)}.danger{color:var(--danger)}.ok{color:var(--ok)}
|
|
344
369
|
.auth{max-width:480px;margin:32px auto}.stack{display:flex;flex-direction:column;gap:12px}.actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
345
370
|
.status{margin-top:12px;color:var(--muted);font-size:14px}.code{word-break:break-all;background:#f2f4f7;border:1px solid var(--line);border-radius:6px;padding:10px}
|
|
371
|
+
.schema-box{border:1px solid var(--line);border-radius:6px;margin:12px 0;padding:10px}.schema-box legend{font-weight:750;color:#344054}
|
|
372
|
+
.schema-repeat{border:1px solid #edf0f5;border-radius:6px;margin:12px 0;padding:10px;background:#fbfcfe}.repeat-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;gap:8px;align-items:start}.schema-repeat[data-array-path] .repeat-row{grid-template-columns:minmax(0,1fr) auto}
|
|
346
373
|
@media(max-width:760px){.shell{display:block}nav{border-right:0;border-bottom:1px solid var(--line)}main{padding:16px}.page-head{display:block}}
|
|
347
374
|
</style>
|
|
348
375
|
</head>
|
|
@@ -544,27 +571,26 @@ function deploymentDetailPage(data, credential) {
|
|
|
544
571
|
</section>
|
|
545
572
|
</div>
|
|
546
573
|
<section><h2>Profile Config</h2>
|
|
547
|
-
|
|
548
|
-
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
549
|
-
<label>Config JSON</label><textarea name="config" required>${escapeHtml(JSON.stringify(draft, null, 2))}</textarea>
|
|
550
|
-
<button>Save Draft</button><p class="status"></p>
|
|
551
|
-
</form>
|
|
574
|
+
${profileConfigEditor(data, draft, redirect)}
|
|
552
575
|
<form data-api="/api/publish" data-redirect="${escapeHtml(redirect)}">
|
|
553
576
|
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
554
577
|
<button class="secondary">Publish Draft</button><p class="status"></p>
|
|
555
578
|
</form>
|
|
556
579
|
</section>
|
|
557
580
|
<section><h2>Container Keys</h2>${profileRuntimeKeyTable(data.runtimeKeys, data)}</section>
|
|
581
|
+
${pluginEditorScript(data.plugins)}
|
|
558
582
|
${formScript()}`;
|
|
559
583
|
}
|
|
560
584
|
function pluginsPage(data, registry, query) {
|
|
585
|
+
const visiblePlugins = data.plugins.filter((plugin) => plugin.kind !== 'config');
|
|
586
|
+
const visibleRegistry = registry.filter((plugin) => plugin.kind !== 'config');
|
|
561
587
|
return `<div class="page-head"><div><h1>Plugin Catalog</h1><p class="muted">Import registry plugins for config authoring, or create private plugin entries manually.</p></div></div>
|
|
562
588
|
<section><h2>Registry Search</h2>
|
|
563
589
|
<form method="get" action="/plugins" class="inline-form">
|
|
564
590
|
${input('query', 'Search', false, query)}
|
|
565
591
|
<button>Search Registry</button>
|
|
566
592
|
</form>
|
|
567
|
-
${
|
|
593
|
+
${visibleRegistry.length === 0 ? '<p class="muted">No configurable registry results loaded.</p>' : registryTable(visibleRegistry)}
|
|
568
594
|
</section>
|
|
569
595
|
<section><h2>Private Plugin</h2>
|
|
570
596
|
<form data-api="/api/plugins" data-redirect="/plugins">
|
|
@@ -574,19 +600,18 @@ function pluginsPage(data, registry, query) {
|
|
|
574
600
|
${input('pluginId', 'Plugin ID', true)}
|
|
575
601
|
${input('packageName', 'Package')}
|
|
576
602
|
${input('version', 'Version', true, '0.0.0')}
|
|
577
|
-
${select('kind', 'Kind', [['service', 'service'], ['events', 'events'], ['observable', 'observable']
|
|
578
|
-
${select('source', 'Source', [['manual', 'manual'], ['registry', 'registry'], ['upload', 'upload']])}
|
|
603
|
+
${select('kind', 'Kind', [['service', 'service'], ['events', 'events'], ['observable', 'observable']])}
|
|
579
604
|
</div>
|
|
580
605
|
<label>Config Schema JSON</label><textarea name="configSchema" placeholder='{"type":"object"}'></textarea>
|
|
581
606
|
<button>Create Plugin</button><p class="status"></p>
|
|
582
607
|
</form>
|
|
583
608
|
</section>
|
|
584
|
-
<section><h2>Catalog</h2>${table(
|
|
609
|
+
<section><h2>Catalog</h2>${table(visiblePlugins.map((x) => [pluginDisplayName(x), x.version, x.kind, x.source, x.packageName ?? '']))}</section>
|
|
585
610
|
${formScript()}`;
|
|
586
611
|
}
|
|
587
612
|
function registryTable(items) {
|
|
588
613
|
return `<table>${items.map((item) => `<tr>
|
|
589
|
-
<td>${escapeHtml(item
|
|
614
|
+
<td>${escapeHtml(pluginDisplayName(item))}</td>
|
|
590
615
|
<td>${escapeHtml(item.version)}</td>
|
|
591
616
|
<td>${escapeHtml(item.kind)}</td>
|
|
592
617
|
<td>${escapeHtml(item.packageName ?? '')}</td>
|
|
@@ -702,6 +727,173 @@ function runtimeKeyTable(keys, data) {
|
|
|
702
727
|
key.revokedAt ?? 'active',
|
|
703
728
|
]));
|
|
704
729
|
}
|
|
730
|
+
function profileConfigEditor(data, draft, redirect) {
|
|
731
|
+
return `
|
|
732
|
+
${addPluginForm(data, redirect)}
|
|
733
|
+
${configSectionEditor(data, draft, 'services', 'Services', redirect)}
|
|
734
|
+
${configSectionEditor(data, draft, 'events', 'Events', redirect)}
|
|
735
|
+
${configSectionEditor(data, draft, 'observable', 'Observable', redirect)}
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
function addPluginForm(data, redirect) {
|
|
739
|
+
const configurablePlugins = data.plugins.filter((plugin) => plugin.kind !== 'config');
|
|
740
|
+
if (configurablePlugins.length === 0) {
|
|
741
|
+
return '<p class="muted">Import or create plugins in the Plugin Catalog before adding profile config.</p>';
|
|
742
|
+
}
|
|
743
|
+
return `<section><h3>Add Plugin</h3>
|
|
744
|
+
<form data-api="/api/profile-plugins" data-redirect="${escapeHtml(redirect)}" data-config-form>
|
|
745
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
746
|
+
<input type="hidden" name="plugin">
|
|
747
|
+
<input type="hidden" name="packageName">
|
|
748
|
+
<input type="hidden" name="version">
|
|
749
|
+
<input type="hidden" name="section">
|
|
750
|
+
<input type="hidden" name="config">
|
|
751
|
+
<div class="form-grid">
|
|
752
|
+
<label>Plugin<select name="catalogId" data-plugin-picker required>${configurablePlugins.map((plugin) => `<option value="${escapeHtml(plugin.id)}">${escapeHtml(pluginDisplayName(plugin))} ${escapeHtml(plugin.version)}</option>`).join('')}</select></label>
|
|
753
|
+
<label>Type<input name="typeDisplay" disabled></label>
|
|
754
|
+
${input('name', 'Config Name', true)}
|
|
755
|
+
<label>Enabled<select name="enabled"><option value="true">Enabled</option><option value="false">Disabled</option></select></label>
|
|
756
|
+
</div>
|
|
757
|
+
<div data-config-fields></div>
|
|
758
|
+
<button>Add Plugin</button><p class="status"></p>
|
|
759
|
+
</form>
|
|
760
|
+
</section>`;
|
|
761
|
+
}
|
|
762
|
+
function configSectionEditor(data, draft, section, title, redirect) {
|
|
763
|
+
const entries = Object.entries(draft[section] ?? {});
|
|
764
|
+
return `<section><h3>${escapeHtml(title)}</h3>
|
|
765
|
+
${entries.length === 0 ? '<p class="muted">No plugins configured.</p>' : entries.map(([name, entry]) => {
|
|
766
|
+
const catalog = findCatalogPlugin(data, entry.plugin, entry.version, entry.package);
|
|
767
|
+
return `<div style="border-top:1px solid var(--line);padding-top:12px;margin-top:12px">
|
|
768
|
+
<form data-api="/api/profile-plugins" data-redirect="${escapeHtml(redirect)}" data-config-form>
|
|
769
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
770
|
+
<input type="hidden" name="section" value="${escapeHtml(section)}">
|
|
771
|
+
<input type="hidden" name="name" value="${escapeHtml(name)}">
|
|
772
|
+
<input type="hidden" name="plugin" value="${escapeHtml(entry.plugin)}">
|
|
773
|
+
<input type="hidden" name="packageName" value="${escapeHtml(entry.package ?? '')}">
|
|
774
|
+
<input type="hidden" name="version" value="${escapeHtml(entry.version ?? '')}">
|
|
775
|
+
<input type="hidden" name="config">
|
|
776
|
+
<div class="form-grid">
|
|
777
|
+
${input('displayName', 'Config Name', false, name).replace('name="displayName"', 'name="displayName" disabled')}
|
|
778
|
+
${input('pluginDisplay', 'Plugin', false, entry.plugin).replace('name="pluginDisplay"', 'name="pluginDisplay" disabled')}
|
|
779
|
+
<label>Enabled<select name="enabled">${entry.enabled ? '<option value="true" selected>Enabled</option><option value="false">Disabled</option>' : '<option value="true">Enabled</option><option value="false" selected>Disabled</option>'}</select></label>
|
|
780
|
+
</div>
|
|
781
|
+
<div data-config-fields>${renderSchemaFields(catalog?.configSchema, entry.config ?? {})}</div>
|
|
782
|
+
<button>Save</button><p class="status"></p>
|
|
783
|
+
</form>
|
|
784
|
+
<form data-api="/api/profile-plugins/delete" data-redirect="${escapeHtml(redirect)}" data-confirm="Remove this plugin from the profile?">
|
|
785
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
786
|
+
<input type="hidden" name="section" value="${escapeHtml(section)}">
|
|
787
|
+
<input type="hidden" name="name" value="${escapeHtml(name)}">
|
|
788
|
+
<button class="secondary">Remove</button><p class="status"></p>
|
|
789
|
+
</form>
|
|
790
|
+
</div>`;
|
|
791
|
+
}).join('')}
|
|
792
|
+
</section>`;
|
|
793
|
+
}
|
|
794
|
+
function findCatalogPlugin(data, pluginId, version, packageName) {
|
|
795
|
+
return data.plugins.find((plugin) => plugin.pluginId === pluginId &&
|
|
796
|
+
(version ? plugin.version === version : true) &&
|
|
797
|
+
(packageName ? plugin.packageName === packageName : true)) ?? data.plugins.find((plugin) => plugin.pluginId === pluginId);
|
|
798
|
+
}
|
|
799
|
+
function pluginDisplayName(plugin) {
|
|
800
|
+
if (!plugin.org || plugin.org === '_')
|
|
801
|
+
return plugin.pluginId;
|
|
802
|
+
return plugin.pluginId.startsWith(`${plugin.org}/`) ? plugin.pluginId : `${plugin.org}/${plugin.pluginId}`;
|
|
803
|
+
}
|
|
804
|
+
function pluginKindLabel(kind) {
|
|
805
|
+
if (kind === 'service')
|
|
806
|
+
return 'Service';
|
|
807
|
+
if (kind === 'events')
|
|
808
|
+
return 'Events';
|
|
809
|
+
if (kind === 'observable')
|
|
810
|
+
return 'Observable';
|
|
811
|
+
return kind;
|
|
812
|
+
}
|
|
813
|
+
function renderSchemaFields(schema, config) {
|
|
814
|
+
const root = objectField(objectField(schema)?.root);
|
|
815
|
+
if (!root || root.kind !== 'object' || !objectField(root.properties)) {
|
|
816
|
+
return '<p class="muted">No config schema available for this plugin.</p>';
|
|
817
|
+
}
|
|
818
|
+
return renderProperties(root.properties, config, '');
|
|
819
|
+
}
|
|
820
|
+
function renderProperties(properties, config, prefix) {
|
|
821
|
+
return Object.entries(properties).map(([key, schema]) => {
|
|
822
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
823
|
+
const value = prefix ? objectField(config)?.[key] : valueAtPath(config, path);
|
|
824
|
+
return renderSchemaControl(key, objectField(schema), path, value);
|
|
825
|
+
}).join('');
|
|
826
|
+
}
|
|
827
|
+
function renderSchemaControl(key, rawNode, path, rawValue) {
|
|
828
|
+
const node = unwrapSchema(rawNode);
|
|
829
|
+
if (!node)
|
|
830
|
+
return '';
|
|
831
|
+
const value = rawValue ?? node.default ?? '';
|
|
832
|
+
const label = schemaLabel(key, node);
|
|
833
|
+
if (node.kind === 'object' && objectField(node.properties)) {
|
|
834
|
+
return `<fieldset class="schema-box"><legend>${escapeHtml(key)}</legend>${renderProperties(node.properties, isRecord(value) ? value : {}, path)}</fieldset>`;
|
|
835
|
+
}
|
|
836
|
+
if (node.kind === 'bool' || node.kind === 'boolean') {
|
|
837
|
+
return `<label>${escapeHtml(label)}<select data-config-path="${escapeHtml(path)}" data-kind="bool"><option value="true" ${value === true ? 'selected' : ''}>true</option><option value="false" ${value === false ? 'selected' : ''}>false</option></select></label>`;
|
|
838
|
+
}
|
|
839
|
+
if (node.kind === 'enum' && Array.isArray(node.values)) {
|
|
840
|
+
return `<label>${escapeHtml(label)}<select data-config-path="${escapeHtml(path)}" data-kind="string">${node.values.map((item) => `<option value="${escapeHtml(String(item))}" ${String(value) === String(item) ? 'selected' : ''}>${escapeHtml(String(item))}</option>`).join('')}</select></label>`;
|
|
841
|
+
}
|
|
842
|
+
if (node.kind === 'array') {
|
|
843
|
+
const itemNode = unwrapSchema(objectField(node.items) ?? objectField(node.item));
|
|
844
|
+
const kind = inputKind(itemNode);
|
|
845
|
+
const values = Array.isArray(value) ? value : [];
|
|
846
|
+
const rows = (values.length ? values : ['']).map((item) => `<div class="repeat-row"><input data-array-item data-kind="${escapeHtml(kind)}" ${kind === 'number' ? 'type="number"' : ''} value="${escapeHtml(String(item ?? ''))}"><button type="button" class="secondary" data-remove-row>Remove</button></div>`).join('');
|
|
847
|
+
return `<div class="schema-repeat" data-array-path="${escapeHtml(path)}" data-item-kind="${escapeHtml(kind)}"><label>${escapeHtml(label)}</label><div data-repeat-rows>${rows}</div><button type="button" class="secondary" data-add-array-item>Add Item</button></div>`;
|
|
848
|
+
}
|
|
849
|
+
if (node.kind === 'record') {
|
|
850
|
+
const valueNode = unwrapSchema(objectField(node.valueSchema) ?? objectField(node.values) ?? objectField(node.value));
|
|
851
|
+
const kind = inputKind(valueNode);
|
|
852
|
+
const entries = isRecord(value) ? Object.entries(value) : [];
|
|
853
|
+
const rows = (entries.length ? entries : [['', '']]).map(([recordKey, recordValue]) => `<div class="repeat-row"><input data-record-key placeholder="Key" value="${escapeHtml(String(recordKey))}"><input data-record-value data-kind="${escapeHtml(kind)}" ${kind === 'number' ? 'type="number"' : ''} placeholder="Value" value="${escapeHtml(String(recordValue ?? ''))}"><button type="button" class="secondary" data-remove-row>Remove</button></div>`).join('');
|
|
854
|
+
return `<div class="schema-repeat" data-record-path="${escapeHtml(path)}" data-value-kind="${escapeHtml(kind)}"><label>${escapeHtml(label)}</label><div data-repeat-rows>${rows}</div><button type="button" class="secondary" data-add-record-row>Add Entry</button></div>`;
|
|
855
|
+
}
|
|
856
|
+
if (node.kind === 'tuple') {
|
|
857
|
+
const items = (Array.isArray(node.items) ? node.items : Array.isArray(node.elements) ? node.elements : []).map((item) => objectField(item));
|
|
858
|
+
const values = Array.isArray(value) ? value : [];
|
|
859
|
+
return `<fieldset class="schema-box" data-tuple-path="${escapeHtml(path)}"><legend>${escapeHtml(label)}</legend>${items.map((item, index) => {
|
|
860
|
+
const child = unwrapSchema(item);
|
|
861
|
+
const kind = inputKind(child);
|
|
862
|
+
return `<label>Item ${index + 1}<input data-tuple-index="${index}" data-kind="${escapeHtml(kind)}" ${kind === 'number' ? 'type="number"' : ''} value="${escapeHtml(String(values[index] ?? child?.default ?? ''))}"></label>`;
|
|
863
|
+
}).join('')}</fieldset>`;
|
|
864
|
+
}
|
|
865
|
+
if (node.kind === 'union' && Array.isArray(node.variants)) {
|
|
866
|
+
const variant = unwrapSchema(objectField(node.variants[0]));
|
|
867
|
+
return renderSchemaControl(label, variant, path, value);
|
|
868
|
+
}
|
|
869
|
+
const kind = inputKind(node);
|
|
870
|
+
return `<label>${escapeHtml(label)}<input data-config-path="${escapeHtml(path)}" data-kind="${escapeHtml(kind)}" ${kind === 'number' ? 'type="number"' : ''} value="${escapeHtml(String(value ?? ''))}"></label>`;
|
|
871
|
+
}
|
|
872
|
+
function schemaLabel(key, node) {
|
|
873
|
+
return node.metadata && typeof node.metadata === 'object' && 'description' in node.metadata
|
|
874
|
+
? String(node.metadata.description)
|
|
875
|
+
: key;
|
|
876
|
+
}
|
|
877
|
+
function inputKind(node) {
|
|
878
|
+
if (!node)
|
|
879
|
+
return 'string';
|
|
880
|
+
if (node.kind === 'bool' || node.kind === 'boolean')
|
|
881
|
+
return 'bool';
|
|
882
|
+
return ['int', 'int32', 'int64', 'number', 'float', 'float32', 'float64'].includes(String(node.kind)) ? 'number' : 'string';
|
|
883
|
+
}
|
|
884
|
+
function isRecord(value) {
|
|
885
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
886
|
+
}
|
|
887
|
+
function unwrapSchema(node) {
|
|
888
|
+
let current = node;
|
|
889
|
+
while (current && (current.kind === 'optional' || current.kind === 'nullable')) {
|
|
890
|
+
current = objectField(current.inner);
|
|
891
|
+
}
|
|
892
|
+
return current;
|
|
893
|
+
}
|
|
894
|
+
function valueAtPath(source, path) {
|
|
895
|
+
return path.split('.').reduce((acc, part) => objectField(acc)?.[part], source);
|
|
896
|
+
}
|
|
705
897
|
function profileRuntimeKeyTable(keys, data) {
|
|
706
898
|
if (keys.length === 0)
|
|
707
899
|
return '<p class="muted">No container keys created for this profile.</p>';
|
|
@@ -770,6 +962,166 @@ function formScript() {
|
|
|
770
962
|
});
|
|
771
963
|
</script>`;
|
|
772
964
|
}
|
|
965
|
+
function pluginEditorScript(plugins) {
|
|
966
|
+
const catalog = plugins.reduce((acc, plugin) => {
|
|
967
|
+
acc[plugin.id] = {
|
|
968
|
+
plugin: plugin.pluginId,
|
|
969
|
+
packageName: plugin.packageName ?? '',
|
|
970
|
+
version: plugin.version,
|
|
971
|
+
kind: plugin.kind,
|
|
972
|
+
kindLabel: pluginKindLabel(plugin.kind),
|
|
973
|
+
schema: plugin.configSchema ?? null,
|
|
974
|
+
};
|
|
975
|
+
return acc;
|
|
976
|
+
}, {});
|
|
977
|
+
return `<script>
|
|
978
|
+
const vaultPluginCatalog = ${jsonForScript(catalog)};
|
|
979
|
+
function setPath(target, path, value) {
|
|
980
|
+
const parts = path.split('.');
|
|
981
|
+
let current = target;
|
|
982
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
983
|
+
current[parts[i]] = current[parts[i]] || {};
|
|
984
|
+
current = current[parts[i]];
|
|
985
|
+
}
|
|
986
|
+
current[parts[parts.length - 1]] = value;
|
|
987
|
+
}
|
|
988
|
+
function readConfigForm(form) {
|
|
989
|
+
const config = {};
|
|
990
|
+
form.querySelectorAll('[data-array-path]').forEach((group) => {
|
|
991
|
+
const values = Array.from(group.querySelectorAll('[data-array-item]'))
|
|
992
|
+
.map((field) => parseFieldValue(field))
|
|
993
|
+
.filter((value) => value !== undefined);
|
|
994
|
+
if (values.length > 0) setPath(config, group.dataset.arrayPath, values);
|
|
995
|
+
});
|
|
996
|
+
form.querySelectorAll('[data-record-path]').forEach((group) => {
|
|
997
|
+
const record = {};
|
|
998
|
+
group.querySelectorAll('.repeat-row').forEach((row) => {
|
|
999
|
+
const key = row.querySelector('[data-record-key]')?.value?.trim();
|
|
1000
|
+
const valueField = row.querySelector('[data-record-value]');
|
|
1001
|
+
const value = valueField ? parseFieldValue(valueField) : undefined;
|
|
1002
|
+
if (key && value !== undefined) record[key] = value;
|
|
1003
|
+
});
|
|
1004
|
+
if (Object.keys(record).length > 0) setPath(config, group.dataset.recordPath, record);
|
|
1005
|
+
});
|
|
1006
|
+
form.querySelectorAll('[data-tuple-path]').forEach((group) => {
|
|
1007
|
+
const values = [];
|
|
1008
|
+
group.querySelectorAll('[data-tuple-index]').forEach((field) => {
|
|
1009
|
+
const value = parseFieldValue(field);
|
|
1010
|
+
if (value !== undefined) values[Number(field.dataset.tupleIndex)] = value;
|
|
1011
|
+
});
|
|
1012
|
+
if (values.length > 0) setPath(config, group.dataset.tuplePath, values);
|
|
1013
|
+
});
|
|
1014
|
+
form.querySelectorAll('[data-config-path]').forEach((field) => {
|
|
1015
|
+
if (field.closest('[data-array-path],[data-record-path],[data-tuple-path]')) return;
|
|
1016
|
+
const value = parseFieldValue(field);
|
|
1017
|
+
if (value !== undefined) setPath(config, field.dataset.configPath, value);
|
|
1018
|
+
});
|
|
1019
|
+
return config;
|
|
1020
|
+
}
|
|
1021
|
+
function parseFieldValue(field) {
|
|
1022
|
+
const raw = field.value;
|
|
1023
|
+
if (raw === '') return undefined;
|
|
1024
|
+
if (field.dataset.kind === 'number') return Number(raw);
|
|
1025
|
+
if (field.dataset.kind === 'bool') return raw === 'true';
|
|
1026
|
+
return raw;
|
|
1027
|
+
}
|
|
1028
|
+
function schemaRoot(schema) {
|
|
1029
|
+
return schema && schema.root && schema.root.kind === 'object' ? schema.root : null;
|
|
1030
|
+
}
|
|
1031
|
+
function escapeClient(value) {
|
|
1032
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
1033
|
+
}
|
|
1034
|
+
function unwrapNode(node) {
|
|
1035
|
+
while (node && (node.kind === 'optional' || node.kind === 'nullable')) node = node.inner;
|
|
1036
|
+
return node;
|
|
1037
|
+
}
|
|
1038
|
+
function inputKind(node) {
|
|
1039
|
+
node = unwrapNode(node);
|
|
1040
|
+
if (!node) return 'string';
|
|
1041
|
+
if (node.kind === 'bool' || node.kind === 'boolean') return 'bool';
|
|
1042
|
+
return ['int','int32','int64','number','float','float32','float64'].includes(String(node.kind)) ? 'number' : 'string';
|
|
1043
|
+
}
|
|
1044
|
+
function primitiveInput(attrs, kind, value) {
|
|
1045
|
+
return '<input ' + attrs + ' data-kind="' + kind + '"' + (kind === 'number' ? ' type="number"' : '') + ' value="' + escapeClient(value || '') + '">';
|
|
1046
|
+
}
|
|
1047
|
+
function repeatRow(kind, key, value) {
|
|
1048
|
+
return '<div class="repeat-row">'
|
|
1049
|
+
+ (key === null ? '' : '<input data-record-key placeholder="Key" value="' + escapeClient(key || '') + '">')
|
|
1050
|
+
+ primitiveInput(key === null ? 'data-array-item' : 'data-record-value', kind, value)
|
|
1051
|
+
+ '<button type="button" class="secondary" data-remove-row>Remove</button></div>';
|
|
1052
|
+
}
|
|
1053
|
+
function renderFields(properties, prefix) {
|
|
1054
|
+
return Object.entries(properties || {}).map(([key, rawNode]) => {
|
|
1055
|
+
let node = unwrapNode(rawNode);
|
|
1056
|
+
if (!node) return '';
|
|
1057
|
+
const path = prefix ? prefix + '.' + key : key;
|
|
1058
|
+
const label = (node.metadata && node.metadata.description) || key;
|
|
1059
|
+
if (node.kind === 'object' && node.properties) {
|
|
1060
|
+
return '<fieldset class="schema-box"><legend>' + escapeClient(key) + '</legend>' + renderFields(node.properties, path) + '</fieldset>';
|
|
1061
|
+
}
|
|
1062
|
+
if (node.kind === 'bool' || node.kind === 'boolean') {
|
|
1063
|
+
return '<label>' + escapeClient(label) + '<select data-config-path="' + escapeClient(path) + '" data-kind="bool"><option value="true">true</option><option value="false">false</option></select></label>';
|
|
1064
|
+
}
|
|
1065
|
+
if (node.kind === 'enum' && Array.isArray(node.values)) {
|
|
1066
|
+
return '<label>' + escapeClient(label) + '<select data-config-path="' + escapeClient(path) + '" data-kind="string">' + node.values.map((item) => '<option value="' + escapeClient(item) + '">' + escapeClient(item) + '</option>').join('') + '</select></label>';
|
|
1067
|
+
}
|
|
1068
|
+
if (node.kind === 'array') {
|
|
1069
|
+
const kind = inputKind(node.items || node.item);
|
|
1070
|
+
return '<div class="schema-repeat" data-array-path="' + escapeClient(path) + '" data-item-kind="' + kind + '"><label>' + escapeClient(label) + '</label><div data-repeat-rows>' + repeatRow(kind, null, '') + '</div><button type="button" class="secondary" data-add-array-item>Add Item</button></div>';
|
|
1071
|
+
}
|
|
1072
|
+
if (node.kind === 'record') {
|
|
1073
|
+
const kind = inputKind(node.valueSchema || node.values || node.value);
|
|
1074
|
+
return '<div class="schema-repeat" data-record-path="' + escapeClient(path) + '" data-value-kind="' + kind + '"><label>' + escapeClient(label) + '</label><div data-repeat-rows>' + repeatRow(kind, '', '') + '</div><button type="button" class="secondary" data-add-record-row>Add Entry</button></div>';
|
|
1075
|
+
}
|
|
1076
|
+
if (node.kind === 'tuple') {
|
|
1077
|
+
const items = Array.isArray(node.items) ? node.items : Array.isArray(node.elements) ? node.elements : [];
|
|
1078
|
+
return '<fieldset class="schema-box" data-tuple-path="' + escapeClient(path) + '"><legend>' + escapeClient(label) + '</legend>' + items.map((item, index) => '<label>Item ' + (index + 1) + primitiveInput('data-tuple-index="' + index + '"', inputKind(item), '') + '</label>').join('') + '</fieldset>';
|
|
1079
|
+
}
|
|
1080
|
+
if (node.kind === 'union' && Array.isArray(node.variants) && node.variants[0]) {
|
|
1081
|
+
return renderFields({ [key]: node.variants[0] }, prefix);
|
|
1082
|
+
}
|
|
1083
|
+
const kind = inputKind(node);
|
|
1084
|
+
return '<label>' + escapeClient(label) + primitiveInput('data-config-path="' + escapeClient(path) + '"', kind, '') + '</label>';
|
|
1085
|
+
}).join('');
|
|
1086
|
+
}
|
|
1087
|
+
document.addEventListener('click', (event) => {
|
|
1088
|
+
const button = event.target.closest('[data-add-array-item],[data-add-record-row],[data-remove-row]');
|
|
1089
|
+
if (!button) return;
|
|
1090
|
+
if (button.matches('[data-remove-row]')) {
|
|
1091
|
+
button.closest('.repeat-row')?.remove();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const group = button.closest('.schema-repeat');
|
|
1095
|
+
const rows = group?.querySelector('[data-repeat-rows]');
|
|
1096
|
+
if (!group || !rows) return;
|
|
1097
|
+
if (button.matches('[data-add-array-item]')) rows.insertAdjacentHTML('beforeend', repeatRow(group.dataset.itemKind || 'string', null, ''));
|
|
1098
|
+
if (button.matches('[data-add-record-row]')) rows.insertAdjacentHTML('beforeend', repeatRow(group.dataset.valueKind || 'string', '', ''));
|
|
1099
|
+
});
|
|
1100
|
+
document.querySelectorAll('[data-plugin-picker]').forEach((select) => {
|
|
1101
|
+
const form = select.closest('form');
|
|
1102
|
+
const sync = () => {
|
|
1103
|
+
const item = vaultPluginCatalog[select.value];
|
|
1104
|
+
if (!item || !form) return;
|
|
1105
|
+
form.elements.plugin.value = item.plugin || '';
|
|
1106
|
+
form.elements.packageName.value = item.packageName || '';
|
|
1107
|
+
form.elements.version.value = item.version || '';
|
|
1108
|
+
if (form.elements.section && item.kind) form.elements.section.value = item.kind === 'service' ? 'services' : item.kind;
|
|
1109
|
+
if (form.elements.typeDisplay && item.kindLabel) form.elements.typeDisplay.value = item.kindLabel;
|
|
1110
|
+
if (form.elements.name && !form.elements.name.value) form.elements.name.value = item.plugin || '';
|
|
1111
|
+
const root = schemaRoot(item.schema);
|
|
1112
|
+
const fields = form.querySelector('[data-config-fields]');
|
|
1113
|
+
if (fields) fields.innerHTML = root ? renderFields(root.properties, '') : '<p class="muted">No config schema available for this plugin.</p>';
|
|
1114
|
+
};
|
|
1115
|
+
select.addEventListener('change', sync);
|
|
1116
|
+
sync();
|
|
1117
|
+
});
|
|
1118
|
+
document.querySelectorAll('form[data-config-form]').forEach((form) => {
|
|
1119
|
+
form.addEventListener('submit', () => {
|
|
1120
|
+
if (form.elements.config) form.elements.config.value = JSON.stringify(readConfigForm(form));
|
|
1121
|
+
}, { capture: true });
|
|
1122
|
+
});
|
|
1123
|
+
</script>`;
|
|
1124
|
+
}
|
|
773
1125
|
function webauthnClientScript() {
|
|
774
1126
|
return `
|
|
775
1127
|
function base64UrlToBuffer(value) {
|
|
@@ -893,6 +1245,9 @@ function parseJsonObject(value) {
|
|
|
893
1245
|
function parseKind(value) {
|
|
894
1246
|
return value === 'events' || value === 'observable' || value === 'config' ? value : 'service';
|
|
895
1247
|
}
|
|
1248
|
+
function parseConfigSection(value) {
|
|
1249
|
+
return value === 'events' || value === 'observable' ? value : 'services';
|
|
1250
|
+
}
|
|
896
1251
|
function parseSource(value) {
|
|
897
1252
|
return value === 'registry' || value === 'upload' ? value : 'manual';
|
|
898
1253
|
}
|
|
@@ -908,4 +1263,7 @@ function escapeHtml(value) {
|
|
|
908
1263
|
"'": ''',
|
|
909
1264
|
}[char] ?? char));
|
|
910
1265
|
}
|
|
1266
|
+
function jsonForScript(value) {
|
|
1267
|
+
return JSON.stringify(value).replace(/<\//g, '<\\/');
|
|
1268
|
+
}
|
|
911
1269
|
//# sourceMappingURL=http-server.js.map
|