@lamalibre/create-lamaste 4.2.0 → 4.3.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/package.json +2 -2
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +4 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +98 -34
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +7 -3
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +190 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +18 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +32 -4
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calling one relay hook: the runner round trip, validation of the
|
|
3
|
+
* decision, the effects it asks for, and the runner restart a timeout calls
|
|
4
|
+
* for. Every pipeline (install, uninstall, request, tunnel state) calls
|
|
5
|
+
* relay hooks through {@link runRelayHook}.
|
|
6
|
+
*
|
|
7
|
+
* What is logged is metadata: plugin, stage, outcome, duration. A failing
|
|
8
|
+
* request-stage hook's message is never logged — it is plugin text and may
|
|
9
|
+
* quote the request or response it saw.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { SHARED_HOOK_EFFECTS } from '@lamalibre/lamaste/hooks';
|
|
13
|
+
import {
|
|
14
|
+
callRelayHook,
|
|
15
|
+
decisionEffects,
|
|
16
|
+
hookRunnerSocketPath,
|
|
17
|
+
validateHookDecision,
|
|
18
|
+
} from '@lamalibre/lamaste/server';
|
|
19
|
+
import { performHookEffects } from './hook-effects.js';
|
|
20
|
+
import { hookSocketDir, restartRelayHookRunner } from './plugin-hooks.js';
|
|
21
|
+
|
|
22
|
+
function isRequestStage(stage) {
|
|
23
|
+
return stage === 'request:pre-forward' || stage === 'request:post-forward';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {{
|
|
28
|
+
* plugin: string,
|
|
29
|
+
* stage: import('@lamalibre/lamaste/hooks').HookStage,
|
|
30
|
+
* call: import('@lamalibre/lamaste/hooks').HookCallTarget,
|
|
31
|
+
* input: Record<string, unknown>,
|
|
32
|
+
* body?: { bytes: Buffer, contentType: string | null } | null,
|
|
33
|
+
* timeoutMs: number,
|
|
34
|
+
* effects: ReadonlyArray<string>,
|
|
35
|
+
* logger?: import('pino').Logger,
|
|
36
|
+
* }} params
|
|
37
|
+
* `effects` — the effect types the plugin declared (for its own hooks;
|
|
38
|
+
* a shared hook uses Lamaste's list).
|
|
39
|
+
* @returns {Promise<
|
|
40
|
+
* | { ok: true, decision: object, body: Buffer | null, bodyIsText: boolean, durationMs: number }
|
|
41
|
+
* | { ok: false, kind: string, message: string, durationMs: number }
|
|
42
|
+
* >}
|
|
43
|
+
*/
|
|
44
|
+
export async function runRelayHook({
|
|
45
|
+
plugin,
|
|
46
|
+
stage,
|
|
47
|
+
call,
|
|
48
|
+
input,
|
|
49
|
+
body,
|
|
50
|
+
timeoutMs,
|
|
51
|
+
effects,
|
|
52
|
+
logger,
|
|
53
|
+
}) {
|
|
54
|
+
const outcome = await callRelayHook(hookRunnerSocketPath(hookSocketDir(), plugin), {
|
|
55
|
+
plugin,
|
|
56
|
+
stage,
|
|
57
|
+
call,
|
|
58
|
+
input,
|
|
59
|
+
body: body ?? null,
|
|
60
|
+
timeoutMs,
|
|
61
|
+
});
|
|
62
|
+
const requestStage = isRequestStage(stage);
|
|
63
|
+
if (!outcome.ok) {
|
|
64
|
+
if (outcome.kind === 'timeout') void restartRelayHookRunner(plugin, logger);
|
|
65
|
+
logger?.warn(
|
|
66
|
+
{
|
|
67
|
+
plugin,
|
|
68
|
+
stage,
|
|
69
|
+
hook: call.kind === 'shared' ? call.name : 'plugin',
|
|
70
|
+
kind: outcome.kind,
|
|
71
|
+
durationMs: outcome.durationMs,
|
|
72
|
+
...(requestStage ? {} : { error: outcome.message }),
|
|
73
|
+
},
|
|
74
|
+
'Relay hook failed',
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
kind: outcome.kind,
|
|
79
|
+
message: requestStage ? '' : outcome.message,
|
|
80
|
+
durationMs: outcome.durationMs,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const validated = validateHookDecision(stage, outcome.decision);
|
|
84
|
+
if (!validated.ok) {
|
|
85
|
+
logger?.warn(
|
|
86
|
+
{ plugin, stage, error: validated.error },
|
|
87
|
+
'Relay hook returned an invalid decision',
|
|
88
|
+
);
|
|
89
|
+
return { ok: false, kind: 'invalid', message: validated.error, durationMs: outcome.durationMs };
|
|
90
|
+
}
|
|
91
|
+
const allowed = new Set(call.kind === 'shared' ? SHARED_HOOK_EFFECTS[call.name] : effects);
|
|
92
|
+
await performHookEffects(plugin, stage, decisionEffects(validated.decision), allowed, logger);
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
decision: validated.decision,
|
|
96
|
+
body: outcome.body,
|
|
97
|
+
bodyIsText: outcome.bodyIsText,
|
|
98
|
+
durationMs: outcome.durationMs,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relay's assertion signing key (`<stateDir>/relay-signing-key.json`,
|
|
3
|
+
* 0600). The panel creates it at startup and signs agent-panel requests
|
|
4
|
+
* with it, and every app job it gives an agent's root helper
|
|
5
|
+
* (`agent-jobs.js`); Gatekeeper reads the same file to sign for plugin
|
|
6
|
+
* tunnels. Agents fetch the public half from `GET /api/agents/me/relay-keys`.
|
|
7
|
+
* See `relay-assertion.ts` and `app-job-signature.ts` in the core library.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createRelaySigner, relayVerifyKeyOf, RELAY_SIGNING_KEY_FILE } from '@lamalibre/lamaste';
|
|
12
|
+
|
|
13
|
+
const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
14
|
+
|
|
15
|
+
export const relaySigner = createRelaySigner(path.join(STATE_DIR, RELAY_SIGNING_KEY_FILE), {
|
|
16
|
+
create: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create the key if it does not exist yet, so Gatekeeper finds it, and log
|
|
21
|
+
* its id. A key that exists but cannot be used (permissions, content) is an
|
|
22
|
+
* error the operator must fix: nothing is signed with it, so the agent-panel
|
|
23
|
+
* and plugin tunnels are refused until then.
|
|
24
|
+
*
|
|
25
|
+
* @param {{ info(o: object, m?: string): void, error(o: object, m?: string): void }} log
|
|
26
|
+
*/
|
|
27
|
+
export async function ensureRelaySigningKey(log) {
|
|
28
|
+
try {
|
|
29
|
+
const key = await relaySigner.current();
|
|
30
|
+
if (key) log.info({ kid: key.kid }, 'Relay assertion signing key ready');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
log.error(
|
|
33
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
34
|
+
'Relay assertion signing key unusable — agent panels and plugin tunnels are refused',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The public keys agents verify relay assertions with (current key only). */
|
|
40
|
+
export async function relayVerifyKeys() {
|
|
41
|
+
const key = await relaySigner.current();
|
|
42
|
+
return key ? [relayVerifyKeyOf(key)] : [];
|
|
43
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel's request hook runtime for the forwarder
|
|
3
|
+
* (`createHookedForwarder` in @lamalibre/lamaste/server): which request
|
|
4
|
+
* hooks a plugin has, how one runs, and the fallback while it is down.
|
|
5
|
+
*
|
|
6
|
+
* Only an approved relay hook set runs. A plugin whose installed set is not
|
|
7
|
+
* the approved one has its forwarded traffic refused (503) — letting it past
|
|
8
|
+
* hooks that do not run would be a bypass.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { findPlugin } from './plugins.js';
|
|
12
|
+
import { installedRelayHooks, installedVersionApproved } from './plugin-hooks.js';
|
|
13
|
+
import { runRelayHook } from './relay-hooks.js';
|
|
14
|
+
import { getStateDb } from './state-db.js';
|
|
15
|
+
|
|
16
|
+
/** plugin → the fallback its `tunnel:down` hook returned, while it is down. */
|
|
17
|
+
const fallbacks = new Map();
|
|
18
|
+
|
|
19
|
+
/** plugin → declared effect types of its relay hooks (from the last `hooksFor`). */
|
|
20
|
+
const declaredEffects = new Map();
|
|
21
|
+
|
|
22
|
+
/** Load the persisted fallbacks (start-up). */
|
|
23
|
+
export async function loadTunnelFallbacks() {
|
|
24
|
+
const db = await getStateDb();
|
|
25
|
+
fallbacks.clear();
|
|
26
|
+
for (const row of db
|
|
27
|
+
.prepare(
|
|
28
|
+
'SELECT plugin, tunnel_fallback FROM plugin_hook_sets WHERE tunnel_fallback IS NOT NULL',
|
|
29
|
+
)
|
|
30
|
+
.all()) {
|
|
31
|
+
try {
|
|
32
|
+
fallbacks.set(row.plugin, JSON.parse(row.tunnel_fallback));
|
|
33
|
+
} catch {
|
|
34
|
+
// A malformed row serves no fallback.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Keep the in-memory fallback in step with the persisted one (tunnel-hooks.js). */
|
|
40
|
+
export function rememberTunnelFallback(plugin, fallback) {
|
|
41
|
+
if (fallback) fallbacks.set(plugin, fallback);
|
|
42
|
+
else fallbacks.delete(plugin);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {import('@lamalibre/lamaste/server').RequestHookRuntime} */
|
|
46
|
+
export const requestHookRuntime = {
|
|
47
|
+
async hooksFor(plugin) {
|
|
48
|
+
const entry = await findPlugin(plugin);
|
|
49
|
+
const relay = entry ? installedRelayHooks(entry) : null;
|
|
50
|
+
if (!relay) return { hooks: [] };
|
|
51
|
+
if (!(await installedVersionApproved(entry))) {
|
|
52
|
+
return {
|
|
53
|
+
refuse: `The relay hooks of plugin "${plugin}" are not approved — an administrator approves them in the panel`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
declaredEffects.set(plugin, relay.set.effects);
|
|
57
|
+
return { hooks: relay.set.request };
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async run(plugin, hook, input, body) {
|
|
61
|
+
const result = await runRelayHook({
|
|
62
|
+
plugin,
|
|
63
|
+
stage: `request:${hook.stage}`,
|
|
64
|
+
call: hook.use
|
|
65
|
+
? { kind: 'shared', name: hook.use, settings: hook.settings }
|
|
66
|
+
: { kind: 'app' },
|
|
67
|
+
input,
|
|
68
|
+
body,
|
|
69
|
+
timeoutMs: hook.timeoutMs,
|
|
70
|
+
effects: declaredEffects.get(plugin) ?? [],
|
|
71
|
+
});
|
|
72
|
+
if (!result.ok) return { ok: false };
|
|
73
|
+
return { ok: true, decision: result.decision, body: result.body };
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
fallbackFor(plugin) {
|
|
77
|
+
return fallbacks.get(plugin) ?? null;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data an agent half leaves on an agent. Removing an agent half without
|
|
3
|
+
* purge (an unhost, an uninstall, a rolled-back install) keeps its data
|
|
4
|
+
* directory and service account on the agent, both named after the route;
|
|
5
|
+
* the relay keeps a record of it so an administrator can purge it later —
|
|
6
|
+
* after the plugin is gone, too — and so another agent package is never
|
|
7
|
+
* installed over it.
|
|
8
|
+
*
|
|
9
|
+
* One record per agent and route (`retained_agent_data`, migration 0010),
|
|
10
|
+
* the way the data exists on the agent. A record goes when the data is
|
|
11
|
+
* purged, or when the same agent package is installed there again and
|
|
12
|
+
* takes the data back.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { APPHOST_PATHS, appServiceUser } from '@lamalibre/lamaste';
|
|
16
|
+
import { getStateDb } from './state-db.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A record as the API shows it: what the table keeps, and what a purge
|
|
20
|
+
* deletes on the agent — the data directory and the account, both derived
|
|
21
|
+
* from the route — so a panel or CLI confirmation can name them.
|
|
22
|
+
*/
|
|
23
|
+
function rowToRecord(row) {
|
|
24
|
+
return {
|
|
25
|
+
agentLabel: row.agent_label,
|
|
26
|
+
route: row.route,
|
|
27
|
+
plugin: row.plugin,
|
|
28
|
+
package: row.package,
|
|
29
|
+
since: row.since,
|
|
30
|
+
operation: row.operation,
|
|
31
|
+
dataDir: APPHOST_PATHS.dataDir(row.route),
|
|
32
|
+
serviceUser: appServiceUser(row.route),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Record that agent `agentLabel` keeps the data of route `route`, owned by
|
|
38
|
+
* agent package `pkg` of plugin `plugin`. A record that exists already keeps
|
|
39
|
+
* its `since` — the data has been there since then.
|
|
40
|
+
*
|
|
41
|
+
* @param {{ agentLabel: string, route: string, plugin: string, package: string, operation?: string | null }} record
|
|
42
|
+
*/
|
|
43
|
+
export async function recordRetainedData({ agentLabel, route, plugin, package: pkg, operation }) {
|
|
44
|
+
const db = await getStateDb();
|
|
45
|
+
db.prepare(
|
|
46
|
+
`INSERT INTO retained_agent_data (agent_label, route, plugin, package, since, operation)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
48
|
+
ON CONFLICT(agent_label, route) DO UPDATE SET
|
|
49
|
+
plugin = excluded.plugin,
|
|
50
|
+
package = excluded.package,
|
|
51
|
+
operation = excluded.operation`,
|
|
52
|
+
).run(agentLabel, route, plugin, pkg, new Date().toISOString(), operation ?? null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Drop the record of the data agent `agentLabel` keeps for route `route` (purged, or taken back). */
|
|
56
|
+
export async function forgetRetainedData(agentLabel, route) {
|
|
57
|
+
const db = await getStateDb();
|
|
58
|
+
return Number(
|
|
59
|
+
db
|
|
60
|
+
.prepare('DELETE FROM retained_agent_data WHERE agent_label = ? AND route = ?')
|
|
61
|
+
.run(agentLabel, route).changes,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The record for agent `agentLabel` and route `route`, or null. */
|
|
66
|
+
export async function findRetainedData(agentLabel, route) {
|
|
67
|
+
const db = await getStateDb();
|
|
68
|
+
const row = db
|
|
69
|
+
.prepare('SELECT * FROM retained_agent_data WHERE agent_label = ? AND route = ?')
|
|
70
|
+
.get(agentLabel, route);
|
|
71
|
+
return row ? rowToRecord(row) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Records, oldest first — all of them, those of plugin `plugin` (by the name
|
|
76
|
+
* it had; it may be uninstalled), or those on agent `agentLabel`.
|
|
77
|
+
*
|
|
78
|
+
* @param {{ plugin?: string, agentLabel?: string }} [filter]
|
|
79
|
+
*/
|
|
80
|
+
export async function listRetainedData({ plugin, agentLabel } = {}) {
|
|
81
|
+
const db = await getStateDb();
|
|
82
|
+
const where = [];
|
|
83
|
+
const args = [];
|
|
84
|
+
if (plugin) {
|
|
85
|
+
where.push('plugin = ?');
|
|
86
|
+
args.push(plugin);
|
|
87
|
+
}
|
|
88
|
+
if (agentLabel) {
|
|
89
|
+
where.push('agent_label = ?');
|
|
90
|
+
args.push(agentLabel);
|
|
91
|
+
}
|
|
92
|
+
return db
|
|
93
|
+
.prepare(
|
|
94
|
+
`SELECT * FROM retained_agent_data${where.length ? ` WHERE ${where.join(' AND ')}` : ''}
|
|
95
|
+
ORDER BY since, agent_label, route`,
|
|
96
|
+
)
|
|
97
|
+
.all(...args)
|
|
98
|
+
.map(rowToRecord);
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which proxy hop the panel trusts for a request's client address — the
|
|
3
|
+
* Fastify `trustProxy` function.
|
|
4
|
+
*
|
|
5
|
+
* The panel listens on 127.0.0.1 only; every request reaches it through the
|
|
6
|
+
* relay's nginx (the panel vhost, the IP-based panel on :9292), which
|
|
7
|
+
* appends the address it accepted the connection from to
|
|
8
|
+
* `X-Forwarded-For` (`proxy_set_header X-Forwarded-For
|
|
9
|
+
* $proxy_add_x_forwarded_for`). The panel trusts exactly that one hop, and
|
|
10
|
+
* only when it comes from loopback: `request.ip` is then the address nginx
|
|
11
|
+
* saw — what push-install IP policies, rate limits and logs see — never an
|
|
12
|
+
* `X-Forwarded-For` entry the client wrote itself (those sit further left),
|
|
13
|
+
* and never a header from a peer that is not local should the panel ever
|
|
14
|
+
* listen elsewhere.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import net from 'node:net';
|
|
18
|
+
|
|
19
|
+
/** Whether `address` is a loopback address (IPv4 127/8, `::1`, or IPv4-mapped 127/8). */
|
|
20
|
+
export function isLoopbackAddress(address) {
|
|
21
|
+
if (typeof address !== 'string') return false;
|
|
22
|
+
const v4 = address.startsWith('::ffff:') ? address.slice(7) : address;
|
|
23
|
+
if (net.isIPv4(v4)) return v4.split('.')[0] === '127';
|
|
24
|
+
return net.isIPv6(address) && (address === '::1' || /^(0{1,4}:){7}0{0,3}1$/.test(address));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fastify / proxy-addr trust function: `hop` 0 is the connection's own peer.
|
|
29
|
+
* @param {string} address
|
|
30
|
+
* @param {number} hop
|
|
31
|
+
*/
|
|
32
|
+
export function trustLocalNginxHop(address, hop) {
|
|
33
|
+
return hop === 0 && isLoopbackAddress(address);
|
|
34
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tunnel-state pipeline on the relay: when the health monitor sees a
|
|
3
|
+
* hosted plugin go down (the tunnel, or the application behind it) or come
|
|
4
|
+
* back, Lamaste runs the plugin's `tunnel:down` / `tunnel:up` relay hook.
|
|
5
|
+
* What `tunnel:down` returns as `fallback` is what the forwarder answers
|
|
6
|
+
* while the plugin is down, instead of a bare 502; `tunnel:up` clears it.
|
|
7
|
+
*
|
|
8
|
+
* `tunnel:up` runs only after a `down` — not when a hosting is first seen
|
|
9
|
+
* up, or after a restart of the panel. The agent side of the pipeline runs
|
|
10
|
+
* on the agent (its sync follows the relay's view of the tunnel).
|
|
11
|
+
*
|
|
12
|
+
* Calls for one plugin are serialised and run beside the monitor, which
|
|
13
|
+
* does not wait for them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { findPlugin } from './plugins.js';
|
|
17
|
+
import {
|
|
18
|
+
installedRelayHooks,
|
|
19
|
+
installedVersionApproved,
|
|
20
|
+
setTunnelFallback,
|
|
21
|
+
} from './plugin-hooks.js';
|
|
22
|
+
import { runRelayHook } from './relay-hooks.js';
|
|
23
|
+
import { rememberTunnelFallback } from './request-hooks.js';
|
|
24
|
+
|
|
25
|
+
const chains = new Map();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* React to a persisted health transition of `plugin` (`before` → `after`).
|
|
29
|
+
* Never throws; failures are logged.
|
|
30
|
+
*/
|
|
31
|
+
export function onHealthTransition(plugin, before, after, logger) {
|
|
32
|
+
const previous = chains.get(plugin) ?? Promise.resolve();
|
|
33
|
+
const next = previous
|
|
34
|
+
.then(() => runTunnelHook(plugin, before, after, logger))
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
logger?.error({ plugin, err: err?.message }, 'Tunnel-state hook pipeline failed');
|
|
37
|
+
});
|
|
38
|
+
chains.set(plugin, next);
|
|
39
|
+
next.finally(() => {
|
|
40
|
+
if (chains.get(plugin) === next) chains.delete(plugin);
|
|
41
|
+
});
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runTunnelHook(plugin, before, after, logger) {
|
|
46
|
+
let stage;
|
|
47
|
+
if (after.state === 'down') stage = 'tunnel:down';
|
|
48
|
+
else if (after.state === 'up' && before?.state === 'down') stage = 'tunnel:up';
|
|
49
|
+
else return;
|
|
50
|
+
|
|
51
|
+
const entry = await findPlugin(plugin);
|
|
52
|
+
const relay = entry ? installedRelayHooks(entry) : null;
|
|
53
|
+
const decl = relay?.set.stages.find((s) => s.stage === stage) ?? null;
|
|
54
|
+
if (stage === 'tunnel:up') {
|
|
55
|
+
// Whatever happens next, the plugin is up: no fallback.
|
|
56
|
+
await setTunnelFallback(plugin, null);
|
|
57
|
+
rememberTunnelFallback(plugin, null);
|
|
58
|
+
}
|
|
59
|
+
if (!decl) return;
|
|
60
|
+
if (!(await installedVersionApproved(entry))) return;
|
|
61
|
+
|
|
62
|
+
const result = await runRelayHook({
|
|
63
|
+
plugin,
|
|
64
|
+
stage,
|
|
65
|
+
call: { kind: 'app' },
|
|
66
|
+
input: {
|
|
67
|
+
state: after.state,
|
|
68
|
+
...(after.reason ? { reason: after.reason } : {}),
|
|
69
|
+
since: after.since,
|
|
70
|
+
...(after.lastError ? { lastError: after.lastError.slice(0, 300) } : {}),
|
|
71
|
+
},
|
|
72
|
+
timeoutMs: decl.timeoutMs,
|
|
73
|
+
effects: relay.set.effects,
|
|
74
|
+
logger,
|
|
75
|
+
});
|
|
76
|
+
if (!result.ok) return;
|
|
77
|
+
if (stage === 'tunnel:down') {
|
|
78
|
+
const fallback = result.decision.fallback ?? null;
|
|
79
|
+
await setTunnelFallback(plugin, fallback);
|
|
80
|
+
rememberTunnelFallback(plugin, fallback);
|
|
81
|
+
logger?.info(
|
|
82
|
+
{ plugin, reason: after.reason, fallback: fallback !== null },
|
|
83
|
+
'Ran tunnel:down hook',
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
logger?.info({ plugin }, 'Ran tunnel:up hook');
|
|
87
|
+
}
|
|
88
|
+
}
|