@lamalibre/create-lamaste 4.2.0 → 4.3.1
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/bundle-vendor.js +13 -8
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +8 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/panel-pin.js +41 -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 +110 -35
- 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 +4 -4
- package/vendor/docs/02-guides/desktop-app-setup.md +11 -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 +8 -4
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +3 -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 +195 -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/cli/index.js +0 -0
- 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 +25 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/panel-pin.js +40 -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 +277 -34
- package/vendor/server/src/panel-identity.js +165 -0
- package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -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,145 @@
|
|
|
1
|
+
// Lamaste's audit log — an append-only record in state.db of administrative
|
|
2
|
+
// decisions (hook approvals, auto-approve changes, hostings, plugin updates)
|
|
3
|
+
// and of what plugins' lifecycle hooks report through the `audit` effect.
|
|
4
|
+
//
|
|
5
|
+
// Entries hold metadata only. `details` is a flat-ish JSON object capped at
|
|
6
|
+
// AUDIT_DETAILS_MAX_BYTES; strings in it are truncated. Nothing here is ever
|
|
7
|
+
// a request or response body or a secret — callers pass identifiers, counts
|
|
8
|
+
// and reasons. The log is pruned by age and count, never edited.
|
|
9
|
+
|
|
10
|
+
import { getStateDb } from './state-db.js';
|
|
11
|
+
|
|
12
|
+
export const AUDIT_RETENTION_DAYS = 180;
|
|
13
|
+
export const AUDIT_MAX_ROWS = 50_000;
|
|
14
|
+
export const AUDIT_DETAILS_MAX_BYTES = 4096;
|
|
15
|
+
const AUDIT_STRING_MAX = 300;
|
|
16
|
+
const PRUNE_EVERY = 500;
|
|
17
|
+
|
|
18
|
+
const ACTION_RE = /^[a-z][a-z0-9.-]{0,99}$/;
|
|
19
|
+
const ACTOR_RE = /^[A-Za-z0-9:._@-]{1,200}$/;
|
|
20
|
+
const PLUGIN_RE = /^[a-z0-9-]{1,100}$/;
|
|
21
|
+
|
|
22
|
+
let insertsSincePrune = 0;
|
|
23
|
+
|
|
24
|
+
function sanitize(value, depth = 0) {
|
|
25
|
+
if (value === null || typeof value === 'boolean') return value;
|
|
26
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return value.length > AUDIT_STRING_MAX ? `${value.slice(0, AUDIT_STRING_MAX)}…` : value;
|
|
29
|
+
}
|
|
30
|
+
if (depth >= 3) return null;
|
|
31
|
+
if (Array.isArray(value)) return value.slice(0, 50).map((v) => sanitize(v, depth + 1));
|
|
32
|
+
if (typeof value === 'object') {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [key, v] of Object.entries(value).slice(0, 50)) {
|
|
35
|
+
if (v !== undefined) out[String(key).slice(0, 64)] = sanitize(v, depth + 1);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Serialise `details`, dropping everything when it would exceed the cap. */
|
|
43
|
+
function encodeDetails(details) {
|
|
44
|
+
const json = JSON.stringify(sanitize(details ?? {}) ?? {});
|
|
45
|
+
if (Buffer.byteLength(json) <= AUDIT_DETAILS_MAX_BYTES) return json;
|
|
46
|
+
return JSON.stringify({ truncated: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Append one entry.
|
|
51
|
+
*
|
|
52
|
+
* @param {{ actor: string, action: string, plugin?: string | null, details?: object }} entry
|
|
53
|
+
* `actor` — `admin`, `agent:<label>`, `plugin:<route>` (a hook's effect),
|
|
54
|
+
* `lamaste` (the panel itself), `auto-approve`; `action` — dotted lower case
|
|
55
|
+
* (`hooks.approved`, `plugin.updated`, `hook.secret-scan.redacted`).
|
|
56
|
+
*/
|
|
57
|
+
export async function recordAudit({ actor, action, plugin = null, details = {} }) {
|
|
58
|
+
if (!ACTOR_RE.test(actor)) throw new Error(`Invalid audit actor "${actor}"`);
|
|
59
|
+
if (!ACTION_RE.test(action)) throw new Error(`Invalid audit action "${action}"`);
|
|
60
|
+
if (plugin !== null && !PLUGIN_RE.test(plugin)) throw new Error('Invalid audit plugin');
|
|
61
|
+
const db = await getStateDb();
|
|
62
|
+
const at = new Date().toISOString();
|
|
63
|
+
const result = db
|
|
64
|
+
.prepare('INSERT INTO audit_events (at, actor, action, plugin, details) VALUES (?, ?, ?, ?, ?)')
|
|
65
|
+
.run(at, actor, action, plugin, encodeDetails(details));
|
|
66
|
+
insertsSincePrune++;
|
|
67
|
+
if (insertsSincePrune >= PRUNE_EVERY) {
|
|
68
|
+
insertsSincePrune = 0;
|
|
69
|
+
pruneAudit(db);
|
|
70
|
+
}
|
|
71
|
+
return { id: Number(result.lastInsertRowid), at };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Append, logging instead of throwing — for paths where the audited action
|
|
76
|
+
* already happened and a full disk must not turn it into an error.
|
|
77
|
+
*/
|
|
78
|
+
export async function recordAuditBestEffort(entry, logger) {
|
|
79
|
+
try {
|
|
80
|
+
return await recordAudit(entry);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger?.error?.({ err: err.message, action: entry.action }, 'Failed to write audit entry');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pruneAudit(db) {
|
|
88
|
+
const cutoff = new Date(Date.now() - AUDIT_RETENTION_DAYS * 86_400_000).toISOString();
|
|
89
|
+
db.prepare('DELETE FROM audit_events WHERE at < ?').run(cutoff);
|
|
90
|
+
db.prepare(
|
|
91
|
+
'DELETE FROM audit_events WHERE id <= (SELECT id FROM audit_events ORDER BY id DESC LIMIT 1 OFFSET ?)',
|
|
92
|
+
).run(AUDIT_MAX_ROWS);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function rowToEntry(row) {
|
|
96
|
+
let details;
|
|
97
|
+
try {
|
|
98
|
+
details = JSON.parse(row.details);
|
|
99
|
+
} catch {
|
|
100
|
+
details = {};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: row.id,
|
|
104
|
+
at: row.at,
|
|
105
|
+
actor: row.actor,
|
|
106
|
+
action: row.action,
|
|
107
|
+
plugin: row.plugin,
|
|
108
|
+
details,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Newest first. `before` pages backwards by id; `action` matches exactly or,
|
|
114
|
+
* ending in `.`, as a prefix (`hooks.`).
|
|
115
|
+
*/
|
|
116
|
+
export async function listAudit({ plugin, action, before, limit = 50 } = {}) {
|
|
117
|
+
const db = await getStateDb();
|
|
118
|
+
const where = [];
|
|
119
|
+
const args = [];
|
|
120
|
+
if (plugin) {
|
|
121
|
+
where.push('plugin = ?');
|
|
122
|
+
args.push(plugin);
|
|
123
|
+
}
|
|
124
|
+
if (action) {
|
|
125
|
+
if (action.endsWith('.')) {
|
|
126
|
+
where.push('substr(action, 1, ?) = ?');
|
|
127
|
+
args.push(action.length, action);
|
|
128
|
+
} else {
|
|
129
|
+
where.push('action = ?');
|
|
130
|
+
args.push(action);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (before) {
|
|
134
|
+
where.push('id < ?');
|
|
135
|
+
args.push(before);
|
|
136
|
+
}
|
|
137
|
+
const sql = `SELECT id, at, actor, action, plugin, details FROM audit_events${
|
|
138
|
+
where.length ? ` WHERE ${where.join(' AND ')}` : ''
|
|
139
|
+
} ORDER BY id DESC LIMIT ?`;
|
|
140
|
+
args.push(Math.min(Math.max(1, limit), 200));
|
|
141
|
+
return db
|
|
142
|
+
.prepare(sql)
|
|
143
|
+
.all(...args)
|
|
144
|
+
.map(rowToEntry);
|
|
145
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effects of relay hooks — the only way a hook changes anything outside its
|
|
3
|
+
* return value. Lamaste performs them with its own credentials:
|
|
4
|
+
*
|
|
5
|
+
* - `audit` an audit log entry, actor `plugin:<route>`, action `hook.<event>`
|
|
6
|
+
* - `notify-admins` a panel notification
|
|
7
|
+
* - `log` one metadata line in the panel's log
|
|
8
|
+
*
|
|
9
|
+
* A hook may only use the effect types its plugin declared (`hooks.relay.effects`,
|
|
10
|
+
* shown in the approval) or, for a shared hook, the ones Lamaste defines for
|
|
11
|
+
* it. Each type is rate-limited per plugin over a sliding minute; what is
|
|
12
|
+
* refused or over the limit is dropped and counted, and the count is
|
|
13
|
+
* audited at most once a minute per plugin.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { HOOK_EFFECT_LIMITS } from '@lamalibre/lamaste/hooks';
|
|
17
|
+
import { recordAuditBestEffort } from './audit.js';
|
|
18
|
+
import { notifyAdminsBestEffort } from './notifications.js';
|
|
19
|
+
|
|
20
|
+
const WINDOW_MS = 60_000;
|
|
21
|
+
|
|
22
|
+
/** `${plugin}\0${type}` → issue times within the window. */
|
|
23
|
+
const windows = new Map();
|
|
24
|
+
/** plugin → { refused, overLimit, lastReportAt } */
|
|
25
|
+
const drops = new Map();
|
|
26
|
+
|
|
27
|
+
function takeToken(plugin, type, now) {
|
|
28
|
+
const key = `${plugin}\0${type}`;
|
|
29
|
+
const limit = HOOK_EFFECT_LIMITS.perMinute[type];
|
|
30
|
+
const times = (windows.get(key) ?? []).filter((t) => now - t < WINDOW_MS);
|
|
31
|
+
if (times.length >= limit) {
|
|
32
|
+
windows.set(key, times);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
times.push(now);
|
|
36
|
+
windows.set(key, times);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function reportDrops(plugin, stage, now, logger) {
|
|
41
|
+
const d = drops.get(plugin);
|
|
42
|
+
if (!d || (d.refused === 0 && d.overLimit === 0)) return;
|
|
43
|
+
if (now - d.lastReportAt < WINDOW_MS) return;
|
|
44
|
+
const { refused, overLimit } = d;
|
|
45
|
+
drops.set(plugin, { refused: 0, overLimit: 0, lastReportAt: now });
|
|
46
|
+
await recordAuditBestEffort(
|
|
47
|
+
{
|
|
48
|
+
actor: 'lamaste',
|
|
49
|
+
action: 'hooks.effects-dropped',
|
|
50
|
+
plugin,
|
|
51
|
+
details: { stage, refused, overLimit },
|
|
52
|
+
},
|
|
53
|
+
logger,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function countDrop(plugin, kind) {
|
|
58
|
+
const d = drops.get(plugin) ?? { refused: 0, overLimit: 0, lastReportAt: 0 };
|
|
59
|
+
d[kind]++;
|
|
60
|
+
drops.set(plugin, d);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Perform the effects of one decision.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} plugin
|
|
67
|
+
* @param {string} stage
|
|
68
|
+
* @param {ReadonlyArray<import('@lamalibre/lamaste/hooks').HookEffect>} effects validated
|
|
69
|
+
* @param {ReadonlySet<string>} allowed effect types this hook may use
|
|
70
|
+
* @param {import('pino').Logger} [logger]
|
|
71
|
+
*/
|
|
72
|
+
export async function performHookEffects(plugin, stage, effects, allowed, logger) {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const effect of effects) {
|
|
75
|
+
if (!allowed.has(effect.type)) {
|
|
76
|
+
countDrop(plugin, 'refused');
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!takeToken(plugin, effect.type, now)) {
|
|
80
|
+
countDrop(plugin, 'overLimit');
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
switch (effect.type) {
|
|
84
|
+
case 'audit':
|
|
85
|
+
await recordAuditBestEffort(
|
|
86
|
+
{
|
|
87
|
+
actor: `plugin:${plugin}`,
|
|
88
|
+
action: `hook.${effect.event}`,
|
|
89
|
+
plugin,
|
|
90
|
+
details: { stage, ...(effect.data ?? {}) },
|
|
91
|
+
},
|
|
92
|
+
logger,
|
|
93
|
+
);
|
|
94
|
+
break;
|
|
95
|
+
case 'notify-admins':
|
|
96
|
+
await notifyAdminsBestEffort(
|
|
97
|
+
{ source: `plugin:${plugin}`, level: effect.level ?? 'info', message: effect.message },
|
|
98
|
+
logger,
|
|
99
|
+
);
|
|
100
|
+
break;
|
|
101
|
+
case 'log': {
|
|
102
|
+
const line = {
|
|
103
|
+
plugin,
|
|
104
|
+
stage,
|
|
105
|
+
hookLog: effect.message,
|
|
106
|
+
...(effect.data ? { data: effect.data } : {}),
|
|
107
|
+
};
|
|
108
|
+
if (effect.level === 'warn') logger?.warn(line, 'Plugin hook log');
|
|
109
|
+
else logger?.info(line, 'Plugin hook log');
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
countDrop(plugin, 'refused');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await reportDrops(plugin, stage, now, logger);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Forget rate-limit state (tests). */
|
|
120
|
+
export function resetHookEffectLimitsForTests() {
|
|
121
|
+
windows.clear();
|
|
122
|
+
drops.clear();
|
|
123
|
+
}
|