@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.
@@ -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.createGroup(user.userId, String(body.applicationId ?? ''), String(body.name ?? ''));
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.saveDraft(user.userId, String(body.profileId ?? ''), config);
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.createRuntimeKey(user.userId, {
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
- return this.page('Configs', configsPage(dashboard), 'configs');
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
- return this.page('Runtime Keys', runtimeKeysPage(dashboard, String(query.secret ?? '')), 'runtime-keys');
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
- return this.page('Plugins', pluginsPage(dashboard), 'plugins');
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('Service Groups', data.groups.length)}
512
+ ${metric('Deployments', data.groups.length)}
443
513
  ${metric('Deployment Profiles', data.profiles.length)}
444
- ${metric('Runtime Keys', data.runtimeKeys.length)}
514
+ ${metric('Container Keys', data.runtimeKeys.length)}
445
515
  </div>
446
- <section><h2>Recent Runtime Keys</h2>${runtimeKeyTable(data.runtimeKeys.slice(0, 8), data)}</section>`;
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">Model service groups and deployment profiles for containers.</p></div></div>
464
- <div class="grid">
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', 'Group Name', true)}
469
- <button>Create Group</button><p class="status"></p>
537
+ ${input('name', 'Deployment Name', true)}
538
+ <button>Create Deployment</button><p class="status"></p>
470
539
  </form>
471
540
  </section>
472
- <section><h2>Create Deployment Profile</h2>
473
- <form data-api="/api/profiles" data-redirect="/deployments">
474
- ${select('groupId', 'Service Group', data.groups.map((x) => [x.id, groupLabel(x, data)]))}
475
- ${input('name', 'Profile Name', true, 'default')}
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>Service Groups</h2>${groupsTable(data)}</section>
481
- <section><h2>Deployment Profiles</h2>${profilesTable(data)}</section>
482
- ${formScript()}`;
483
- }
484
- function configsPage(data) {
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 runtimeKeysPage(data, secret) {
502
- return `<div class="page-head"><div><h1>Runtime Keys</h1><p class="muted">Bind a container credential to one application, service group, deployment profile, and config plugin.</p></div></div>
503
- ${secret ? `<section><h2>Runtime Secret</h2><p class="muted">Shown once. Store it in the container environment now.</p><p class="code"><code>${escapeHtml(secret)}</code></p></section>` : ''}
504
- <section><h2>Create Runtime Key</h2>
505
- <form data-api="/api/runtime-keys" data-secret-redirect="/runtime-keys">
506
- <div class="form-grid">
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>Runtime Keys</h2>${runtimeKeyTable(data.runtimeKeys, data)}</section>
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">${deleteForm('/api/applications/delete', app.id, '/applications', 'Delete application and related groups, profiles, configs, and keys?')}</td></tr>`).join('')}</table>`;
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">${deleteForm('/api/groups/delete', group.id, '/deployments', 'Delete group and related profiles, configs, and keys?')}</td></tr>`).join('')}</table>`;
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', 'Service Group', selectedOptions(data.groups.map((x) => [x.id, groupLabel(x, data)]), profile.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">${deleteForm('/api/profiles/delete', profile.id, '/deployments', 'Delete deployment profile and related configs and keys?')}</td></tr>`).join('')}</table>`;
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
- location.href = form.dataset.secretRedirect + '?secret=' + encodeURIComponent(result.secret);
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
  "'": '&#39;',
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