@bsb/config-vault 9.6.9 → 9.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -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 +221 -81
- 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 +26 -0
- package/lib/plugins/service-config-vault/store.js.map +1 -1
- package/lib/plugins/service-config-vault/vault.d.ts +30 -1
- package/lib/plugins/service-config-vault/vault.d.ts.map +1 -1
- package/lib/plugins/service-config-vault/vault.js +61 -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
package/README.md
CHANGED
|
@@ -11,15 +11,14 @@ Runtime containers do not choose applications, groups, profiles, or versions. Th
|
|
|
11
11
|
|
|
12
12
|
## Runtime
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
apiSecret: vs_xxx
|
|
14
|
+
Runtime containers activate Vault as the BSB config plugin with env vars:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
BSB_CONFIG_PLUGIN=config-vault
|
|
18
|
+
BSB_CONFIG_PLUGIN_PACKAGE=@bsb/config-vault
|
|
19
|
+
vaultUrl=https://vault.example.com
|
|
20
|
+
apiKeyId=vk_xxx
|
|
21
|
+
apiSecret=vs_xxx
|
|
23
22
|
```
|
|
24
23
|
|
|
25
24
|
When a container restarts, it pulls the active published version for the API key's bound deployment profile.
|
|
@@ -38,6 +37,7 @@ service-config-vault:
|
|
|
38
37
|
production: true
|
|
39
38
|
databaseUrl: postgres://vault:secret@postgres:5432/vault
|
|
40
39
|
masterKey: BASE64_32_BYTE_KEY
|
|
40
|
+
registryUrl: https://io.bsbcode.dev
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
`masterKey` must be a base64 encoded 32-byte key. Generate one with:
|
|
@@ -58,4 +58,16 @@ After enrollment, every admin login requires password, TOTP, and a browser passk
|
|
|
58
58
|
|
|
59
59
|
## Admin UI
|
|
60
60
|
|
|
61
|
-
Vault has
|
|
61
|
+
Vault has pages for Overview, Applications, Deployments, Plugins, and Profile. Deployment profiles own config drafts, publishing, and container key create/rotate flows.
|
|
62
|
+
|
|
63
|
+
When editing a profile config, enter only the profile body:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"observable": {},
|
|
68
|
+
"events": {},
|
|
69
|
+
"services": {}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Vault wraps that body under the profile name internally. Container keys are generated from the deployment profile page and the UI shows the BSB container env vars once on creation or rotation.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-config-vault/http-server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,UAAU,CAAC;IAChB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,gBAAgB;IAI/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-config-vault/http-server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,UAAU,CAAC;IAChB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,gBAAgB;IAI/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyTtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAUb,WAAW;YAUX,gBAAgB;IAY9B,OAAO,CAAC,IAAI;CAGb"}
|
|
@@ -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,21 @@ 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/runtime-keys/rotate', defineEventHandler(async (event) => {
|
|
189
|
+
const user = await this.requireUser(event);
|
|
190
|
+
const body = await readBody(event);
|
|
191
|
+
return this.options.vault.rotateProfileRuntimeKey(user.userId, {
|
|
192
|
+
keyId: String(body.keyId ?? ''),
|
|
193
|
+
name: stringOrUndefined(body.name),
|
|
194
|
+
});
|
|
195
|
+
}));
|
|
173
196
|
app.use('/api/runtime-keys', defineEventHandler(async (event) => {
|
|
174
197
|
const user = await this.requireUser(event);
|
|
175
198
|
const body = await readBody(event);
|
|
176
|
-
return this.options.vault.
|
|
199
|
+
return this.options.vault.createProfileRuntimeKey(user.userId, {
|
|
177
200
|
name: String(body.name ?? ''),
|
|
178
|
-
applicationId: String(body.applicationId ?? ''),
|
|
179
|
-
groupId: String(body.groupId ?? ''),
|
|
180
201
|
profileId: String(body.profileId ?? ''),
|
|
181
202
|
containerName: body.containerName === undefined ? null : String(body.containerName),
|
|
182
|
-
configPluginId: String(body.configPluginId ?? 'config-vault'),
|
|
183
203
|
});
|
|
184
204
|
}));
|
|
185
205
|
app.use('/applications', defineEventHandler(async (event) => {
|
|
@@ -192,21 +212,45 @@ export class VaultHttpServer {
|
|
|
192
212
|
const dashboard = await this.options.vault.dashboard();
|
|
193
213
|
return this.page('Deployments', deploymentsPage(dashboard), 'deployments');
|
|
194
214
|
}));
|
|
215
|
+
app.use('/deployment', defineEventHandler(async (event) => {
|
|
216
|
+
await this.requireUser(event);
|
|
217
|
+
const query = getQuery(event);
|
|
218
|
+
const profileId = String(query.profileId ?? '');
|
|
219
|
+
if (!profileId)
|
|
220
|
+
return sendRedirect(event, '/deployments');
|
|
221
|
+
const profile = await this.options.vault.deploymentProfile(profileId);
|
|
222
|
+
return this.page('Deployment', deploymentDetailPage(profile, {
|
|
223
|
+
publicUrl: this.options.publicUrl,
|
|
224
|
+
keyId: String(query.keyId ?? ''),
|
|
225
|
+
secret: String(query.secret ?? ''),
|
|
226
|
+
}), 'deployments');
|
|
227
|
+
}));
|
|
195
228
|
app.use('/configs', defineEventHandler(async (event) => {
|
|
196
229
|
await this.requireUser(event);
|
|
197
230
|
const dashboard = await this.options.vault.dashboard();
|
|
198
|
-
|
|
231
|
+
const firstProfile = dashboard.profiles[0];
|
|
232
|
+
return firstProfile ? sendRedirect(event, `/deployment?profileId=${encodeURIComponent(firstProfile.id)}`) : sendRedirect(event, '/deployments');
|
|
199
233
|
}));
|
|
200
234
|
app.use('/runtime-keys', defineEventHandler(async (event) => {
|
|
201
235
|
await this.requireUser(event);
|
|
202
236
|
const query = getQuery(event);
|
|
203
237
|
const dashboard = await this.options.vault.dashboard();
|
|
204
|
-
|
|
238
|
+
const firstProfile = dashboard.profiles[0];
|
|
239
|
+
if (!String(query.secret ?? '') && firstProfile) {
|
|
240
|
+
return sendRedirect(event, `/deployment?profileId=${encodeURIComponent(firstProfile.id)}`);
|
|
241
|
+
}
|
|
242
|
+
return this.page('Container Key', runtimeKeysPage(dashboard, {
|
|
243
|
+
publicUrl: this.options.publicUrl,
|
|
244
|
+
keyId: String(query.keyId ?? ''),
|
|
245
|
+
secret: String(query.secret ?? ''),
|
|
246
|
+
}), 'runtime-keys');
|
|
205
247
|
}));
|
|
206
248
|
app.use('/plugins', defineEventHandler(async (event) => {
|
|
207
249
|
await this.requireUser(event);
|
|
250
|
+
const query = getQuery(event);
|
|
208
251
|
const dashboard = await this.options.vault.dashboard();
|
|
209
|
-
|
|
252
|
+
const registry = await registrySearch(this.options.registryUrl, String(query.query ?? ''));
|
|
253
|
+
return this.page('Plugins', pluginsPage(dashboard, registry, String(query.query ?? '')), 'plugins');
|
|
210
254
|
}));
|
|
211
255
|
app.use('/profile', defineEventHandler(async (event) => {
|
|
212
256
|
const session = await this.requireUser(event);
|
|
@@ -295,6 +339,7 @@ function html(title, body, active, authenticated) {
|
|
|
295
339
|
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
|
|
296
340
|
.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
341
|
.inline-form{display:flex;gap:10px;align-items:end;flex-wrap:wrap}.inline-form label{min-width:190px}
|
|
342
|
+
.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
343
|
.muted{color:var(--muted)}.danger{color:var(--danger)}.ok{color:var(--ok)}
|
|
299
344
|
.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
345
|
.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}
|
|
@@ -312,8 +357,6 @@ function nav(active) {
|
|
|
312
357
|
['overview', 'Overview', '/'],
|
|
313
358
|
['applications', 'Applications', '/applications'],
|
|
314
359
|
['deployments', 'Deployments', '/deployments'],
|
|
315
|
-
['configs', 'Configs', '/configs'],
|
|
316
|
-
['runtime-keys', 'Runtime Keys', '/runtime-keys'],
|
|
317
360
|
['plugins', 'Plugins', '/plugins'],
|
|
318
361
|
['profile', 'Profile', '/profile'],
|
|
319
362
|
];
|
|
@@ -439,11 +482,11 @@ function overviewPage(data) {
|
|
|
439
482
|
return `<div class="page-head"><div><h1>Overview</h1><p class="muted">Current Vault inventory and deployment configuration status.</p></div></div>
|
|
440
483
|
<div class="grid">
|
|
441
484
|
${metric('Applications', data.applications.length)}
|
|
442
|
-
${metric('
|
|
485
|
+
${metric('Deployments', data.groups.length)}
|
|
443
486
|
${metric('Deployment Profiles', data.profiles.length)}
|
|
444
|
-
${metric('
|
|
487
|
+
${metric('Container Keys', data.runtimeKeys.length)}
|
|
445
488
|
</div>
|
|
446
|
-
<section><h2>Recent
|
|
489
|
+
<section><h2>Recent Container Keys</h2>${runtimeKeyTable(data.runtimeKeys.slice(0, 8), data)}</section>`;
|
|
447
490
|
}
|
|
448
491
|
function applicationsPage(data) {
|
|
449
492
|
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 +503,70 @@ function applicationsPage(data) {
|
|
|
460
503
|
${formScript()}`;
|
|
461
504
|
}
|
|
462
505
|
function deploymentsPage(data) {
|
|
463
|
-
return `<div class="page-head"><div><h1>Deployments</h1><p class="muted">
|
|
464
|
-
<
|
|
465
|
-
<section><h2>Create Service Group</h2>
|
|
506
|
+
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>
|
|
507
|
+
<section><h2>Create Deployment</h2>
|
|
466
508
|
<form data-api="/api/groups" data-redirect="/deployments">
|
|
467
509
|
${select('applicationId', 'Application', data.applications.map((x) => [x.id, x.name]))}
|
|
468
|
-
${input('name', '
|
|
469
|
-
<button>Create
|
|
510
|
+
${input('name', 'Deployment Name', true)}
|
|
511
|
+
<button>Create Deployment</button><p class="status"></p>
|
|
470
512
|
</form>
|
|
471
513
|
</section>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
514
|
+
<section><h2>Deployments</h2>${groupsTable(data)}</section>
|
|
515
|
+
<section><h2>Profiles</h2>${profilesTable(data)}</section>
|
|
516
|
+
${formScript()}`;
|
|
517
|
+
}
|
|
518
|
+
function runtimeKeysPage(data, credential) {
|
|
519
|
+
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>
|
|
520
|
+
${credential.secret ? runtimeEnvBlock(credential) : ''}
|
|
521
|
+
<section><h2>Container Keys</h2>${runtimeKeyTable(data.runtimeKeys, data)}</section>`;
|
|
522
|
+
}
|
|
523
|
+
function deploymentDetailPage(data, credential) {
|
|
524
|
+
const draft = data.draft ?? { observable: {}, events: {}, services: {} };
|
|
525
|
+
const redirect = `/deployment?profileId=${encodeURIComponent(data.profile.id)}`;
|
|
526
|
+
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>
|
|
527
|
+
<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>
|
|
528
|
+
${credential.secret ? runtimeEnvBlock(credential) : ''}
|
|
529
|
+
<div class="grid">
|
|
530
|
+
<section><h2>Create Profile</h2>
|
|
531
|
+
<form data-api="/api/profiles" data-redirect="${escapeHtml(redirect)}">
|
|
532
|
+
<input type="hidden" name="groupId" value="${escapeHtml(data.group.id)}">
|
|
533
|
+
${input('name', 'Profile Name', true)}
|
|
476
534
|
<button>Create Profile</button><p class="status"></p>
|
|
477
535
|
</form>
|
|
478
536
|
</section>
|
|
537
|
+
<section><h2>Container Key</h2>
|
|
538
|
+
<form data-api="/api/runtime-keys" data-secret-redirect="${escapeHtml(redirect)}">
|
|
539
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
540
|
+
${input('name', 'Key Name', true, `${data.group.name}-${data.profile.name}`)}
|
|
541
|
+
${input('containerName', 'Container Name')}
|
|
542
|
+
<button>Create Key</button><p class="status"></p>
|
|
543
|
+
</form>
|
|
544
|
+
</section>
|
|
479
545
|
</div>
|
|
480
|
-
<section><h2>
|
|
481
|
-
|
|
482
|
-
|
|
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>
|
|
546
|
+
<section><h2>Profile Config</h2>
|
|
547
|
+
<form data-api="/api/drafts" data-redirect="${escapeHtml(redirect)}">
|
|
548
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
549
|
+
<label>Config JSON</label><textarea name="config" required>${escapeHtml(JSON.stringify(draft, null, 2))}</textarea>
|
|
490
550
|
<button>Save Draft</button><p class="status"></p>
|
|
491
551
|
</form>
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
${select('profileId', 'Deployment Profile', data.profiles.map((x) => [x.id, profileLabel(x, data)]))}
|
|
496
|
-
<button>Publish Active Version</button><p class="status"></p>
|
|
552
|
+
<form data-api="/api/publish" data-redirect="${escapeHtml(redirect)}">
|
|
553
|
+
<input type="hidden" name="profileId" value="${escapeHtml(data.profile.id)}">
|
|
554
|
+
<button class="secondary">Publish Draft</button><p class="status"></p>
|
|
497
555
|
</form>
|
|
498
556
|
</section>
|
|
557
|
+
<section><h2>Container Keys</h2>${profileRuntimeKeyTable(data.runtimeKeys, data)}</section>
|
|
499
558
|
${formScript()}`;
|
|
500
559
|
}
|
|
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>
|
|
560
|
+
function pluginsPage(data, registry, query) {
|
|
561
|
+
return `<div class="page-head"><div><h1>Plugin Catalog</h1><p class="muted">Import registry plugins for config authoring, or create private plugin entries manually.</p></div></div>
|
|
562
|
+
<section><h2>Registry Search</h2>
|
|
563
|
+
<form method="get" action="/plugins" class="inline-form">
|
|
564
|
+
${input('query', 'Search', false, query)}
|
|
565
|
+
<button>Search Registry</button>
|
|
515
566
|
</form>
|
|
567
|
+
${registry.length === 0 ? '<p class="muted">No registry results loaded.</p>' : registryTable(registry)}
|
|
516
568
|
</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>
|
|
569
|
+
<section><h2>Private Plugin</h2>
|
|
523
570
|
<form data-api="/api/plugins" data-redirect="/plugins">
|
|
524
571
|
<div class="form-grid">
|
|
525
572
|
${input('org', 'Org', true, '_')}
|
|
@@ -537,6 +584,27 @@ function pluginsPage(data) {
|
|
|
537
584
|
<section><h2>Catalog</h2>${table(data.plugins.map((x) => [x.pluginId, x.version, x.kind, x.source, x.packageName ?? '']))}</section>
|
|
538
585
|
${formScript()}`;
|
|
539
586
|
}
|
|
587
|
+
function registryTable(items) {
|
|
588
|
+
return `<table>${items.map((item) => `<tr>
|
|
589
|
+
<td>${escapeHtml(item.pluginId)}</td>
|
|
590
|
+
<td>${escapeHtml(item.version)}</td>
|
|
591
|
+
<td>${escapeHtml(item.kind)}</td>
|
|
592
|
+
<td>${escapeHtml(item.packageName ?? '')}</td>
|
|
593
|
+
<td>
|
|
594
|
+
<form data-api="/api/plugins/import" data-redirect="/plugins">
|
|
595
|
+
<input type="hidden" name="org" value="${escapeHtml(item.org)}">
|
|
596
|
+
<input type="hidden" name="name" value="${escapeHtml(item.name)}">
|
|
597
|
+
<input type="hidden" name="pluginId" value="${escapeHtml(item.pluginId)}">
|
|
598
|
+
<input type="hidden" name="packageName" value="${escapeHtml(item.packageName ?? '')}">
|
|
599
|
+
<input type="hidden" name="version" value="${escapeHtml(item.version)}">
|
|
600
|
+
<input type="hidden" name="kind" value="${escapeHtml(item.kind)}">
|
|
601
|
+
<input type="hidden" name="configSchema" value="${escapeHtml(JSON.stringify(item.configSchema ?? {}))}">
|
|
602
|
+
<input type="hidden" name="eventSchema" value="${escapeHtml(JSON.stringify(item.eventSchema ?? {}))}">
|
|
603
|
+
<button class="secondary">Import</button><p class="status"></p>
|
|
604
|
+
</form>
|
|
605
|
+
</td>
|
|
606
|
+
</tr>`).join('')}</table>`;
|
|
607
|
+
}
|
|
540
608
|
function profilePage(data) {
|
|
541
609
|
return `<div class="page-head"><div><h1>Profile</h1><p class="muted">Account security and admin authentication settings.</p></div></div>
|
|
542
610
|
<section><h2>Account</h2>${table([[data.user.email, data.user.createdAt]])}</section>
|
|
@@ -555,11 +623,15 @@ function applicationsTable(data) {
|
|
|
555
623
|
${input('description', 'Description', false, app.description ?? '')}
|
|
556
624
|
<button>Save</button><p class="status"></p>
|
|
557
625
|
</form>
|
|
558
|
-
</td><td class="actions"
|
|
626
|
+
</td><td class="actions">
|
|
627
|
+
<a class="button secondary" href="/deployments">Deployments</a>
|
|
628
|
+
${deleteForm('/api/applications/delete', app.id, '/applications', 'Delete application and related deployments, profiles, configs, and keys?')}
|
|
629
|
+
</td></tr>`).join('')}</table>`;
|
|
559
630
|
}
|
|
560
631
|
function groupsTable(data) {
|
|
561
632
|
if (data.groups.length === 0)
|
|
562
633
|
return '<p class="muted">None</p>';
|
|
634
|
+
const defaultProfileFor = (groupId) => data.profiles.find((profile) => profile.groupId === groupId && profile.name === 'default') ?? data.profiles.find((profile) => profile.groupId === groupId);
|
|
563
635
|
return `<table>${data.groups.map((group) => `<tr><td>
|
|
564
636
|
<form data-api="/api/groups/update" data-redirect="/deployments" class="inline-form">
|
|
565
637
|
<input type="hidden" name="id" value="${escapeHtml(group.id)}">
|
|
@@ -567,7 +639,10 @@ function groupsTable(data) {
|
|
|
567
639
|
${input('name', 'Name', true, group.name)}
|
|
568
640
|
<button>Save</button><p class="status"></p>
|
|
569
641
|
</form>
|
|
570
|
-
</td><td class="actions"
|
|
642
|
+
</td><td class="actions">
|
|
643
|
+
${defaultProfileFor(group.id) ? `<a class="button secondary" href="/deployment?profileId=${encodeURIComponent(defaultProfileFor(group.id).id)}">Open</a>` : ''}
|
|
644
|
+
${deleteForm('/api/groups/delete', group.id, '/deployments', 'Delete deployment and related profiles, configs, and keys?')}
|
|
645
|
+
</td></tr>`).join('')}</table>`;
|
|
571
646
|
}
|
|
572
647
|
function profilesTable(data) {
|
|
573
648
|
if (data.profiles.length === 0)
|
|
@@ -575,12 +650,15 @@ function profilesTable(data) {
|
|
|
575
650
|
return `<table>${data.profiles.map((profile) => `<tr><td>
|
|
576
651
|
<form data-api="/api/profiles/update" data-redirect="/deployments" class="inline-form">
|
|
577
652
|
<input type="hidden" name="id" value="${escapeHtml(profile.id)}">
|
|
578
|
-
${select('groupId', '
|
|
653
|
+
${select('groupId', 'Deployment', selectedOptions(data.groups.map((x) => [x.id, groupLabel(x, data)]), profile.groupId))}
|
|
579
654
|
${input('name', 'Name', true, profile.name)}
|
|
580
655
|
<span class="muted">${profile.activeVersionId ? 'published' : 'no published config'}</span>
|
|
581
656
|
<button>Save</button><p class="status"></p>
|
|
582
657
|
</form>
|
|
583
|
-
</td><td class="actions"
|
|
658
|
+
</td><td class="actions">
|
|
659
|
+
<a class="button secondary" href="/deployment?profileId=${encodeURIComponent(profile.id)}">Open</a>
|
|
660
|
+
${deleteForm('/api/profiles/delete', profile.id, '/deployments', 'Delete deployment profile and related configs and keys?')}
|
|
661
|
+
</td></tr>`).join('')}</table>`;
|
|
584
662
|
}
|
|
585
663
|
function deleteForm(action, id, redirect, confirm) {
|
|
586
664
|
return `<form data-api="${escapeHtml(action)}" data-redirect="${escapeHtml(redirect)}" data-confirm="${escapeHtml(confirm)}">
|
|
@@ -624,6 +702,31 @@ function runtimeKeyTable(keys, data) {
|
|
|
624
702
|
key.revokedAt ?? 'active',
|
|
625
703
|
]));
|
|
626
704
|
}
|
|
705
|
+
function profileRuntimeKeyTable(keys, data) {
|
|
706
|
+
if (keys.length === 0)
|
|
707
|
+
return '<p class="muted">No container keys created for this profile.</p>';
|
|
708
|
+
return `<table>${keys.map((key) => `<tr>
|
|
709
|
+
<td>${escapeHtml(key.name)}</td>
|
|
710
|
+
<td>${escapeHtml(key.id)}</td>
|
|
711
|
+
<td>${escapeHtml(data.profile.name)}</td>
|
|
712
|
+
<td>${escapeHtml(key.containerName ?? '')}</td>
|
|
713
|
+
<td>${escapeHtml(key.revokedAt ?? 'active')}</td>
|
|
714
|
+
<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>
|
|
715
|
+
</tr>`).join('')}</table>`;
|
|
716
|
+
}
|
|
717
|
+
function runtimeEnvBlock(credential) {
|
|
718
|
+
const env = [
|
|
719
|
+
'BSB_CONFIG_PLUGIN=config-vault',
|
|
720
|
+
'BSB_CONFIG_PLUGIN_PACKAGE=@bsb/config-vault',
|
|
721
|
+
`vaultUrl=${credential.publicUrl}`,
|
|
722
|
+
`apiKeyId=${credential.keyId}`,
|
|
723
|
+
`apiSecret=${credential.secret}`,
|
|
724
|
+
].join('\n');
|
|
725
|
+
return `<section><h2>Container Environment</h2>
|
|
726
|
+
<p class="muted">Shown once. Add these env vars to the BSB container that should load this deployment profile.</p>
|
|
727
|
+
<pre class="code"><code>${escapeHtml(env)}</code></pre>
|
|
728
|
+
</section>`;
|
|
729
|
+
}
|
|
627
730
|
function shortId(value) {
|
|
628
731
|
return value.length > 18 ? `${value.slice(0, 10)}...${value.slice(-6)}` : value;
|
|
629
732
|
}
|
|
@@ -650,7 +753,10 @@ function formScript() {
|
|
|
650
753
|
const result = await res.json();
|
|
651
754
|
if (!res.ok) throw new Error(result.message || 'Save failed');
|
|
652
755
|
if (form.dataset.secretRedirect && result.secret) {
|
|
653
|
-
|
|
756
|
+
const joiner = form.dataset.secretRedirect.includes('?') ? '&' : '?';
|
|
757
|
+
location.href = form.dataset.secretRedirect
|
|
758
|
+
+ joiner + 'keyId=' + encodeURIComponent(result.keyId || result.id || '')
|
|
759
|
+
+ '&secret=' + encodeURIComponent(result.secret);
|
|
654
760
|
return;
|
|
655
761
|
}
|
|
656
762
|
location.href = form.dataset.redirect || location.pathname;
|
|
@@ -717,29 +823,63 @@ function webauthnClientScript() {
|
|
|
717
823
|
}
|
|
718
824
|
`;
|
|
719
825
|
}
|
|
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
826
|
function table(rows) {
|
|
739
827
|
if (rows.length === 0)
|
|
740
828
|
return '<p class="muted">None</p>';
|
|
741
829
|
return `<table>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</table>`;
|
|
742
830
|
}
|
|
831
|
+
async function registrySearch(registryUrl, query) {
|
|
832
|
+
const url = new URL('/plugins', registryUrl);
|
|
833
|
+
url.searchParams.set('language', 'nodejs');
|
|
834
|
+
url.searchParams.set('limit', '20');
|
|
835
|
+
if (query.trim())
|
|
836
|
+
url.searchParams.set('query', query.trim());
|
|
837
|
+
try {
|
|
838
|
+
const response = await fetch(url, { headers: { accept: 'application/json' } });
|
|
839
|
+
if (!response.ok)
|
|
840
|
+
return [];
|
|
841
|
+
const parsed = await response.json();
|
|
842
|
+
return (Array.isArray(parsed.plugins) ? parsed.plugins : [])
|
|
843
|
+
.map(normalizeRegistryCandidate)
|
|
844
|
+
.filter((item) => item !== null);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return [];
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function normalizeRegistryCandidate(input) {
|
|
851
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input))
|
|
852
|
+
return null;
|
|
853
|
+
const value = input;
|
|
854
|
+
const org = stringField(value.org) ?? orgFromPackage(value.packageName ?? value.package) ?? '_';
|
|
855
|
+
const name = stringField(value.name) ?? stringField(value.pluginId) ?? stringField(value.id);
|
|
856
|
+
if (!name)
|
|
857
|
+
return null;
|
|
858
|
+
const packageName = stringField(value.packageName) ?? stringField(value.package) ?? null;
|
|
859
|
+
const pluginId = stringField(value.pluginId) ?? stringField(value.id) ?? name;
|
|
860
|
+
return {
|
|
861
|
+
org,
|
|
862
|
+
name,
|
|
863
|
+
pluginId,
|
|
864
|
+
packageName,
|
|
865
|
+
version: stringField(value.version) ?? '0.0.0',
|
|
866
|
+
kind: parseKind(value.kind ?? value.category ?? value.type),
|
|
867
|
+
configSchema: objectField(value.configSchema) ?? objectField(value.schema) ?? objectField(value.validationSchema) ?? null,
|
|
868
|
+
eventSchema: objectField(value.eventSchema) ?? objectField(value.events) ?? null,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function stringField(value) {
|
|
872
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
873
|
+
}
|
|
874
|
+
function objectField(value) {
|
|
875
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
|
|
876
|
+
}
|
|
877
|
+
function orgFromPackage(value) {
|
|
878
|
+
if (typeof value !== 'string' || !value.startsWith('@'))
|
|
879
|
+
return undefined;
|
|
880
|
+
const [org] = value.split('/');
|
|
881
|
+
return org || undefined;
|
|
882
|
+
}
|
|
743
883
|
function parseJsonObject(value) {
|
|
744
884
|
if (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
745
885
|
return value;
|