@lamalibre/create-lamaste 4.0.0
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/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKI path derivation for the Lamaste server daemon.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the canonical CA cert/key locations consumed by other daemon
|
|
5
|
+
* modules (mtls, csr-signing, enrollment, revocation, plugins) and shared
|
|
6
|
+
* with plugins through the plugin host's `resolvePluginOptions` callback.
|
|
7
|
+
*
|
|
8
|
+
* The directory is derived from `LAMALIBRE_LAMASTE_PKI_DIR` with the same
|
|
9
|
+
* `/etc/lamalibre/lamaste/pki` default used throughout the daemon. Keeping the
|
|
10
|
+
* default in one exported helper gives us a single place to point at when
|
|
11
|
+
* the other consumers are migrated onto it.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PKI_DIR = '/etc/lamalibre/lamaste/pki';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the PKI directory.
|
|
19
|
+
*
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function getPkiDir() {
|
|
23
|
+
return process.env.LAMALIBRE_LAMASTE_PKI_DIR || DEFAULT_PKI_DIR;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return the canonical CA cert and key paths.
|
|
28
|
+
*
|
|
29
|
+
* The files themselves are not read here — callers that need to verify
|
|
30
|
+
* existence or parse the certificate must do so explicitly. This helper
|
|
31
|
+
* only centralizes the path derivation.
|
|
32
|
+
*
|
|
33
|
+
* @returns {{ certPath: string, keyPath: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function getCaPaths() {
|
|
36
|
+
const pkiDir = getPkiDir();
|
|
37
|
+
return {
|
|
38
|
+
certPath: path.join(pkiDir, 'ca.crt'),
|
|
39
|
+
keyPath: path.join(pkiDir, 'ca.key'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent certificate signing with the panel CA.
|
|
3
|
+
*
|
|
4
|
+
* The panel is the CA: the lamaste user owns the PKI directory and the CA key
|
|
5
|
+
* (plugins that issue their own certificates get the key's path too — see
|
|
6
|
+
* plugin-router.js). Signing therefore needs no privileges and runs as the
|
|
7
|
+
* panel itself.
|
|
8
|
+
*
|
|
9
|
+
* The certificate's subject is taken from the CSR as is, and no CSR
|
|
10
|
+
* extension is copied. Callers verify the exact CN they expect before
|
|
11
|
+
* signing; `signAgentCsr` re-checks its shape as a last line — only agent and
|
|
12
|
+
* plugin-agent labels, never `admin` (admin certificates come from
|
|
13
|
+
* `lamaste-server reset-admin` alone).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { execa } from 'execa';
|
|
18
|
+
|
|
19
|
+
const PKI_DIR = process.env.LAMALIBRE_LAMASTE_PKI_DIR || '/etc/lamalibre/lamaste/pki';
|
|
20
|
+
|
|
21
|
+
const LABEL = '[a-z0-9][a-z0-9-]{0,61}[a-z0-9]?';
|
|
22
|
+
const AGENT_CN_RE = new RegExp(`^(?:agent|plugin-agent):${LABEL}(?::${LABEL})?$`);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A 16-byte (128-bit) random serial as lower-case hex with no leading zero,
|
|
26
|
+
* so it stays a positive integer when openssl reads it as `0x<serial>`.
|
|
27
|
+
*
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function randomCertSerial() {
|
|
31
|
+
let hex = crypto.randomBytes(16).toString('hex').replace(/^0+/, '');
|
|
32
|
+
if (hex.length === 0) hex = '1';
|
|
33
|
+
return hex;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract the CN from a CSR's subject. Returns the CN string or null.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} csrPath
|
|
40
|
+
* @returns {Promise<string | null>}
|
|
41
|
+
*/
|
|
42
|
+
export async function readCsrCN(csrPath) {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execa('openssl', [
|
|
45
|
+
'req',
|
|
46
|
+
'-in',
|
|
47
|
+
csrPath,
|
|
48
|
+
'-noout',
|
|
49
|
+
'-subject',
|
|
50
|
+
'-nameopt',
|
|
51
|
+
'RFC2253',
|
|
52
|
+
]);
|
|
53
|
+
// Output: "subject=CN=agent:foo,O=Lamaste". RFC2253 form is
|
|
54
|
+
// comma-separated; the agent label charset has no commas to escape.
|
|
55
|
+
const subject = stdout.trim().replace(/^subject=\s*/, '');
|
|
56
|
+
for (const part of subject.split(',')) {
|
|
57
|
+
const trimmed = part.trim();
|
|
58
|
+
if (trimmed.startsWith('CN=')) {
|
|
59
|
+
return trimmed.slice(3);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sign the CSR at `csrPath` with the panel CA, writing the certificate to
|
|
70
|
+
* `certPath`.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} csrPath
|
|
73
|
+
* @param {string} certPath
|
|
74
|
+
* @param {string} serial - lower-case hex, see randomCertSerial()
|
|
75
|
+
* @param {number} days
|
|
76
|
+
*/
|
|
77
|
+
export async function signAgentCsr(csrPath, certPath, serial, days) {
|
|
78
|
+
const cn = await readCsrCN(csrPath);
|
|
79
|
+
if (!cn || !AGENT_CN_RE.test(cn)) {
|
|
80
|
+
throw Object.assign(new Error(`Refusing to sign a CSR with CN ${cn ?? '(none)'}`), {
|
|
81
|
+
statusCode: 400,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (!/^[0-9a-f]{1,32}$/.test(serial)) throw new Error('Invalid certificate serial');
|
|
85
|
+
if (!Number.isInteger(days) || days < 1 || days > 7300) throw new Error('Invalid validity');
|
|
86
|
+
await execa('openssl', [
|
|
87
|
+
'x509',
|
|
88
|
+
'-req',
|
|
89
|
+
'-in',
|
|
90
|
+
csrPath,
|
|
91
|
+
'-CA',
|
|
92
|
+
`${PKI_DIR}/ca.crt`,
|
|
93
|
+
'-CAkey',
|
|
94
|
+
`${PKI_DIR}/ca.key`,
|
|
95
|
+
'-days',
|
|
96
|
+
String(days),
|
|
97
|
+
'-set_serial',
|
|
98
|
+
`0x${serial}`,
|
|
99
|
+
'-sha256',
|
|
100
|
+
'-out',
|
|
101
|
+
certPath,
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side plugin lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Schema validation and constants are imported from @lamalibre/lamaste (core).
|
|
5
|
+
* This module retains server-specific lifecycle operations (install, uninstall,
|
|
6
|
+
* enable, disable) that use the local promise-chain mutex and execa for npm.
|
|
7
|
+
*
|
|
8
|
+
* Backed by SQLite (`state.db`, table `plugins`) per
|
|
9
|
+
* docs/decisions/sqlite-migration.md §3c.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { execa } from 'execa';
|
|
16
|
+
import {
|
|
17
|
+
RESERVED_API_PREFIXES,
|
|
18
|
+
RESERVED_NAV_LABELS,
|
|
19
|
+
derivePluginRoute,
|
|
20
|
+
validateManifest as coreValidateManifest,
|
|
21
|
+
} from '@lamalibre/lamaste';
|
|
22
|
+
import { revokePluginCapabilitiesFromAgents } from '@lamalibre/lamaste/server';
|
|
23
|
+
import { listRegisteredSubScopeNames } from './tickets.js';
|
|
24
|
+
import { getStateDb } from './state-db.js';
|
|
25
|
+
|
|
26
|
+
const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
27
|
+
const PLUGINS_DIR = path.join(STATE_DIR, 'plugins');
|
|
28
|
+
const PKI_DIR = process.env.LAMALIBRE_LAMASTE_PKI_DIR || '/etc/lamalibre/lamaste/pki';
|
|
29
|
+
|
|
30
|
+
// Promise-chain mutex. SQLite's BEGIN IMMEDIATE serialises a single write
|
|
31
|
+
// transaction, but install/uninstall are read-modify-write sequences that
|
|
32
|
+
// span multiple statements (and shell out to npm in between). This lock
|
|
33
|
+
// keeps each such sequence atomic. Removable once each operation is
|
|
34
|
+
// collapsed into one transaction.
|
|
35
|
+
let pluginLock = Promise.resolve();
|
|
36
|
+
function withPluginLock(fn) {
|
|
37
|
+
const prev = pluginLock;
|
|
38
|
+
let resolve;
|
|
39
|
+
pluginLock = new Promise((r) => {
|
|
40
|
+
resolve = r;
|
|
41
|
+
});
|
|
42
|
+
return prev.then(fn).finally(resolve);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- SQLite prepared-statement bundle (lazy init) ---
|
|
46
|
+
|
|
47
|
+
let stmts = null;
|
|
48
|
+
|
|
49
|
+
async function getStmts() {
|
|
50
|
+
if (stmts) return stmts;
|
|
51
|
+
|
|
52
|
+
const db = await getStateDb();
|
|
53
|
+
|
|
54
|
+
stmts = {
|
|
55
|
+
db,
|
|
56
|
+
|
|
57
|
+
selectAll: db.prepare('SELECT * FROM plugins'),
|
|
58
|
+
selectByName: db.prepare('SELECT * FROM plugins WHERE name = ?'),
|
|
59
|
+
selectByPackageName: db.prepare('SELECT * FROM plugins WHERE package_name = ?'),
|
|
60
|
+
insert: db.prepare(`
|
|
61
|
+
INSERT INTO plugins
|
|
62
|
+
(name, display_name, package_name, version, description, capabilities,
|
|
63
|
+
packages, panel, config, modes, status, installed_at, enabled_at)
|
|
64
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
65
|
+
`),
|
|
66
|
+
deleteByName: db.prepare('DELETE FROM plugins WHERE name = ?'),
|
|
67
|
+
deleteAll: db.prepare('DELETE FROM plugins'),
|
|
68
|
+
updateStatusEnable: db.prepare(`
|
|
69
|
+
UPDATE plugins SET status = 'enabled', enabled_at = ? WHERE name = ?
|
|
70
|
+
`),
|
|
71
|
+
updateStatusDisable: db.prepare(`
|
|
72
|
+
UPDATE plugins SET status = 'disabled', enabled_at = NULL WHERE name = ?
|
|
73
|
+
`),
|
|
74
|
+
|
|
75
|
+
begin: db.prepare('BEGIN IMMEDIATE'),
|
|
76
|
+
commit: db.prepare('COMMIT'),
|
|
77
|
+
rollback: db.prepare('ROLLBACK'),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return stmts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Row → JS object helper ---
|
|
84
|
+
//
|
|
85
|
+
// JSON-typed columns (capabilities, packages, panel, config, modes) are
|
|
86
|
+
// JSON-encoded TEXT in storage. They round-trip through JSON.parse/stringify
|
|
87
|
+
// so callers see the same shape the JSON-backed module returned — including
|
|
88
|
+
// `undefined` for optional fields (mirroring the JSON object layout where
|
|
89
|
+
// missing keys are absent rather than null).
|
|
90
|
+
|
|
91
|
+
function rowToPlugin(row) {
|
|
92
|
+
const entry = {
|
|
93
|
+
name: row.name,
|
|
94
|
+
packageName: row.package_name,
|
|
95
|
+
version: row.version,
|
|
96
|
+
capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
|
|
97
|
+
status: row.status,
|
|
98
|
+
installedAt: row.installed_at,
|
|
99
|
+
};
|
|
100
|
+
if (row.display_name != null) entry.displayName = row.display_name;
|
|
101
|
+
if (row.description != null) entry.description = row.description;
|
|
102
|
+
if (row.packages != null) entry.packages = JSON.parse(row.packages);
|
|
103
|
+
if (row.panel != null) entry.panel = JSON.parse(row.panel);
|
|
104
|
+
if (row.config != null) entry.config = JSON.parse(row.config);
|
|
105
|
+
if (row.modes != null) entry.modes = JSON.parse(row.modes);
|
|
106
|
+
if (row.enabled_at != null) entry.enabledAt = row.enabled_at;
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function nullableString(value) {
|
|
111
|
+
return value === undefined || value === null ? null : value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function nullableJson(value) {
|
|
115
|
+
return value === undefined || value === null ? null : JSON.stringify(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Plugin registry persistence ---
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the plugin registry from disk.
|
|
122
|
+
* Returns `{ plugins: [] }` if the file does not exist.
|
|
123
|
+
*/
|
|
124
|
+
export async function readPlugins() {
|
|
125
|
+
const s = await getStmts();
|
|
126
|
+
const rows = s.selectAll.all();
|
|
127
|
+
return { plugins: rows.map(rowToPlugin) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Write the plugin registry to disk atomically.
|
|
132
|
+
*
|
|
133
|
+
* The function preserves its byte-identical signature from the JSON era: it
|
|
134
|
+
* accepts a `{ plugins: [...] }` shape and replaces the entire registry. The
|
|
135
|
+
* SQLite implementation runs DELETE + INSERTs in one BEGIN IMMEDIATE
|
|
136
|
+
* transaction so observers see either the old or the new state, never a
|
|
137
|
+
* partial write.
|
|
138
|
+
*/
|
|
139
|
+
export async function writePlugins(data) {
|
|
140
|
+
const s = await getStmts();
|
|
141
|
+
const list = Array.isArray(data?.plugins) ? data.plugins : [];
|
|
142
|
+
|
|
143
|
+
s.begin.run();
|
|
144
|
+
try {
|
|
145
|
+
s.deleteAll.run();
|
|
146
|
+
for (const p of list) {
|
|
147
|
+
s.insert.run(
|
|
148
|
+
p.name,
|
|
149
|
+
nullableString(p.displayName),
|
|
150
|
+
p.packageName,
|
|
151
|
+
p.version,
|
|
152
|
+
nullableString(p.description),
|
|
153
|
+
JSON.stringify(p.capabilities ?? []),
|
|
154
|
+
nullableJson(p.packages),
|
|
155
|
+
nullableJson(p.panel),
|
|
156
|
+
nullableJson(p.config),
|
|
157
|
+
nullableJson(p.modes),
|
|
158
|
+
p.status,
|
|
159
|
+
p.installedAt,
|
|
160
|
+
nullableString(p.enabledAt),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
s.commit.run();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
s.rollback.run();
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validate a raw plugin manifest against the schema.
|
|
172
|
+
* Delegates to the core library's ManifestSchema.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} raw - The raw manifest object
|
|
175
|
+
* @returns {object} The validated and parsed manifest
|
|
176
|
+
* @throws {z.ZodError} If validation fails
|
|
177
|
+
*/
|
|
178
|
+
export function validateManifest(raw) {
|
|
179
|
+
return coreValidateManifest(raw);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Install a plugin by npm package name.
|
|
184
|
+
* Only @lamalibre/ scoped packages are allowed.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} packageName - The npm package name (must start with @lamalibre/)
|
|
187
|
+
* @param {import('pino').Logger} logger
|
|
188
|
+
* @returns {Promise<object>} The installed plugin entry
|
|
189
|
+
*/
|
|
190
|
+
// Reject npm aliases (e.g. "@lamalibre/x@npm:totally-evil") and any name that
|
|
191
|
+
// could resolve outside the @lamalibre/ scope. Optional version suffix
|
|
192
|
+
// (`@1.2.3`, `@beta`, etc.) is allowed; alias schemes (`@npm:`, `@github:`,
|
|
193
|
+
// etc.) and shell metacharacters are not.
|
|
194
|
+
const LAMALIBRE_PACKAGE_RE = /^@lamalibre\/[a-z0-9][a-z0-9._-]{0,213}(@[A-Za-z0-9._-]+)?$/;
|
|
195
|
+
|
|
196
|
+
function assertSafeLamalibrePackage(packageName) {
|
|
197
|
+
if (typeof packageName !== 'string' || packageName.length === 0 || packageName.length > 256) {
|
|
198
|
+
throw Object.assign(new Error('Invalid package name'), { statusCode: 400 });
|
|
199
|
+
}
|
|
200
|
+
if (!LAMALIBRE_PACKAGE_RE.test(packageName)) {
|
|
201
|
+
throw Object.assign(
|
|
202
|
+
new Error('Only @lamalibre/ scoped packages are allowed (alias schemes are rejected)'),
|
|
203
|
+
{ statusCode: 400 },
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
// Defense-in-depth against npm alias syntax even if the regex above shifts.
|
|
207
|
+
const versionSuffix = packageName.indexOf('@', 1);
|
|
208
|
+
if (versionSuffix !== -1 && packageName.slice(versionSuffix).startsWith('@npm:')) {
|
|
209
|
+
throw Object.assign(new Error('npm: alias references are not permitted'), { statusCode: 400 });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function installPlugin(packageName, logger) {
|
|
214
|
+
return withPluginLock(async () => {
|
|
215
|
+
// Validate package scope
|
|
216
|
+
assertSafeLamalibrePackage(packageName);
|
|
217
|
+
|
|
218
|
+
const s = await getStmts();
|
|
219
|
+
|
|
220
|
+
// Check if already installed
|
|
221
|
+
const existingRow = s.selectByPackageName.get(packageName);
|
|
222
|
+
if (existingRow) {
|
|
223
|
+
throw Object.assign(new Error(`Plugin "${packageName}" is already installed`), {
|
|
224
|
+
statusCode: 409,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Ensure STATE_DIR has a package.json so npm does not walk up the tree
|
|
229
|
+
const statePackageJson = path.join(STATE_DIR, 'package.json');
|
|
230
|
+
try {
|
|
231
|
+
await readFile(statePackageJson, 'utf-8');
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (err.code === 'ENOENT') {
|
|
234
|
+
await writeFile(statePackageJson, '{"private":true}\n', { encoding: 'utf-8', mode: 0o600 });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Install the npm package
|
|
239
|
+
logger.info({ packageName }, 'Installing plugin package');
|
|
240
|
+
try {
|
|
241
|
+
await execa('npm', ['install', '--ignore-scripts', packageName], {
|
|
242
|
+
cwd: STATE_DIR,
|
|
243
|
+
timeout: 120000,
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
logger.error({ packageName, stderr: err.stderr, err: err.message }, 'npm install failed');
|
|
247
|
+
throw Object.assign(new Error(`Failed to install "${packageName}"`), { statusCode: 500 });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Read the plugin manifest from the installed package
|
|
251
|
+
let manifest;
|
|
252
|
+
try {
|
|
253
|
+
const require = createRequire(`${STATE_DIR}/`);
|
|
254
|
+
const manifestPath = require.resolve(`${packageName}/lamaste-plugin.json`);
|
|
255
|
+
const manifestRaw = await readFile(manifestPath, 'utf-8');
|
|
256
|
+
manifest = validateManifest(JSON.parse(manifestRaw));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
// Clean up the installed package if manifest is missing/invalid
|
|
259
|
+
logger.warn({ packageName, err: err.message }, 'Invalid or missing manifest, uninstalling');
|
|
260
|
+
await execa('npm', ['uninstall', packageName], { cwd: STATE_DIR }).catch(() => {});
|
|
261
|
+
|
|
262
|
+
if (err instanceof z.ZodError) {
|
|
263
|
+
throw Object.assign(
|
|
264
|
+
new Error(`Invalid plugin manifest: ${err.errors.map((e) => e.message).join(', ')}`),
|
|
265
|
+
{ statusCode: 400 },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
throw Object.assign(new Error(`Plugin manifest not found in "${packageName}"`), {
|
|
269
|
+
statusCode: 400,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Reject names that collide with core API route prefixes
|
|
274
|
+
if (RESERVED_API_PREFIXES.includes(manifest.name)) {
|
|
275
|
+
await execa('npm', ['uninstall', packageName], { cwd: STATE_DIR }).catch(() => {});
|
|
276
|
+
throw Object.assign(new Error(`Plugin name "${manifest.name}" is reserved`), {
|
|
277
|
+
statusCode: 400,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Reject displayName that matches reserved navigation labels
|
|
282
|
+
if (manifest.displayName) {
|
|
283
|
+
if (RESERVED_NAV_LABELS.includes(manifest.displayName.toLowerCase())) {
|
|
284
|
+
await execa('npm', ['uninstall', packageName], { cwd: STATE_DIR }).catch(() => {});
|
|
285
|
+
throw Object.assign(
|
|
286
|
+
new Error(
|
|
287
|
+
`Plugin display name "${manifest.displayName}" conflicts with a core navigation label`,
|
|
288
|
+
),
|
|
289
|
+
{ statusCode: 400 },
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate apiPrefix matches plugin name if specified
|
|
295
|
+
if (manifest.panel?.apiPrefix && manifest.panel.apiPrefix !== `/api/${manifest.name}`) {
|
|
296
|
+
await execa('npm', ['uninstall', packageName], { cwd: STATE_DIR }).catch(() => {});
|
|
297
|
+
throw Object.assign(
|
|
298
|
+
new Error(
|
|
299
|
+
`Plugin apiPrefix must be "/api/${manifest.name}" but got "${manifest.panel.apiPrefix}"`,
|
|
300
|
+
),
|
|
301
|
+
{ statusCode: 400 },
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Cross-namespace collision: any capability declared by the plugin must
|
|
306
|
+
// not match a sub-scope name already owned by a registered ticket scope.
|
|
307
|
+
// Schema-level namespacing puts both sides in `plugin:*`; this check is
|
|
308
|
+
// a flat set intersection. The reverse direction (registering a scope
|
|
309
|
+
// that collides with an installed plugin's cap) is enforced in
|
|
310
|
+
// `registerScope`.
|
|
311
|
+
if (Array.isArray(manifest.capabilities) && manifest.capabilities.length > 0) {
|
|
312
|
+
const subScopeNames = new Set(await listRegisteredSubScopeNames());
|
|
313
|
+
const collision = manifest.capabilities.find((c) => subScopeNames.has(c));
|
|
314
|
+
if (collision) {
|
|
315
|
+
await execa('npm', ['uninstall', packageName], { cwd: STATE_DIR }).catch(() => {});
|
|
316
|
+
throw Object.assign(
|
|
317
|
+
new Error(
|
|
318
|
+
`Plugin capability "${collision}" collides with an existing ticket scope sub-scope name. ` +
|
|
319
|
+
'Pick a different action name or unregister the conflicting ticket scope first.',
|
|
320
|
+
),
|
|
321
|
+
{ statusCode: 409 },
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Create the plugin state directory
|
|
327
|
+
const pluginDir = path.join(PLUGINS_DIR, manifest.name);
|
|
328
|
+
await mkdir(pluginDir, { recursive: true, mode: 0o700 });
|
|
329
|
+
|
|
330
|
+
// Build the entry, then persist via INSERT
|
|
331
|
+
const entry = {
|
|
332
|
+
name: manifest.name,
|
|
333
|
+
displayName: manifest.displayName,
|
|
334
|
+
packageName,
|
|
335
|
+
version: manifest.version,
|
|
336
|
+
description: manifest.description,
|
|
337
|
+
capabilities: manifest.capabilities,
|
|
338
|
+
packages: manifest.packages,
|
|
339
|
+
panel: manifest.panel,
|
|
340
|
+
config: manifest.config,
|
|
341
|
+
modes: manifest.modes,
|
|
342
|
+
status: 'disabled',
|
|
343
|
+
installedAt: new Date().toISOString(),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
s.begin.run();
|
|
347
|
+
try {
|
|
348
|
+
s.insert.run(
|
|
349
|
+
entry.name,
|
|
350
|
+
nullableString(entry.displayName),
|
|
351
|
+
entry.packageName,
|
|
352
|
+
entry.version,
|
|
353
|
+
nullableString(entry.description),
|
|
354
|
+
JSON.stringify(entry.capabilities ?? []),
|
|
355
|
+
nullableJson(entry.packages),
|
|
356
|
+
nullableJson(entry.panel),
|
|
357
|
+
nullableJson(entry.config),
|
|
358
|
+
nullableJson(entry.modes),
|
|
359
|
+
entry.status,
|
|
360
|
+
entry.installedAt,
|
|
361
|
+
null,
|
|
362
|
+
);
|
|
363
|
+
s.commit.run();
|
|
364
|
+
} catch (err) {
|
|
365
|
+
s.rollback.run();
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
logger.info({ name: manifest.name, packageName }, 'Plugin installed');
|
|
370
|
+
return entry;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Uninstall a plugin. Must be disabled first.
|
|
376
|
+
*
|
|
377
|
+
* @param {string} name - The plugin name
|
|
378
|
+
* @param {import('pino').Logger} logger
|
|
379
|
+
* @returns {Promise<{ ok: true, name: string }>}
|
|
380
|
+
*/
|
|
381
|
+
export function uninstallPlugin(name, logger) {
|
|
382
|
+
return withPluginLock(async () => {
|
|
383
|
+
const s = await getStmts();
|
|
384
|
+
const row = s.selectByName.get(name);
|
|
385
|
+
|
|
386
|
+
if (!row) {
|
|
387
|
+
throw Object.assign(new Error(`Plugin "${name}" not found`), { statusCode: 404 });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const plugin = rowToPlugin(row);
|
|
391
|
+
|
|
392
|
+
if (plugin.status === 'enabled') {
|
|
393
|
+
throw Object.assign(new Error(`Plugin "${name}" must be disabled before uninstalling`), {
|
|
394
|
+
statusCode: 400,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Uninstall the npm package
|
|
399
|
+
logger.info({ name, packageName: plugin.packageName }, 'Uninstalling plugin package');
|
|
400
|
+
await execa('npm', ['uninstall', plugin.packageName], {
|
|
401
|
+
cwd: STATE_DIR,
|
|
402
|
+
}).catch((err) => {
|
|
403
|
+
logger.warn({ err: err.message }, 'npm uninstall failed (continuing)');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Remove the plugin state directory
|
|
407
|
+
const pluginDir = path.join(PLUGINS_DIR, name);
|
|
408
|
+
await rm(pluginDir, { recursive: true, force: true }).catch((err) => {
|
|
409
|
+
logger.warn({ err: err.message }, 'Failed to remove plugin state directory');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Remove from registry
|
|
413
|
+
s.begin.run();
|
|
414
|
+
try {
|
|
415
|
+
s.deleteByName.run(name);
|
|
416
|
+
s.commit.run();
|
|
417
|
+
} catch (err) {
|
|
418
|
+
s.rollback.run();
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Sweep this plugin's capabilities off every agent so re-installing the
|
|
423
|
+
// plugin later does NOT silently re-grant. Failure here is non-fatal
|
|
424
|
+
// (the plugin is already gone), but logged loudly.
|
|
425
|
+
try {
|
|
426
|
+
const route = derivePluginRoute(plugin.name);
|
|
427
|
+
const diffs = await revokePluginCapabilitiesFromAgents(PKI_DIR, route);
|
|
428
|
+
if (diffs.length > 0) {
|
|
429
|
+
logger.info({ name, route, diffs }, 'Revoked plugin capabilities from agents on uninstall');
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
logger.warn(
|
|
433
|
+
{ name, err: err.message },
|
|
434
|
+
'Failed to sweep plugin capabilities from agent registry on uninstall',
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
logger.info({ name }, 'Plugin uninstalled');
|
|
439
|
+
return { ok: true, name };
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Enable a plugin.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} name - The plugin name
|
|
447
|
+
* @param {import('pino').Logger} [logger]
|
|
448
|
+
* @returns {Promise<{ ok: true, name: string, status: string }>}
|
|
449
|
+
*/
|
|
450
|
+
export function enablePlugin(name, logger) {
|
|
451
|
+
return withPluginLock(async () => {
|
|
452
|
+
const s = await getStmts();
|
|
453
|
+
const row = s.selectByName.get(name);
|
|
454
|
+
|
|
455
|
+
if (!row) {
|
|
456
|
+
throw Object.assign(new Error(`Plugin "${name}" not found`), { statusCode: 404 });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (row.status === 'enabled') {
|
|
460
|
+
return { ok: true, name, status: 'enabled' };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
s.begin.run();
|
|
464
|
+
try {
|
|
465
|
+
s.updateStatusEnable.run(new Date().toISOString(), name);
|
|
466
|
+
s.commit.run();
|
|
467
|
+
} catch (err) {
|
|
468
|
+
s.rollback.run();
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
logger?.info({ name }, 'Plugin enabled');
|
|
473
|
+
return { ok: true, name, status: 'enabled' };
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Disable a plugin.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} name - The plugin name
|
|
481
|
+
* @param {import('pino').Logger} [logger]
|
|
482
|
+
* @returns {Promise<{ ok: true, name: string, status: string }>}
|
|
483
|
+
*/
|
|
484
|
+
export function disablePlugin(name, logger) {
|
|
485
|
+
return withPluginLock(async () => {
|
|
486
|
+
const s = await getStmts();
|
|
487
|
+
const row = s.selectByName.get(name);
|
|
488
|
+
|
|
489
|
+
if (!row) {
|
|
490
|
+
throw Object.assign(new Error(`Plugin "${name}" not found`), { statusCode: 404 });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (row.status === 'disabled') {
|
|
494
|
+
return { ok: true, name, status: 'disabled' };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
s.begin.run();
|
|
498
|
+
try {
|
|
499
|
+
s.updateStatusDisable.run(name);
|
|
500
|
+
s.commit.run();
|
|
501
|
+
} catch (err) {
|
|
502
|
+
s.rollback.run();
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
logger?.info({ name }, 'Plugin disabled');
|
|
507
|
+
return { ok: true, name, status: 'disabled' };
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get all enabled plugins.
|
|
513
|
+
*
|
|
514
|
+
* @returns {Promise<Array>}
|
|
515
|
+
*/
|
|
516
|
+
export async function getEnabledPlugins() {
|
|
517
|
+
const s = await getStmts();
|
|
518
|
+
const rows = s.selectAll.all();
|
|
519
|
+
return rows.filter((r) => r.status === 'enabled').map(rowToPlugin);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get capabilities contributed by all installed plugins.
|
|
524
|
+
*
|
|
525
|
+
* @returns {Promise<string[]>}
|
|
526
|
+
*/
|
|
527
|
+
export async function getPluginCapabilities() {
|
|
528
|
+
const s = await getStmts();
|
|
529
|
+
const rows = s.selectAll.all();
|
|
530
|
+
const caps = [];
|
|
531
|
+
for (const row of rows) {
|
|
532
|
+
if (row.status === 'enabled' && row.capabilities) {
|
|
533
|
+
const parsed = JSON.parse(row.capabilities);
|
|
534
|
+
if (Array.isArray(parsed)) caps.push(...parsed);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return [...new Set(caps)];
|
|
538
|
+
}
|