@bsb/config-vault 9.6.9 → 9.6.11
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 +24 -10
- package/lib/plugins/service-config-vault/http-server.d.ts +1 -0
- package/lib/plugins/service-config-vault/http-server.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/http-server.js +563 -83
- package/lib/plugins/service-config-vault/http-server.js.map +1 -1
- package/lib/plugins/service-config-vault/index.d.ts +3 -0
- package/lib/plugins/service-config-vault/index.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/index.js +2 -0
- package/lib/plugins/service-config-vault/index.js.map +1 -1
- package/lib/plugins/service-config-vault/store.d.ts +7 -0
- package/lib/plugins/service-config-vault/store.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/store.js +32 -2
- package/lib/plugins/service-config-vault/store.js.map +1 -1
- package/lib/plugins/service-config-vault/vault.d.ts +47 -1
- package/lib/plugins/service-config-vault/vault.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/vault.js +291 -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 +10 -2
- package/lib/schemas/service-config-vault.plugin.json +10 -2
- package/package.json +1 -1
|
@@ -122,7 +122,7 @@ export class VaultHttpServer {
|
|
|
122
122
|
app.use('/api/groups', defineEventHandler(async (event) => {
|
|
123
123
|
const user = await this.requireUser(event);
|
|
124
124
|
const body = await readBody(event);
|
|
125
|
-
return this.options.vault.
|
|
125
|
+
return this.options.vault.createDeployment(user.userId, String(body.applicationId ?? ''), String(body.name ?? ''));
|
|
126
126
|
}));
|
|
127
127
|
app.use('/api/profiles/update', defineEventHandler(async (event) => {
|
|
128
128
|
const user = await this.requireUser(event);
|
|
@@ -141,6 +141,21 @@ export class VaultHttpServer {
|
|
|
141
141
|
const body = await readBody(event);
|
|
142
142
|
return this.options.vault.createProfile(user.userId, String(body.groupId ?? ''), String(body.name ?? 'default'));
|
|
143
143
|
}));
|
|
144
|
+
app.use('/api/plugins/import', defineEventHandler(async (event) => {
|
|
145
|
+
const user = await this.requireUser(event);
|
|
146
|
+
const body = await readBody(event);
|
|
147
|
+
return this.options.vault.createPlugin(user.userId, {
|
|
148
|
+
org: String(body.org ?? '_'),
|
|
149
|
+
name: String(body.name ?? ''),
|
|
150
|
+
pluginId: String(body.pluginId ?? body.name ?? ''),
|
|
151
|
+
packageName: body.packageName === undefined || body.packageName === '' ? null : String(body.packageName),
|
|
152
|
+
version: String(body.version ?? '0.0.0'),
|
|
153
|
+
kind: parseKind(body.kind),
|
|
154
|
+
source: 'registry',
|
|
155
|
+
configSchema: parseJsonObject(body.configSchema) ?? null,
|
|
156
|
+
eventSchema: parseJsonObject(body.eventSchema) ?? null,
|
|
157
|
+
});
|
|
158
|
+
}));
|
|
144
159
|
app.use('/api/plugins', defineEventHandler(async (event) => {
|
|
145
160
|
const user = await this.requireUser(event);
|
|
146
161
|
const body = await readBody(event);
|
|
@@ -162,7 +177,7 @@ export class VaultHttpServer {
|
|
|
162
177
|
const config = parseJsonObject(body.config);
|
|
163
178
|
if (!config)
|
|
164
179
|
throw new Error('Config must be a JSON object');
|
|
165
|
-
await this.options.vault.
|
|
180
|
+
await this.options.vault.saveProfileDraft(user.userId, String(body.profileId ?? ''), config);
|
|
166
181
|
return { success: true };
|
|
167
182
|
}));
|
|
168
183
|
app.use('/api/publish', defineEventHandler(async (event) => {
|
|
@@ -170,16 +185,46 @@ export class VaultHttpServer {
|
|
|
170
185
|
const body = await readBody(event);
|
|
171
186
|
return this.options.vault.publishDraft(user.userId, String(body.profileId ?? ''));
|
|
172
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
|
+
}));
|
|
213
|
+
app.use('/api/runtime-keys/rotate', defineEventHandler(async (event) => {
|
|
214
|
+
const user = await this.requireUser(event);
|
|
215
|
+
const body = await readBody(event);
|
|
216
|
+
return this.options.vault.rotateProfileRuntimeKey(user.userId, {
|
|
217
|
+
keyId: String(body.keyId ?? ''),
|
|
218
|
+
name: stringOrUndefined(body.name),
|
|
219
|
+
});
|
|
220
|
+
}));
|
|
173
221
|
app.use('/api/runtime-keys', defineEventHandler(async (event) => {
|
|
174
222
|
const user = await this.requireUser(event);
|
|
175
223
|
const body = await readBody(event);
|
|
176
|
-
return this.options.vault.
|
|
224
|
+
return this.options.vault.createProfileRuntimeKey(user.userId, {
|
|
177
225
|
name: String(body.name ?? ''),
|
|
178
|
-
applicationId: String(body.applicationId ?? ''),
|
|
179
|
-
groupId: String(body.groupId ?? ''),
|
|
180
226
|
profileId: String(body.profileId ?? ''),
|
|
181
227
|
containerName: body.containerName === undefined ? null : String(body.containerName),
|
|
182
|
-
configPluginId: String(body.configPluginId ?? 'config-vault'),
|
|
183
228
|
});
|
|
184
229
|
}));
|
|
185
230
|
app.use('/applications', defineEventHandler(async (event) => {
|
|
@@ -192,21 +237,45 @@ export class VaultHttpServer {
|
|
|
192
237
|
const dashboard = await this.options.vault.dashboard();
|
|
193
238
|
return this.page('Deployments', deploymentsPage(dashboard), 'deployments');
|
|
194
239
|
}));
|
|
240
|
+
app.use('/deployment', defineEventHandler(async (event) => {
|
|
241
|
+
await this.requireUser(event);
|
|
242
|
+
const query = getQuery(event);
|
|
243
|
+
const profileId = String(query.profileId ?? '');
|
|
244
|
+
if (!profileId)
|
|
245
|
+
return sendRedirect(event, '/deployments');
|
|
246
|
+
const profile = await this.options.vault.deploymentProfile(profileId);
|
|
247
|
+
return this.page('Deployment', deploymentDetailPage(profile, {
|
|
248
|
+
publicUrl: this.options.publicUrl,
|
|
249
|
+
keyId: String(query.keyId ?? ''),
|
|
250
|
+
secret: String(query.secret ?? ''),
|
|
251
|
+
}), 'deployments');
|
|
252
|
+
}));
|
|
195
253
|
app.use('/configs', defineEventHandler(async (event) => {
|
|
196
254
|
await this.requireUser(event);
|
|
197
255
|
const dashboard = await this.options.vault.dashboard();
|
|
198
|
-
|
|
256
|
+
const firstProfile = dashboard.profiles[0];
|
|
257
|
+
return firstProfile ? sendRedirect(event, `/deployment?profileId=${encodeURIComponent(firstProfile.id)}`) : sendRedirect(event, '/deployments');
|
|
199
258
|
}));
|
|
200
259
|
app.use('/runtime-keys', defineEventHandler(async (event) => {
|
|
201
260
|
await this.requireUser(event);
|
|
202
261
|
const query = getQuery(event);
|
|
203
262
|
const dashboard = await this.options.vault.dashboard();
|
|
204
|
-
|
|
263
|
+
const firstProfile = dashboard.profiles[0];
|
|
264
|
+
if (!String(query.secret ?? '') && firstProfile) {
|
|
265
|
+
return sendRedirect(event, `/deployment?profileId=${encodeURIComponent(firstProfile.id)}`);
|
|
266
|
+
}
|
|
267
|
+
return this.page('Container Key', runtimeKeysPage(dashboard, {
|
|
268
|
+
publicUrl: this.options.publicUrl,
|
|
269
|
+
keyId: String(query.keyId ?? ''),
|
|
270
|
+
secret: String(query.secret ?? ''),
|
|
271
|
+
}), 'runtime-keys');
|
|
205
272
|
}));
|
|
206
273
|
app.use('/plugins', defineEventHandler(async (event) => {
|
|
207
274
|
await this.requireUser(event);
|
|
275
|
+
const query = getQuery(event);
|
|
208
276
|
const dashboard = await this.options.vault.dashboard();
|
|
209
|
-
|
|
277
|
+
const registry = await registrySearch(this.options.registryUrl, String(query.query ?? ''));
|
|
278
|
+
return this.page('Plugins', pluginsPage(dashboard, registry, String(query.query ?? '')), 'plugins');
|
|
210
279
|
}));
|
|
211
280
|
app.use('/profile', defineEventHandler(async (event) => {
|
|
212
281
|
const session = await this.requireUser(event);
|
|
@@ -295,9 +364,12 @@ function html(title, body, active, authenticated) {
|
|
|
295
364
|
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
|
|
296
365
|
.metric{font-size:28px;font-weight:750}.page-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin:0 0 18px}
|
|
297
366
|
.inline-form{display:flex;gap:10px;align-items:end;flex-wrap:wrap}.inline-form label{min-width:190px}
|
|
367
|
+
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 16px}.tabs a{padding:8px 10px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:#344054;background:#fff;font-weight:650}.tabs a.active{background:#eaf1ff;color:#155eef;border-color:#b8cdfd}
|
|
298
368
|
.muted{color:var(--muted)}.danger{color:var(--danger)}.ok{color:var(--ok)}
|
|
299
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}
|
|
300
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}
|
|
301
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}}
|
|
302
374
|
</style>
|
|
303
375
|
</head>
|
|
@@ -312,8 +384,6 @@ function nav(active) {
|
|
|
312
384
|
['overview', 'Overview', '/'],
|
|
313
385
|
['applications', 'Applications', '/applications'],
|
|
314
386
|
['deployments', 'Deployments', '/deployments'],
|
|
315
|
-
['configs', 'Configs', '/configs'],
|
|
316
|
-
['runtime-keys', 'Runtime Keys', '/runtime-keys'],
|
|
317
387
|
['plugins', 'Plugins', '/plugins'],
|
|
318
388
|
['profile', 'Profile', '/profile'],
|
|
319
389
|
];
|
|
@@ -439,11 +509,11 @@ function overviewPage(data) {
|
|
|
439
509
|
return `<div class="page-head"><div><h1>Overview</h1><p class="muted">Current Vault inventory and deployment configuration status.</p></div></div>
|
|
440
510
|
<div class="grid">
|
|
441
511
|
${metric('Applications', data.applications.length)}
|
|
442
|
-
${metric('
|
|
512
|
+
${metric('Deployments', data.groups.length)}
|
|
443
513
|
${metric('Deployment Profiles', data.profiles.length)}
|
|
444
|
-
${metric('
|
|
514
|
+
${metric('Container Keys', data.runtimeKeys.length)}
|
|
445
515
|
</div>
|
|
446
|
-
<section><h2>Recent
|
|
516
|
+
<section><h2>Recent Container Keys</h2>${runtimeKeyTable(data.runtimeKeys.slice(0, 8), data)}</section>`;
|
|
447
517
|
}
|
|
448
518
|
function applicationsPage(data) {
|
|
449
519
|
return `<div class="page-head"><div><h1>Applications</h1><p class="muted">Create product or system boundaries for deployment profiles.</p></div></div>
|
|
@@ -460,66 +530,67 @@ function applicationsPage(data) {
|
|
|
460
530
|
${formScript()}`;
|
|
461
531
|
}
|
|
462
532
|
function deploymentsPage(data) {
|
|
463
|
-
return `<div class="page-head"><div><h1>Deployments</h1><p class="muted">
|
|
464
|
-
<
|
|
465
|
-
<section><h2>Create Service Group</h2>
|
|
533
|
+
return `<div class="page-head"><div><h1>Deployments</h1><p class="muted">A deployment represents the container group that will receive one selected profile.</p></div></div>
|
|
534
|
+
<section><h2>Create Deployment</h2>
|
|
466
535
|
<form data-api="/api/groups" data-redirect="/deployments">
|
|
467
536
|
${select('applicationId', 'Application', data.applications.map((x) => [x.id, x.name]))}
|
|
468
|
-
${input('name', '
|
|
469
|
-
<button>Create
|
|
537
|
+
${input('name', 'Deployment Name', true)}
|
|
538
|
+
<button>Create Deployment</button><p class="status"></p>
|
|
470
539
|
</form>
|
|
471
540
|
</section>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
541
|
+
<section><h2>Deployments</h2>${groupsTable(data)}</section>
|
|
542
|
+
<section><h2>Profiles</h2>${profilesTable(data)}</section>
|
|
543
|
+
${formScript()}`;
|
|
544
|
+
}
|
|
545
|
+
function runtimeKeysPage(data, credential) {
|
|
546
|
+
return `<div class="page-head"><div><h1>Container Key</h1><p class="muted">Use these env vars in the target BSB container.</p></div></div>
|
|
547
|
+
${credential.secret ? runtimeEnvBlock(credential) : ''}
|
|
548
|
+
<section><h2>Container Keys</h2>${runtimeKeyTable(data.runtimeKeys, data)}</section>`;
|
|
549
|
+
}
|
|
550
|
+
function deploymentDetailPage(data, credential) {
|
|
551
|
+
const draft = data.draft ?? { observable: {}, events: {}, services: {} };
|
|
552
|
+
const redirect = `/deployment?profileId=${encodeURIComponent(data.profile.id)}`;
|
|
553
|
+
return `<div class="page-head"><div><h1>${escapeHtml(data.group.name)}</h1><p class="muted">${escapeHtml(data.application.name)} / ${escapeHtml(data.profile.name)}</p></div><a class="button secondary" href="/deployments">Back</a></div>
|
|
554
|
+
<div class="tabs">${data.profiles.map((profile) => `<a class="${profile.id === data.profile.id ? 'active' : ''}" href="/deployment?profileId=${encodeURIComponent(profile.id)}">${escapeHtml(profile.name)}</a>`).join('')}</div>
|
|
555
|
+
${credential.secret ? runtimeEnvBlock(credential) : ''}
|
|
556
|
+
<div class="grid">
|
|
557
|
+
<section><h2>Create Profile</h2>
|
|
558
|
+
<form data-api="/api/profiles" data-redirect="${escapeHtml(redirect)}">
|
|
559
|
+
<input type="hidden" name="groupId" value="${escapeHtml(data.group.id)}">
|
|
560
|
+
${input('name', 'Profile Name', true)}
|
|
476
561
|
<button>Create Profile</button><p class="status"></p>
|
|
477
562
|
</form>
|
|
478
563
|
</section>
|
|
564
|
+
<section><h2>Container Key</h2>
|
|
565
|
+
<form data-api="/api/runtime-keys" data-secret-redirect="${escapeHtml(redirect)}">
|
|
566
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
567
|
+
${input('name', 'Key Name', true, `${data.group.name}-${data.profile.name}`)}
|
|
568
|
+
${input('containerName', 'Container Name')}
|
|
569
|
+
<button>Create Key</button><p class="status"></p>
|
|
570
|
+
</form>
|
|
571
|
+
</section>
|
|
479
572
|
</div>
|
|
480
|
-
<section><h2>
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return `<div class="page-head"><div><h1>Configs</h1><p class="muted">Save draft runtime config for a deployment profile, then publish it when ready.</p></div></div>
|
|
486
|
-
<section><h2>Edit Draft</h2>
|
|
487
|
-
<form data-api="/api/drafts" data-redirect="/configs">
|
|
488
|
-
${select('profileId', 'Deployment Profile', data.profiles.map((x) => [x.id, profileLabel(x, data)]))}
|
|
489
|
-
<label>Config JSON</label><textarea name="config" required placeholder='{"default":{"observable":{},"events":{},"services":{}}}'></textarea>
|
|
490
|
-
<button>Save Draft</button><p class="status"></p>
|
|
491
|
-
</form>
|
|
492
|
-
</section>
|
|
493
|
-
<section><h2>Publish Draft</h2>
|
|
494
|
-
<form data-api="/api/publish" data-redirect="/configs">
|
|
495
|
-
${select('profileId', 'Deployment Profile', data.profiles.map((x) => [x.id, profileLabel(x, data)]))}
|
|
496
|
-
<button>Publish Active Version</button><p class="status"></p>
|
|
573
|
+
<section><h2>Profile Config</h2>
|
|
574
|
+
${profileConfigEditor(data, draft, redirect)}
|
|
575
|
+
<form data-api="/api/publish" data-redirect="${escapeHtml(redirect)}">
|
|
576
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
577
|
+
<button class="secondary">Publish Draft</button><p class="status"></p>
|
|
497
578
|
</form>
|
|
498
579
|
</section>
|
|
580
|
+
<section><h2>Container Keys</h2>${profileRuntimeKeyTable(data.runtimeKeys, data)}</section>
|
|
581
|
+
${pluginEditorScript(data.plugins)}
|
|
499
582
|
${formScript()}`;
|
|
500
583
|
}
|
|
501
|
-
function
|
|
502
|
-
return `<div class="page-head"><div><h1>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
<
|
|
507
|
-
${input('name', 'Name', true)}
|
|
508
|
-
${select('applicationId', 'Application', data.applications.map((x) => [x.id, x.name]))}
|
|
509
|
-
${select('groupId', 'Service Group', data.groups.map((x) => [x.id, groupLabel(x, data)]))}
|
|
510
|
-
${select('profileId', 'Deployment Profile', data.profiles.map((x) => [x.id, profileLabel(x, data)]))}
|
|
511
|
-
${input('containerName', 'Container Name')}
|
|
512
|
-
${input('configPluginId', 'Config Plugin', true, 'config-vault')}
|
|
513
|
-
</div>
|
|
514
|
-
<button>Create Runtime Key</button><p class="status"></p>
|
|
584
|
+
function pluginsPage(data, registry, query) {
|
|
585
|
+
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>
|
|
586
|
+
<section><h2>Registry Search</h2>
|
|
587
|
+
<form method="get" action="/plugins" class="inline-form">
|
|
588
|
+
${input('query', 'Search', false, query)}
|
|
589
|
+
<button>Search Registry</button>
|
|
515
590
|
</form>
|
|
591
|
+
${registry.length === 0 ? '<p class="muted">No registry results loaded.</p>' : registryTable(registry)}
|
|
516
592
|
</section>
|
|
517
|
-
<section><h2>
|
|
518
|
-
${formScript()}`;
|
|
519
|
-
}
|
|
520
|
-
function pluginsPage(data) {
|
|
521
|
-
return `<div class="page-head"><div><h1>Plugin Catalog</h1><p class="muted">Register public, private, or uploaded plugin schemas for config authoring.</p></div></div>
|
|
522
|
-
<section><h2>Create Plugin</h2>
|
|
593
|
+
<section><h2>Private Plugin</h2>
|
|
523
594
|
<form data-api="/api/plugins" data-redirect="/plugins">
|
|
524
595
|
<div class="form-grid">
|
|
525
596
|
${input('org', 'Org', true, '_')}
|
|
@@ -537,6 +608,27 @@ function pluginsPage(data) {
|
|
|
537
608
|
<section><h2>Catalog</h2>${table(data.plugins.map((x) => [x.pluginId, x.version, x.kind, x.source, x.packageName ?? '']))}</section>
|
|
538
609
|
${formScript()}`;
|
|
539
610
|
}
|
|
611
|
+
function registryTable(items) {
|
|
612
|
+
return `<table>${items.map((item) => `<tr>
|
|
613
|
+
<td>${escapeHtml(item.pluginId)}</td>
|
|
614
|
+
<td>${escapeHtml(item.version)}</td>
|
|
615
|
+
<td>${escapeHtml(item.kind)}</td>
|
|
616
|
+
<td>${escapeHtml(item.packageName ?? '')}</td>
|
|
617
|
+
<td>
|
|
618
|
+
<form data-api="/api/plugins/import" data-redirect="/plugins">
|
|
619
|
+
<input type="hidden" name="org" value="${escapeHtml(item.org)}">
|
|
620
|
+
<input type="hidden" name="name" value="${escapeHtml(item.name)}">
|
|
621
|
+
<input type="hidden" name="pluginId" value="${escapeHtml(item.pluginId)}">
|
|
622
|
+
<input type="hidden" name="packageName" value="${escapeHtml(item.packageName ?? '')}">
|
|
623
|
+
<input type="hidden" name="version" value="${escapeHtml(item.version)}">
|
|
624
|
+
<input type="hidden" name="kind" value="${escapeHtml(item.kind)}">
|
|
625
|
+
<input type="hidden" name="configSchema" value="${escapeHtml(JSON.stringify(item.configSchema ?? {}))}">
|
|
626
|
+
<input type="hidden" name="eventSchema" value="${escapeHtml(JSON.stringify(item.eventSchema ?? {}))}">
|
|
627
|
+
<button class="secondary">Import</button><p class="status"></p>
|
|
628
|
+
</form>
|
|
629
|
+
</td>
|
|
630
|
+
</tr>`).join('')}</table>`;
|
|
631
|
+
}
|
|
540
632
|
function profilePage(data) {
|
|
541
633
|
return `<div class="page-head"><div><h1>Profile</h1><p class="muted">Account security and admin authentication settings.</p></div></div>
|
|
542
634
|
<section><h2>Account</h2>${table([[data.user.email, data.user.createdAt]])}</section>
|
|
@@ -555,11 +647,15 @@ function applicationsTable(data) {
|
|
|
555
647
|
${input('description', 'Description', false, app.description ?? '')}
|
|
556
648
|
<button>Save</button><p class="status"></p>
|
|
557
649
|
</form>
|
|
558
|
-
</td><td class="actions"
|
|
650
|
+
</td><td class="actions">
|
|
651
|
+
<a class="button secondary" href="/deployments">Deployments</a>
|
|
652
|
+
${deleteForm('/api/applications/delete', app.id, '/applications', 'Delete application and related deployments, profiles, configs, and keys?')}
|
|
653
|
+
</td></tr>`).join('')}</table>`;
|
|
559
654
|
}
|
|
560
655
|
function groupsTable(data) {
|
|
561
656
|
if (data.groups.length === 0)
|
|
562
657
|
return '<p class="muted">None</p>';
|
|
658
|
+
const defaultProfileFor = (groupId) => data.profiles.find((profile) => profile.groupId === groupId && profile.name === 'default') ?? data.profiles.find((profile) => profile.groupId === groupId);
|
|
563
659
|
return `<table>${data.groups.map((group) => `<tr><td>
|
|
564
660
|
<form data-api="/api/groups/update" data-redirect="/deployments" class="inline-form">
|
|
565
661
|
<input type="hidden" name="id" value="${escapeHtml(group.id)}">
|
|
@@ -567,7 +663,10 @@ function groupsTable(data) {
|
|
|
567
663
|
${input('name', 'Name', true, group.name)}
|
|
568
664
|
<button>Save</button><p class="status"></p>
|
|
569
665
|
</form>
|
|
570
|
-
</td><td class="actions"
|
|
666
|
+
</td><td class="actions">
|
|
667
|
+
${defaultProfileFor(group.id) ? `<a class="button secondary" href="/deployment?profileId=${encodeURIComponent(defaultProfileFor(group.id).id)}">Open</a>` : ''}
|
|
668
|
+
${deleteForm('/api/groups/delete', group.id, '/deployments', 'Delete deployment and related profiles, configs, and keys?')}
|
|
669
|
+
</td></tr>`).join('')}</table>`;
|
|
571
670
|
}
|
|
572
671
|
function profilesTable(data) {
|
|
573
672
|
if (data.profiles.length === 0)
|
|
@@ -575,12 +674,15 @@ function profilesTable(data) {
|
|
|
575
674
|
return `<table>${data.profiles.map((profile) => `<tr><td>
|
|
576
675
|
<form data-api="/api/profiles/update" data-redirect="/deployments" class="inline-form">
|
|
577
676
|
<input type="hidden" name="id" value="${escapeHtml(profile.id)}">
|
|
578
|
-
${select('groupId', '
|
|
677
|
+
${select('groupId', 'Deployment', selectedOptions(data.groups.map((x) => [x.id, groupLabel(x, data)]), profile.groupId))}
|
|
579
678
|
${input('name', 'Name', true, profile.name)}
|
|
580
679
|
<span class="muted">${profile.activeVersionId ? 'published' : 'no published config'}</span>
|
|
581
680
|
<button>Save</button><p class="status"></p>
|
|
582
681
|
</form>
|
|
583
|
-
</td><td class="actions"
|
|
682
|
+
</td><td class="actions">
|
|
683
|
+
<a class="button secondary" href="/deployment?profileId=${encodeURIComponent(profile.id)}">Open</a>
|
|
684
|
+
${deleteForm('/api/profiles/delete', profile.id, '/deployments', 'Delete deployment profile and related configs and keys?')}
|
|
685
|
+
</td></tr>`).join('')}</table>`;
|
|
584
686
|
}
|
|
585
687
|
function deleteForm(action, id, redirect, confirm) {
|
|
586
688
|
return `<form data-api="${escapeHtml(action)}" data-redirect="${escapeHtml(redirect)}" data-confirm="${escapeHtml(confirm)}">
|
|
@@ -624,6 +726,183 @@ function runtimeKeyTable(keys, data) {
|
|
|
624
726
|
key.revokedAt ?? 'active',
|
|
625
727
|
]));
|
|
626
728
|
}
|
|
729
|
+
function profileConfigEditor(data, draft, redirect) {
|
|
730
|
+
return `
|
|
731
|
+
${addPluginForm(data, redirect)}
|
|
732
|
+
${configSectionEditor(data, draft, 'services', 'Services', redirect)}
|
|
733
|
+
${configSectionEditor(data, draft, 'events', 'Events', redirect)}
|
|
734
|
+
${configSectionEditor(data, draft, 'observable', 'Observable', redirect)}
|
|
735
|
+
`;
|
|
736
|
+
}
|
|
737
|
+
function addPluginForm(data, redirect) {
|
|
738
|
+
const configurablePlugins = data.plugins.filter((plugin) => plugin.kind !== 'config');
|
|
739
|
+
if (configurablePlugins.length === 0) {
|
|
740
|
+
return '<p class="muted">Import or create plugins in the Plugin Catalog before adding profile config.</p>';
|
|
741
|
+
}
|
|
742
|
+
return `<section><h3>Add Plugin</h3>
|
|
743
|
+
<form data-api="/api/profile-plugins" data-redirect="${escapeHtml(redirect)}" data-config-form>
|
|
744
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
745
|
+
<input type="hidden" name="plugin">
|
|
746
|
+
<input type="hidden" name="packageName">
|
|
747
|
+
<input type="hidden" name="version">
|
|
748
|
+
<input type="hidden" name="config">
|
|
749
|
+
<div class="form-grid">
|
|
750
|
+
${select('section', 'Type', [['services', 'Service'], ['events', 'Events'], ['observable', 'Observable']])}
|
|
751
|
+
<label>Plugin<select name="catalogId" data-plugin-picker required>${configurablePlugins.map((plugin) => `<option value="${escapeHtml(plugin.id)}">${escapeHtml(plugin.pluginId)} ${escapeHtml(plugin.version)}</option>`).join('')}</select></label>
|
|
752
|
+
${input('name', 'Config Name', true)}
|
|
753
|
+
<label>Enabled<select name="enabled"><option value="true">Enabled</option><option value="false">Disabled</option></select></label>
|
|
754
|
+
</div>
|
|
755
|
+
<div data-config-fields></div>
|
|
756
|
+
<button>Add Plugin</button><p class="status"></p>
|
|
757
|
+
</form>
|
|
758
|
+
</section>`;
|
|
759
|
+
}
|
|
760
|
+
function configSectionEditor(data, draft, section, title, redirect) {
|
|
761
|
+
const entries = Object.entries(draft[section] ?? {});
|
|
762
|
+
return `<section><h3>${escapeHtml(title)}</h3>
|
|
763
|
+
${entries.length === 0 ? '<p class="muted">No plugins configured.</p>' : entries.map(([name, entry]) => {
|
|
764
|
+
const catalog = findCatalogPlugin(data, entry.plugin, entry.version, entry.package);
|
|
765
|
+
return `<div style="border-top:1px solid var(--line);padding-top:12px;margin-top:12px">
|
|
766
|
+
<form data-api="/api/profile-plugins" data-redirect="${escapeHtml(redirect)}" data-config-form>
|
|
767
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
768
|
+
<input type="hidden" name="section" value="${escapeHtml(section)}">
|
|
769
|
+
<input type="hidden" name="name" value="${escapeHtml(name)}">
|
|
770
|
+
<input type="hidden" name="plugin" value="${escapeHtml(entry.plugin)}">
|
|
771
|
+
<input type="hidden" name="packageName" value="${escapeHtml(entry.package ?? '')}">
|
|
772
|
+
<input type="hidden" name="version" value="${escapeHtml(entry.version ?? '')}">
|
|
773
|
+
<input type="hidden" name="config">
|
|
774
|
+
<div class="form-grid">
|
|
775
|
+
${input('displayName', 'Config Name', false, name).replace('name="displayName"', 'name="displayName" disabled')}
|
|
776
|
+
${input('pluginDisplay', 'Plugin', false, entry.plugin).replace('name="pluginDisplay"', 'name="pluginDisplay" disabled')}
|
|
777
|
+
<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>
|
|
778
|
+
</div>
|
|
779
|
+
<div data-config-fields>${renderSchemaFields(catalog?.configSchema, entry.config ?? {})}</div>
|
|
780
|
+
<button>Save</button><p class="status"></p>
|
|
781
|
+
</form>
|
|
782
|
+
<form data-api="/api/profile-plugins/delete" data-redirect="${escapeHtml(redirect)}" data-confirm="Remove this plugin from the profile?">
|
|
783
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
784
|
+
<input type="hidden" name="section" value="${escapeHtml(section)}">
|
|
785
|
+
<input type="hidden" name="name" value="${escapeHtml(name)}">
|
|
786
|
+
<button class="secondary">Remove</button><p class="status"></p>
|
|
787
|
+
</form>
|
|
788
|
+
</div>`;
|
|
789
|
+
}).join('')}
|
|
790
|
+
</section>`;
|
|
791
|
+
}
|
|
792
|
+
function findCatalogPlugin(data, pluginId, version, packageName) {
|
|
793
|
+
return data.plugins.find((plugin) => plugin.pluginId === pluginId &&
|
|
794
|
+
(version ? plugin.version === version : true) &&
|
|
795
|
+
(packageName ? plugin.packageName === packageName : true)) ?? data.plugins.find((plugin) => plugin.pluginId === pluginId);
|
|
796
|
+
}
|
|
797
|
+
function renderSchemaFields(schema, config) {
|
|
798
|
+
const root = objectField(objectField(schema)?.root);
|
|
799
|
+
if (!root || root.kind !== 'object' || !objectField(root.properties)) {
|
|
800
|
+
return '<p class="muted">No config schema available for this plugin.</p>';
|
|
801
|
+
}
|
|
802
|
+
return renderProperties(root.properties, config, '');
|
|
803
|
+
}
|
|
804
|
+
function renderProperties(properties, config, prefix) {
|
|
805
|
+
return Object.entries(properties).map(([key, schema]) => {
|
|
806
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
807
|
+
const value = prefix ? objectField(config)?.[key] : valueAtPath(config, path);
|
|
808
|
+
return renderSchemaControl(key, objectField(schema), path, value);
|
|
809
|
+
}).join('');
|
|
810
|
+
}
|
|
811
|
+
function renderSchemaControl(key, rawNode, path, rawValue) {
|
|
812
|
+
const node = unwrapSchema(rawNode);
|
|
813
|
+
if (!node)
|
|
814
|
+
return '';
|
|
815
|
+
const value = rawValue ?? node.default ?? '';
|
|
816
|
+
const label = schemaLabel(key, node);
|
|
817
|
+
if (node.kind === 'object' && objectField(node.properties)) {
|
|
818
|
+
return `<fieldset class="schema-box"><legend>${escapeHtml(key)}</legend>${renderProperties(node.properties, isRecord(value) ? value : {}, path)}</fieldset>`;
|
|
819
|
+
}
|
|
820
|
+
if (node.kind === 'bool' || node.kind === 'boolean') {
|
|
821
|
+
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>`;
|
|
822
|
+
}
|
|
823
|
+
if (node.kind === 'enum' && Array.isArray(node.values)) {
|
|
824
|
+
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>`;
|
|
825
|
+
}
|
|
826
|
+
if (node.kind === 'array') {
|
|
827
|
+
const itemNode = unwrapSchema(objectField(node.items) ?? objectField(node.item));
|
|
828
|
+
const kind = inputKind(itemNode);
|
|
829
|
+
const values = Array.isArray(value) ? value : [];
|
|
830
|
+
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('');
|
|
831
|
+
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>`;
|
|
832
|
+
}
|
|
833
|
+
if (node.kind === 'record') {
|
|
834
|
+
const valueNode = unwrapSchema(objectField(node.valueSchema) ?? objectField(node.values) ?? objectField(node.value));
|
|
835
|
+
const kind = inputKind(valueNode);
|
|
836
|
+
const entries = isRecord(value) ? Object.entries(value) : [];
|
|
837
|
+
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('');
|
|
838
|
+
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>`;
|
|
839
|
+
}
|
|
840
|
+
if (node.kind === 'tuple') {
|
|
841
|
+
const items = (Array.isArray(node.items) ? node.items : Array.isArray(node.elements) ? node.elements : []).map((item) => objectField(item));
|
|
842
|
+
const values = Array.isArray(value) ? value : [];
|
|
843
|
+
return `<fieldset class="schema-box" data-tuple-path="${escapeHtml(path)}"><legend>${escapeHtml(label)}</legend>${items.map((item, index) => {
|
|
844
|
+
const child = unwrapSchema(item);
|
|
845
|
+
const kind = inputKind(child);
|
|
846
|
+
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>`;
|
|
847
|
+
}).join('')}</fieldset>`;
|
|
848
|
+
}
|
|
849
|
+
if (node.kind === 'union' && Array.isArray(node.variants)) {
|
|
850
|
+
const variant = unwrapSchema(objectField(node.variants[0]));
|
|
851
|
+
return renderSchemaControl(label, variant, path, value);
|
|
852
|
+
}
|
|
853
|
+
const kind = inputKind(node);
|
|
854
|
+
return `<label>${escapeHtml(label)}<input data-config-path="${escapeHtml(path)}" data-kind="${escapeHtml(kind)}" ${kind === 'number' ? 'type="number"' : ''} value="${escapeHtml(String(value ?? ''))}"></label>`;
|
|
855
|
+
}
|
|
856
|
+
function schemaLabel(key, node) {
|
|
857
|
+
return node.metadata && typeof node.metadata === 'object' && 'description' in node.metadata
|
|
858
|
+
? String(node.metadata.description)
|
|
859
|
+
: key;
|
|
860
|
+
}
|
|
861
|
+
function inputKind(node) {
|
|
862
|
+
if (!node)
|
|
863
|
+
return 'string';
|
|
864
|
+
if (node.kind === 'bool' || node.kind === 'boolean')
|
|
865
|
+
return 'bool';
|
|
866
|
+
return ['int', 'int32', 'int64', 'number', 'float', 'float32', 'float64'].includes(String(node.kind)) ? 'number' : 'string';
|
|
867
|
+
}
|
|
868
|
+
function isRecord(value) {
|
|
869
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
870
|
+
}
|
|
871
|
+
function unwrapSchema(node) {
|
|
872
|
+
let current = node;
|
|
873
|
+
while (current && (current.kind === 'optional' || current.kind === 'nullable')) {
|
|
874
|
+
current = objectField(current.inner);
|
|
875
|
+
}
|
|
876
|
+
return current;
|
|
877
|
+
}
|
|
878
|
+
function valueAtPath(source, path) {
|
|
879
|
+
return path.split('.').reduce((acc, part) => objectField(acc)?.[part], source);
|
|
880
|
+
}
|
|
881
|
+
function profileRuntimeKeyTable(keys, data) {
|
|
882
|
+
if (keys.length === 0)
|
|
883
|
+
return '<p class="muted">No container keys created for this profile.</p>';
|
|
884
|
+
return `<table>${keys.map((key) => `<tr>
|
|
885
|
+
<td>${escapeHtml(key.name)}</td>
|
|
886
|
+
<td>${escapeHtml(key.id)}</td>
|
|
887
|
+
<td>${escapeHtml(data.profile.name)}</td>
|
|
888
|
+
<td>${escapeHtml(key.containerName ?? '')}</td>
|
|
889
|
+
<td>${escapeHtml(key.revokedAt ?? 'active')}</td>
|
|
890
|
+
<td>${key.revokedAt ? '' : `<form data-api="/api/runtime-keys/rotate" data-secret-redirect="/deployment?profileId=${escapeHtml(data.profile.id)}"><input type="hidden" name="keyId" value="${escapeHtml(key.id)}"><button class="secondary">Rotate</button><p class="status"></p></form>`}</td>
|
|
891
|
+
</tr>`).join('')}</table>`;
|
|
892
|
+
}
|
|
893
|
+
function runtimeEnvBlock(credential) {
|
|
894
|
+
const env = [
|
|
895
|
+
'BSB_CONFIG_PLUGIN=config-vault',
|
|
896
|
+
'BSB_CONFIG_PLUGIN_PACKAGE=@bsb/config-vault',
|
|
897
|
+
`vaultUrl=${credential.publicUrl}`,
|
|
898
|
+
`apiKeyId=${credential.keyId}`,
|
|
899
|
+
`apiSecret=${credential.secret}`,
|
|
900
|
+
].join('\n');
|
|
901
|
+
return `<section><h2>Container Environment</h2>
|
|
902
|
+
<p class="muted">Shown once. Add these env vars to the BSB container that should load this deployment profile.</p>
|
|
903
|
+
<pre class="code"><code>${escapeHtml(env)}</code></pre>
|
|
904
|
+
</section>`;
|
|
905
|
+
}
|
|
627
906
|
function shortId(value) {
|
|
628
907
|
return value.length > 18 ? `${value.slice(0, 10)}...${value.slice(-6)}` : value;
|
|
629
908
|
}
|
|
@@ -650,7 +929,10 @@ function formScript() {
|
|
|
650
929
|
const result = await res.json();
|
|
651
930
|
if (!res.ok) throw new Error(result.message || 'Save failed');
|
|
652
931
|
if (form.dataset.secretRedirect && result.secret) {
|
|
653
|
-
|
|
932
|
+
const joiner = form.dataset.secretRedirect.includes('?') ? '&' : '?';
|
|
933
|
+
location.href = form.dataset.secretRedirect
|
|
934
|
+
+ joiner + 'keyId=' + encodeURIComponent(result.keyId || result.id || '')
|
|
935
|
+
+ '&secret=' + encodeURIComponent(result.secret);
|
|
654
936
|
return;
|
|
655
937
|
}
|
|
656
938
|
location.href = form.dataset.redirect || location.pathname;
|
|
@@ -664,6 +946,164 @@ function formScript() {
|
|
|
664
946
|
});
|
|
665
947
|
</script>`;
|
|
666
948
|
}
|
|
949
|
+
function pluginEditorScript(plugins) {
|
|
950
|
+
const catalog = plugins.reduce((acc, plugin) => {
|
|
951
|
+
acc[plugin.id] = {
|
|
952
|
+
plugin: plugin.pluginId,
|
|
953
|
+
packageName: plugin.packageName ?? '',
|
|
954
|
+
version: plugin.version,
|
|
955
|
+
kind: plugin.kind,
|
|
956
|
+
schema: plugin.configSchema ?? null,
|
|
957
|
+
};
|
|
958
|
+
return acc;
|
|
959
|
+
}, {});
|
|
960
|
+
return `<script>
|
|
961
|
+
const vaultPluginCatalog = ${jsonForScript(catalog)};
|
|
962
|
+
function setPath(target, path, value) {
|
|
963
|
+
const parts = path.split('.');
|
|
964
|
+
let current = target;
|
|
965
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
966
|
+
current[parts[i]] = current[parts[i]] || {};
|
|
967
|
+
current = current[parts[i]];
|
|
968
|
+
}
|
|
969
|
+
current[parts[parts.length - 1]] = value;
|
|
970
|
+
}
|
|
971
|
+
function readConfigForm(form) {
|
|
972
|
+
const config = {};
|
|
973
|
+
form.querySelectorAll('[data-array-path]').forEach((group) => {
|
|
974
|
+
const values = Array.from(group.querySelectorAll('[data-array-item]'))
|
|
975
|
+
.map((field) => parseFieldValue(field))
|
|
976
|
+
.filter((value) => value !== undefined);
|
|
977
|
+
if (values.length > 0) setPath(config, group.dataset.arrayPath, values);
|
|
978
|
+
});
|
|
979
|
+
form.querySelectorAll('[data-record-path]').forEach((group) => {
|
|
980
|
+
const record = {};
|
|
981
|
+
group.querySelectorAll('.repeat-row').forEach((row) => {
|
|
982
|
+
const key = row.querySelector('[data-record-key]')?.value?.trim();
|
|
983
|
+
const valueField = row.querySelector('[data-record-value]');
|
|
984
|
+
const value = valueField ? parseFieldValue(valueField) : undefined;
|
|
985
|
+
if (key && value !== undefined) record[key] = value;
|
|
986
|
+
});
|
|
987
|
+
if (Object.keys(record).length > 0) setPath(config, group.dataset.recordPath, record);
|
|
988
|
+
});
|
|
989
|
+
form.querySelectorAll('[data-tuple-path]').forEach((group) => {
|
|
990
|
+
const values = [];
|
|
991
|
+
group.querySelectorAll('[data-tuple-index]').forEach((field) => {
|
|
992
|
+
const value = parseFieldValue(field);
|
|
993
|
+
if (value !== undefined) values[Number(field.dataset.tupleIndex)] = value;
|
|
994
|
+
});
|
|
995
|
+
if (values.length > 0) setPath(config, group.dataset.tuplePath, values);
|
|
996
|
+
});
|
|
997
|
+
form.querySelectorAll('[data-config-path]').forEach((field) => {
|
|
998
|
+
if (field.closest('[data-array-path],[data-record-path],[data-tuple-path]')) return;
|
|
999
|
+
const value = parseFieldValue(field);
|
|
1000
|
+
if (value !== undefined) setPath(config, field.dataset.configPath, value);
|
|
1001
|
+
});
|
|
1002
|
+
return config;
|
|
1003
|
+
}
|
|
1004
|
+
function parseFieldValue(field) {
|
|
1005
|
+
const raw = field.value;
|
|
1006
|
+
if (raw === '') return undefined;
|
|
1007
|
+
if (field.dataset.kind === 'number') return Number(raw);
|
|
1008
|
+
if (field.dataset.kind === 'bool') return raw === 'true';
|
|
1009
|
+
return raw;
|
|
1010
|
+
}
|
|
1011
|
+
function schemaRoot(schema) {
|
|
1012
|
+
return schema && schema.root && schema.root.kind === 'object' ? schema.root : null;
|
|
1013
|
+
}
|
|
1014
|
+
function escapeClient(value) {
|
|
1015
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
1016
|
+
}
|
|
1017
|
+
function unwrapNode(node) {
|
|
1018
|
+
while (node && (node.kind === 'optional' || node.kind === 'nullable')) node = node.inner;
|
|
1019
|
+
return node;
|
|
1020
|
+
}
|
|
1021
|
+
function inputKind(node) {
|
|
1022
|
+
node = unwrapNode(node);
|
|
1023
|
+
if (!node) return 'string';
|
|
1024
|
+
if (node.kind === 'bool' || node.kind === 'boolean') return 'bool';
|
|
1025
|
+
return ['int','int32','int64','number','float','float32','float64'].includes(String(node.kind)) ? 'number' : 'string';
|
|
1026
|
+
}
|
|
1027
|
+
function primitiveInput(attrs, kind, value) {
|
|
1028
|
+
return '<input ' + attrs + ' data-kind="' + kind + '"' + (kind === 'number' ? ' type="number"' : '') + ' value="' + escapeClient(value || '') + '">';
|
|
1029
|
+
}
|
|
1030
|
+
function repeatRow(kind, key, value) {
|
|
1031
|
+
return '<div class="repeat-row">'
|
|
1032
|
+
+ (key === null ? '' : '<input data-record-key placeholder="Key" value="' + escapeClient(key || '') + '">')
|
|
1033
|
+
+ primitiveInput(key === null ? 'data-array-item' : 'data-record-value', kind, value)
|
|
1034
|
+
+ '<button type="button" class="secondary" data-remove-row>Remove</button></div>';
|
|
1035
|
+
}
|
|
1036
|
+
function renderFields(properties, prefix) {
|
|
1037
|
+
return Object.entries(properties || {}).map(([key, rawNode]) => {
|
|
1038
|
+
let node = unwrapNode(rawNode);
|
|
1039
|
+
if (!node) return '';
|
|
1040
|
+
const path = prefix ? prefix + '.' + key : key;
|
|
1041
|
+
const label = (node.metadata && node.metadata.description) || key;
|
|
1042
|
+
if (node.kind === 'object' && node.properties) {
|
|
1043
|
+
return '<fieldset class="schema-box"><legend>' + escapeClient(key) + '</legend>' + renderFields(node.properties, path) + '</fieldset>';
|
|
1044
|
+
}
|
|
1045
|
+
if (node.kind === 'bool' || node.kind === 'boolean') {
|
|
1046
|
+
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>';
|
|
1047
|
+
}
|
|
1048
|
+
if (node.kind === 'enum' && Array.isArray(node.values)) {
|
|
1049
|
+
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>';
|
|
1050
|
+
}
|
|
1051
|
+
if (node.kind === 'array') {
|
|
1052
|
+
const kind = inputKind(node.items || node.item);
|
|
1053
|
+
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>';
|
|
1054
|
+
}
|
|
1055
|
+
if (node.kind === 'record') {
|
|
1056
|
+
const kind = inputKind(node.valueSchema || node.values || node.value);
|
|
1057
|
+
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>';
|
|
1058
|
+
}
|
|
1059
|
+
if (node.kind === 'tuple') {
|
|
1060
|
+
const items = Array.isArray(node.items) ? node.items : Array.isArray(node.elements) ? node.elements : [];
|
|
1061
|
+
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>';
|
|
1062
|
+
}
|
|
1063
|
+
if (node.kind === 'union' && Array.isArray(node.variants) && node.variants[0]) {
|
|
1064
|
+
return renderFields({ [key]: node.variants[0] }, prefix);
|
|
1065
|
+
}
|
|
1066
|
+
const kind = inputKind(node);
|
|
1067
|
+
return '<label>' + escapeClient(label) + primitiveInput('data-config-path="' + escapeClient(path) + '"', kind, '') + '</label>';
|
|
1068
|
+
}).join('');
|
|
1069
|
+
}
|
|
1070
|
+
document.addEventListener('click', (event) => {
|
|
1071
|
+
const button = event.target.closest('[data-add-array-item],[data-add-record-row],[data-remove-row]');
|
|
1072
|
+
if (!button) return;
|
|
1073
|
+
if (button.matches('[data-remove-row]')) {
|
|
1074
|
+
button.closest('.repeat-row')?.remove();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const group = button.closest('.schema-repeat');
|
|
1078
|
+
const rows = group?.querySelector('[data-repeat-rows]');
|
|
1079
|
+
if (!group || !rows) return;
|
|
1080
|
+
if (button.matches('[data-add-array-item]')) rows.insertAdjacentHTML('beforeend', repeatRow(group.dataset.itemKind || 'string', null, ''));
|
|
1081
|
+
if (button.matches('[data-add-record-row]')) rows.insertAdjacentHTML('beforeend', repeatRow(group.dataset.valueKind || 'string', '', ''));
|
|
1082
|
+
});
|
|
1083
|
+
document.querySelectorAll('[data-plugin-picker]').forEach((select) => {
|
|
1084
|
+
const form = select.closest('form');
|
|
1085
|
+
const sync = () => {
|
|
1086
|
+
const item = vaultPluginCatalog[select.value];
|
|
1087
|
+
if (!item || !form) return;
|
|
1088
|
+
form.elements.plugin.value = item.plugin || '';
|
|
1089
|
+
form.elements.packageName.value = item.packageName || '';
|
|
1090
|
+
form.elements.version.value = item.version || '';
|
|
1091
|
+
if (form.elements.section && item.kind) form.elements.section.value = item.kind === 'service' ? 'services' : item.kind;
|
|
1092
|
+
if (form.elements.name && !form.elements.name.value) form.elements.name.value = item.plugin || '';
|
|
1093
|
+
const root = schemaRoot(item.schema);
|
|
1094
|
+
const fields = form.querySelector('[data-config-fields]');
|
|
1095
|
+
if (fields) fields.innerHTML = root ? renderFields(root.properties, '') : '<p class="muted">No config schema available for this plugin.</p>';
|
|
1096
|
+
};
|
|
1097
|
+
select.addEventListener('change', sync);
|
|
1098
|
+
sync();
|
|
1099
|
+
});
|
|
1100
|
+
document.querySelectorAll('form[data-config-form]').forEach((form) => {
|
|
1101
|
+
form.addEventListener('submit', () => {
|
|
1102
|
+
if (form.elements.config) form.elements.config.value = JSON.stringify(readConfigForm(form));
|
|
1103
|
+
}, { capture: true });
|
|
1104
|
+
});
|
|
1105
|
+
</script>`;
|
|
1106
|
+
}
|
|
667
1107
|
function webauthnClientScript() {
|
|
668
1108
|
return `
|
|
669
1109
|
function base64UrlToBuffer(value) {
|
|
@@ -717,29 +1157,63 @@ function webauthnClientScript() {
|
|
|
717
1157
|
}
|
|
718
1158
|
`;
|
|
719
1159
|
}
|
|
720
|
-
function apiForm(action, fields, textarea) {
|
|
721
|
-
return `<form data-api="${action}" onsubmit="return submitJson(this)">
|
|
722
|
-
${fields.map((field) => `<input name="${field}" placeholder="${field}" ${field === 'configPluginId' ? 'value="config-vault"' : ''}>`).join('')}
|
|
723
|
-
${textarea ? `<textarea name="${textarea}" placeholder="${textarea} JSON"></textarea>` : ''}
|
|
724
|
-
<button>Submit</button>
|
|
725
|
-
</form>
|
|
726
|
-
<script>
|
|
727
|
-
async function submitJson(form){
|
|
728
|
-
const data={}; for(const item of new FormData(form).entries()){data[item[0]]=item[1]}
|
|
729
|
-
const csrf=document.cookie.split('; ').find(x=>x.startsWith('vault_csrf='))?.split('=')[1]||'';
|
|
730
|
-
const res=await fetch(form.dataset.api,{method:'POST',headers:{'content-type':'application/json','x-csrf-token':csrf},body:JSON.stringify(data)});
|
|
731
|
-
alert(JSON.stringify(await res.json(),null,2)); location.reload(); return false;
|
|
732
|
-
}
|
|
733
|
-
</script>`;
|
|
734
|
-
}
|
|
735
|
-
function pluginForm() {
|
|
736
|
-
return apiForm('/api/plugins', ['org', 'name', 'pluginId', 'packageName', 'version', 'kind', 'source'], 'configSchema');
|
|
737
|
-
}
|
|
738
1160
|
function table(rows) {
|
|
739
1161
|
if (rows.length === 0)
|
|
740
1162
|
return '<p class="muted">None</p>';
|
|
741
1163
|
return `<table>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</table>`;
|
|
742
1164
|
}
|
|
1165
|
+
async function registrySearch(registryUrl, query) {
|
|
1166
|
+
const url = new URL('/plugins', registryUrl);
|
|
1167
|
+
url.searchParams.set('language', 'nodejs');
|
|
1168
|
+
url.searchParams.set('limit', '20');
|
|
1169
|
+
if (query.trim())
|
|
1170
|
+
url.searchParams.set('query', query.trim());
|
|
1171
|
+
try {
|
|
1172
|
+
const response = await fetch(url, { headers: { accept: 'application/json' } });
|
|
1173
|
+
if (!response.ok)
|
|
1174
|
+
return [];
|
|
1175
|
+
const parsed = await response.json();
|
|
1176
|
+
return (Array.isArray(parsed.plugins) ? parsed.plugins : [])
|
|
1177
|
+
.map(normalizeRegistryCandidate)
|
|
1178
|
+
.filter((item) => item !== null);
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return [];
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function normalizeRegistryCandidate(input) {
|
|
1185
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input))
|
|
1186
|
+
return null;
|
|
1187
|
+
const value = input;
|
|
1188
|
+
const org = stringField(value.org) ?? orgFromPackage(value.packageName ?? value.package) ?? '_';
|
|
1189
|
+
const name = stringField(value.name) ?? stringField(value.pluginId) ?? stringField(value.id);
|
|
1190
|
+
if (!name)
|
|
1191
|
+
return null;
|
|
1192
|
+
const packageName = stringField(value.packageName) ?? stringField(value.package) ?? null;
|
|
1193
|
+
const pluginId = stringField(value.pluginId) ?? stringField(value.id) ?? name;
|
|
1194
|
+
return {
|
|
1195
|
+
org,
|
|
1196
|
+
name,
|
|
1197
|
+
pluginId,
|
|
1198
|
+
packageName,
|
|
1199
|
+
version: stringField(value.version) ?? '0.0.0',
|
|
1200
|
+
kind: parseKind(value.kind ?? value.category ?? value.type),
|
|
1201
|
+
configSchema: objectField(value.configSchema) ?? objectField(value.schema) ?? objectField(value.validationSchema) ?? null,
|
|
1202
|
+
eventSchema: objectField(value.eventSchema) ?? objectField(value.events) ?? null,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function stringField(value) {
|
|
1206
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1207
|
+
}
|
|
1208
|
+
function objectField(value) {
|
|
1209
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
|
|
1210
|
+
}
|
|
1211
|
+
function orgFromPackage(value) {
|
|
1212
|
+
if (typeof value !== 'string' || !value.startsWith('@'))
|
|
1213
|
+
return undefined;
|
|
1214
|
+
const [org] = value.split('/');
|
|
1215
|
+
return org || undefined;
|
|
1216
|
+
}
|
|
743
1217
|
function parseJsonObject(value) {
|
|
744
1218
|
if (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
745
1219
|
return value;
|
|
@@ -753,6 +1227,9 @@ function parseJsonObject(value) {
|
|
|
753
1227
|
function parseKind(value) {
|
|
754
1228
|
return value === 'events' || value === 'observable' || value === 'config' ? value : 'service';
|
|
755
1229
|
}
|
|
1230
|
+
function parseConfigSection(value) {
|
|
1231
|
+
return value === 'events' || value === 'observable' ? value : 'services';
|
|
1232
|
+
}
|
|
756
1233
|
function parseSource(value) {
|
|
757
1234
|
return value === 'registry' || value === 'upload' ? value : 'manual';
|
|
758
1235
|
}
|
|
@@ -768,4 +1245,7 @@ function escapeHtml(value) {
|
|
|
768
1245
|
"'": ''',
|
|
769
1246
|
}[char] ?? char));
|
|
770
1247
|
}
|
|
1248
|
+
function jsonForScript(value) {
|
|
1249
|
+
return JSON.stringify(value).replace(/<\//g, '<\\/');
|
|
1250
|
+
}
|
|
771
1251
|
//# sourceMappingURL=http-server.js.map
|