@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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup reconciliation of the chisel server — fail closed.
|
|
3
|
+
*
|
|
4
|
+
* Every time the panel starts it brings chisel in line with persisted state:
|
|
5
|
+
*
|
|
6
|
+
* 1. Every active agent has a chisel credential (fresh install, or agents
|
|
7
|
+
* enrolled while credential minting failed).
|
|
8
|
+
* 2. Tunnels persisted before tunnels had owners are bound where the owner is
|
|
9
|
+
* certain (see `bindUnownedTunnels`); the rest stay unowned and dark.
|
|
10
|
+
* 3. Once onboarding has completed: the server key exists and the authfile
|
|
11
|
+
* is rewritten from state with its ownership and mode re-applied.
|
|
12
|
+
* 4. Chisel is restarted when a revocation requires it, and started if it is
|
|
13
|
+
* enabled but not running.
|
|
14
|
+
*
|
|
15
|
+
* The binary (the pinned release) and the unit are create-lamaste's, which
|
|
16
|
+
* installs them as root and restarts chisel when either changes. The panel
|
|
17
|
+
* only checks the installed version: an older chisel does not reload its
|
|
18
|
+
* authfile reliably, so until the pinned release runs, chisel-runtime.js
|
|
19
|
+
* restarts it on every authfile change.
|
|
20
|
+
*
|
|
21
|
+
* Fail closed: if steps 1–3 cannot establish a correct authfile, chisel is
|
|
22
|
+
* stopped AND disabled, and a marker file records why. An authfile left by
|
|
23
|
+
* an older version may grant every agent every port (`.*`) — keeping that
|
|
24
|
+
* live because reconciliation failed would silently re-open what this
|
|
25
|
+
* version closes, and a disabled unit keeps a reboot from starting it before
|
|
26
|
+
* the panel has reconciled. Reconciliation is retried with backoff until it
|
|
27
|
+
* succeeds; then chisel is enabled and started again and the marker removed.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { execa } from 'execa';
|
|
31
|
+
import { rm, writeFile, access } from 'node:fs/promises';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { AGENT_LABEL_REGEX, CHISEL_RELEASE } from '@lamalibre/lamaste';
|
|
34
|
+
import { bindUnownedTunnels, withTunnelLock, withheldTunnels } from '@lamalibre/lamaste/server';
|
|
35
|
+
import { readTunnels, writeTunnels } from './state.js';
|
|
36
|
+
import { loadAgentRegistry } from './mtls.js';
|
|
37
|
+
import { migrateChiselCredentialsIfNeeded, syncChiselAuthfile } from './chisel-users.js';
|
|
38
|
+
import {
|
|
39
|
+
ensureChiselKey,
|
|
40
|
+
getInstalledChiselVersion,
|
|
41
|
+
isChiselRunning,
|
|
42
|
+
reloadChisel,
|
|
43
|
+
startChisel,
|
|
44
|
+
stopChisel,
|
|
45
|
+
} from './chisel.js';
|
|
46
|
+
import { setChiselRunningPinnedRelease } from './chisel-runtime.js';
|
|
47
|
+
import { getConfig } from './config.js';
|
|
48
|
+
|
|
49
|
+
/** Chisel is the panel's to run once onboarding has completed. */
|
|
50
|
+
function isOnboarded() {
|
|
51
|
+
return getConfig().onboarding?.status === 'COMPLETED';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Present while chisel is held down by a failed reconciliation. */
|
|
55
|
+
function failClosedMarker() {
|
|
56
|
+
const stateDir = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
57
|
+
return path.join(stateDir, 'chisel-failed-closed');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function isFailClosed() {
|
|
61
|
+
try {
|
|
62
|
+
await access(failClosedMarker());
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const RETRY_MIN_MS = 30_000;
|
|
70
|
+
const RETRY_MAX_MS = 10 * 60_000;
|
|
71
|
+
|
|
72
|
+
async function isChiselEnabled() {
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execa('systemctl', ['is-enabled', 'chisel']);
|
|
75
|
+
return stdout.trim() === 'enabled';
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function bindOwnership(logger) {
|
|
82
|
+
await withTunnelLock(async () => {
|
|
83
|
+
const tunnels = await readTunnels();
|
|
84
|
+
const registry = await loadAgentRegistry();
|
|
85
|
+
// Machine agents only: plugin-agent certificates carry no tunnels, and
|
|
86
|
+
// counting them would make "the sole agent" ambiguous.
|
|
87
|
+
const activeLabels = registry.agents
|
|
88
|
+
.filter((a) => !a.revoked && AGENT_LABEL_REGEX.test(a.label))
|
|
89
|
+
.map((a) => a.label);
|
|
90
|
+
|
|
91
|
+
const { tunnels: next, bound, unowned } = bindUnownedTunnels(tunnels, activeLabels);
|
|
92
|
+
if (bound.length > 0) {
|
|
93
|
+
await writeTunnels(next);
|
|
94
|
+
logger.info({ bound }, 'Bound ownerless tunnels to the agent that carries them');
|
|
95
|
+
}
|
|
96
|
+
const withheld = withheldTunnels(bound.length > 0 ? next : tunnels);
|
|
97
|
+
if (withheld.length > 0) {
|
|
98
|
+
logger.warn(
|
|
99
|
+
{ subdomains: withheld.map((t) => t.subdomain) },
|
|
100
|
+
'Tunnels on a reserved port or a port another tunnel also claims are carried by no ' +
|
|
101
|
+
'agent — delete them and recreate each on a free port',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (unowned.length > 0) {
|
|
105
|
+
logger.warn(
|
|
106
|
+
{ subdomains: unowned, activeAgents: activeLabels.length },
|
|
107
|
+
'Tunnels without an owning agent are carried by no agent until an administrator ' +
|
|
108
|
+
'assigns one (PATCH /api/tunnels/:id with agentLabel)',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One reconciliation pass.
|
|
116
|
+
* @returns {Promise<{ provisioned: boolean, binaryPinned: boolean }>}
|
|
117
|
+
*/
|
|
118
|
+
export async function reconcileChiselServer(logger) {
|
|
119
|
+
const provisioned = isOnboarded();
|
|
120
|
+
let migrateRevoked = false;
|
|
121
|
+
let revoked;
|
|
122
|
+
try {
|
|
123
|
+
// Its authfile write can already be the one that withdraws something
|
|
124
|
+
// (state persisted before a crash, but never synced) — keep that for the
|
|
125
|
+
// restart decision below.
|
|
126
|
+
({ revoked: migrateRevoked } = await migrateChiselCredentialsIfNeeded(
|
|
127
|
+
loadAgentRegistry,
|
|
128
|
+
logger,
|
|
129
|
+
));
|
|
130
|
+
await bindOwnership(logger);
|
|
131
|
+
if (!provisioned) {
|
|
132
|
+
// Onboarding has not started chisel yet; keep the authfile correct
|
|
133
|
+
// for when it does.
|
|
134
|
+
await syncChiselAuthfile({ force: true });
|
|
135
|
+
return { provisioned: false, binaryPinned: false };
|
|
136
|
+
}
|
|
137
|
+
await ensureChiselKey();
|
|
138
|
+
({ revoked } = await syncChiselAuthfile({ force: true }));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
await failClosed(logger, provisioned);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
revoked = revoked || migrateRevoked;
|
|
144
|
+
|
|
145
|
+
const installed = await getInstalledChiselVersion();
|
|
146
|
+
const binaryPinned = installed === CHISEL_RELEASE.version;
|
|
147
|
+
if (!binaryPinned) {
|
|
148
|
+
logger.error(
|
|
149
|
+
{ installed, pinned: CHISEL_RELEASE.version },
|
|
150
|
+
'The installed chisel is not the pinned release — run create-lamaste on the server to ' +
|
|
151
|
+
'install it. Until then chisel is restarted on every authfile change.',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (await isFailClosed()) {
|
|
156
|
+
await execa('sudo', ['systemctl', 'enable', 'chisel']);
|
|
157
|
+
await startChisel();
|
|
158
|
+
await rm(failClosedMarker(), { force: true });
|
|
159
|
+
logger.info({}, 'Chisel re-enabled and started: reconciliation succeeded');
|
|
160
|
+
} else if (await isChiselRunning()) {
|
|
161
|
+
if (revoked) {
|
|
162
|
+
await reloadChisel();
|
|
163
|
+
logger.info({}, 'Chisel restarted after reconciliation withdrew access');
|
|
164
|
+
}
|
|
165
|
+
} else if (await isChiselEnabled()) {
|
|
166
|
+
await startChisel();
|
|
167
|
+
logger.info({}, 'Chisel started after reconciliation');
|
|
168
|
+
}
|
|
169
|
+
setChiselRunningPinnedRelease(binaryPinned);
|
|
170
|
+
return { provisioned: true, binaryPinned };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function failClosed(logger, provisioned) {
|
|
174
|
+
setChiselRunningPinnedRelease(false);
|
|
175
|
+
if (!provisioned) return;
|
|
176
|
+
// Stopping comes first and does not depend on the other two steps.
|
|
177
|
+
try {
|
|
178
|
+
await stopChisel();
|
|
179
|
+
} catch (stopErr) {
|
|
180
|
+
logger.fatal(
|
|
181
|
+
{ err: stopErr },
|
|
182
|
+
'Chisel could not be reconciled and could not be stopped — check chisel-users by hand',
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
await writeFile(failClosedMarker(), `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
188
|
+
await execa('sudo', ['systemctl', 'disable', 'chisel']);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger.error(
|
|
191
|
+
{ err },
|
|
192
|
+
'Chisel is stopped but could not be disabled; a reboot would start it before the panel ' +
|
|
193
|
+
'reconciles',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
logger.error(
|
|
197
|
+
{},
|
|
198
|
+
'Chisel stopped: its authfile could not be brought in line with the tunnel ' +
|
|
199
|
+
'state, and an authfile from an older version may grant more than it should. ' +
|
|
200
|
+
'Tunnels stay down until reconciliation succeeds; it is retried automatically.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run reconciliation in the background now and, on failure, again with
|
|
206
|
+
* exponential backoff until it succeeds. Never throws and never blocks
|
|
207
|
+
* startup: the panel must stay reachable meanwhile — it is how an
|
|
208
|
+
* administrator fixes whatever made reconciliation fail.
|
|
209
|
+
*/
|
|
210
|
+
export function startChiselReconciler(logger) {
|
|
211
|
+
let delay = RETRY_MIN_MS;
|
|
212
|
+
const attempt = async () => {
|
|
213
|
+
try {
|
|
214
|
+
await reconcileChiselServer(logger);
|
|
215
|
+
return true;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
logger.error({ err }, 'Chisel reconciliation failed');
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const schedule = () => {
|
|
222
|
+
const timer = setTimeout(async () => {
|
|
223
|
+
if (await attempt()) return;
|
|
224
|
+
delay = Math.min(delay * 2, RETRY_MAX_MS);
|
|
225
|
+
schedule();
|
|
226
|
+
}, delay);
|
|
227
|
+
timer.unref();
|
|
228
|
+
};
|
|
229
|
+
void attempt().then((ok) => {
|
|
230
|
+
if (!ok) schedule();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When an authfile change must restart chisel.
|
|
3
|
+
*
|
|
4
|
+
* Chisel >= 1.12.0 reloads its authfile on every atomic replace, and the
|
|
5
|
+
* reload applies to new sessions: a new agent, a new password or a new port
|
|
6
|
+
* grant takes effect without a restart. What a live session already holds —
|
|
7
|
+
* its authenticated user and the reverse listeners it bound — survives a
|
|
8
|
+
* reload, so a change that *revokes* something (a removed or rotated
|
|
9
|
+
* password, a withdrawn port) needs a restart to take effect.
|
|
10
|
+
*
|
|
11
|
+
* An older chisel does not reload reliably across atomic renames, so until
|
|
12
|
+
* this process has seen chisel running the pinned release, every change
|
|
13
|
+
* restarts it. Startup reconciliation and onboarding set that flag after they
|
|
14
|
+
* (re)start chisel on the pinned binary.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execa } from 'execa';
|
|
18
|
+
import { reloadChiselAuth } from '@lamalibre/lamaste/server';
|
|
19
|
+
|
|
20
|
+
let runningPinnedRelease = false;
|
|
21
|
+
|
|
22
|
+
/** Record whether the chisel process now running is the pinned release. */
|
|
23
|
+
export function setChiselRunningPinnedRelease(value) {
|
|
24
|
+
runningPinnedRelease = value === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when an additive authfile change takes effect without a restart. */
|
|
28
|
+
export function chiselReloadsAuthfile() {
|
|
29
|
+
return runningPinnedRelease;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Restart chisel if the change requires it. `try-restart` leaves a stopped
|
|
34
|
+
* chisel stopped (see chisel-reconcile.js: a server that failed to reconcile
|
|
35
|
+
* keeps chisel down until it succeeds).
|
|
36
|
+
*
|
|
37
|
+
* @param {{ changed: boolean, revoked: boolean }} change
|
|
38
|
+
* @param {import('pino').Logger} [logger]
|
|
39
|
+
* @returns {Promise<{ restartOk: boolean, restartError?: string }>}
|
|
40
|
+
*/
|
|
41
|
+
export async function applyAuthfileChange(change, logger) {
|
|
42
|
+
const needsRestart = change.revoked || (change.changed && !runningPinnedRelease);
|
|
43
|
+
if (!needsRestart) return { restartOk: true };
|
|
44
|
+
const result = await reloadChiselAuth(execa, logger);
|
|
45
|
+
return result.ok ? { restartOk: true } : { restartOk: false, restartError: result.error };
|
|
46
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shim — chisel authfile credential management lives in
|
|
3
|
+
* `@lamalibre/lamaste/server`. This file resolves the chisel credential /
|
|
4
|
+
* authfile paths from env, wires the daemon's execa instance and the
|
|
5
|
+
* persisted tunnel state (the source of per-agent port grants) to the
|
|
6
|
+
* parameterized core API, and applies the restart policy of
|
|
7
|
+
* chisel-runtime.js to every change.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { execa } from 'execa';
|
|
12
|
+
import {
|
|
13
|
+
addChiselCredential as addChiselCredentialCore,
|
|
14
|
+
removeChiselCredential as removeChiselCredentialCore,
|
|
15
|
+
rotateChiselCredential as rotateChiselCredentialCore,
|
|
16
|
+
getChiselCredential as getChiselCredentialCore,
|
|
17
|
+
getChiselCredentialIssuedAt as getChiselCredentialIssuedAtCore,
|
|
18
|
+
migrateChiselCredentialsIfNeeded as migrateChiselCredentialsIfNeededCore,
|
|
19
|
+
loadChiselCredentials as loadChiselCredentialsCore,
|
|
20
|
+
syncChiselAuthfile as syncChiselAuthfileCore,
|
|
21
|
+
} from '@lamalibre/lamaste/server';
|
|
22
|
+
import { readTunnels } from './state.js';
|
|
23
|
+
import { applyAuthfileChange } from './chisel-runtime.js';
|
|
24
|
+
|
|
25
|
+
function paths() {
|
|
26
|
+
const stateDir = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
27
|
+
return {
|
|
28
|
+
credentialsFile: path.join(stateDir, 'chisel-credentials.json'),
|
|
29
|
+
authFilePath:
|
|
30
|
+
process.env.LAMALIBRE_LAMASTE_CHISEL_AUTHFILE || path.join(stateDir, 'chisel-users'),
|
|
31
|
+
sentinelFile: path.join(stateDir, 'chisel-sentinel'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadChiselCredentials() {
|
|
36
|
+
return loadChiselCredentialsCore(paths());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mint a credential for a new agent. Hot-reloaded by chisel; no restart.
|
|
41
|
+
* @returns {Promise<{ user: string, password: string, createdAt: string, restartOk: boolean, restartError?: string }>}
|
|
42
|
+
*/
|
|
43
|
+
export async function addChiselCredential(label, logger) {
|
|
44
|
+
const result = await addChiselCredentialCore(label, paths(), readTunnels, execa);
|
|
45
|
+
const restart = await applyAuthfileChange(result, logger);
|
|
46
|
+
return { user: result.user, password: result.password, createdAt: result.createdAt, ...restart };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Remove an agent's credential. Restarts chisel so a live session using it ends.
|
|
51
|
+
* @returns {Promise<{ removed: boolean, restartOk: boolean, restartError?: string }>}
|
|
52
|
+
*/
|
|
53
|
+
export async function removeChiselCredential(label, logger) {
|
|
54
|
+
const result = await removeChiselCredentialCore(label, paths(), readTunnels, execa);
|
|
55
|
+
const restart = await applyAuthfileChange(result, logger);
|
|
56
|
+
return { removed: result.removed, ...restart };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Replace an agent's credential. Restarts chisel so the old password stops working.
|
|
61
|
+
* @returns {Promise<{ user: string, password: string, createdAt: string, restartOk: boolean, restartError?: string }>}
|
|
62
|
+
*/
|
|
63
|
+
export async function rotateChiselCredential(label, logger) {
|
|
64
|
+
const result = await rotateChiselCredentialCore(label, paths(), readTunnels, execa);
|
|
65
|
+
const restart = await applyAuthfileChange(result, logger);
|
|
66
|
+
return { user: result.user, password: result.password, createdAt: result.createdAt, ...restart };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getChiselCredential(label) {
|
|
70
|
+
return getChiselCredentialCore(label, paths());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** When the agent's current chisel password was issued (not secret), or null. */
|
|
74
|
+
export function getChiselCredentialIssuedAt(label) {
|
|
75
|
+
return getChiselCredentialIssuedAtCore(label, paths());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mint credentials for active agents that lack one and re-render the
|
|
80
|
+
* authfile. Does not restart chisel — the caller (onboarding, startup
|
|
81
|
+
* reconciliation) owns the chisel lifecycle at that point.
|
|
82
|
+
*/
|
|
83
|
+
export function migrateChiselCredentialsIfNeeded(loadAgentRegistry, logger) {
|
|
84
|
+
return migrateChiselCredentialsIfNeededCore(
|
|
85
|
+
loadAgentRegistry,
|
|
86
|
+
paths(),
|
|
87
|
+
readTunnels,
|
|
88
|
+
execa,
|
|
89
|
+
logger,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Re-render the authfile from the credential store and tunnel state.
|
|
95
|
+
* `force` rewrites unchanged content too, re-applying ownership and mode.
|
|
96
|
+
* @param {{ force?: boolean }} [options]
|
|
97
|
+
* @returns {Promise<{ changed: boolean, revoked: boolean }>}
|
|
98
|
+
*/
|
|
99
|
+
export function syncChiselAuthfile(options = {}) {
|
|
100
|
+
return syncChiselAuthfileCore(paths(), readTunnels, execa, options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getAuthfilePath() {
|
|
104
|
+
return paths().authFilePath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getCredentialsPath() {
|
|
108
|
+
return paths().credentialsFile;
|
|
109
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shim — chisel service lifecycle lives in `@lamalibre/lamaste/server`.
|
|
3
|
+
* This file wires the daemon's execa instance and resolved authfile path
|
|
4
|
+
* to the parameterized core API.
|
|
5
|
+
*
|
|
6
|
+
* The binary and the unit are create-lamaste's (installed as root); the
|
|
7
|
+
* panel manages the key, the authfile and the service state.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
import { access } from 'node:fs/promises';
|
|
12
|
+
import { CHISEL_BIN } from '@lamalibre/lamaste/server';
|
|
13
|
+
import {
|
|
14
|
+
getInstalledChiselVersion as getInstalledChiselVersionCore,
|
|
15
|
+
ensureChiselKey as ensureChiselKeyCore,
|
|
16
|
+
startChisel as startChiselCore,
|
|
17
|
+
reloadChisel as reloadChiselCore,
|
|
18
|
+
stopChisel as stopChiselCore,
|
|
19
|
+
isChiselRunning as isChiselRunningCore,
|
|
20
|
+
getChiselStatus as getChiselStatusCore,
|
|
21
|
+
} from '@lamalibre/lamaste/server';
|
|
22
|
+
import { syncChiselAuthfile } from './chisel-users.js';
|
|
23
|
+
import { applyAuthfileChange } from './chisel-runtime.js';
|
|
24
|
+
|
|
25
|
+
function keyFilePath() {
|
|
26
|
+
return process.env.LAMALIBRE_LAMASTE_CHISEL_KEYFILE || '/etc/lamalibre/lamaste/chisel-server.key';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True once create-lamaste has installed the chisel binary. */
|
|
30
|
+
export async function isChiselInstalled() {
|
|
31
|
+
try {
|
|
32
|
+
await access(CHISEL_BIN);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getInstalledChiselVersion() {
|
|
40
|
+
return getInstalledChiselVersionCore(execa);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ensureChiselKey() {
|
|
44
|
+
return ensureChiselKeyCore(keyFilePath(), execa);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function startChisel() {
|
|
48
|
+
return startChiselCore(execa);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function reloadChisel() {
|
|
52
|
+
return reloadChiselCore(execa);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function stopChisel() {
|
|
56
|
+
return stopChiselCore(execa);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isChiselRunning() {
|
|
60
|
+
return isChiselRunningCore(execa);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getChiselStatus() {
|
|
64
|
+
return getChiselStatusCore(execa);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Bring chisel in line with the persisted tunnel state: re-render the
|
|
69
|
+
* authfile (each agent may bind only the ports of the enabled tunnels it
|
|
70
|
+
* owns). Chisel reloads the file by itself; when the new file withdraws a
|
|
71
|
+
* grant, chisel is restarted so live sessions drop the binding. Call only
|
|
72
|
+
* after the tunnel state has been written. Throws if the restart fails,
|
|
73
|
+
* since a withdrawn grant would otherwise stay live.
|
|
74
|
+
*/
|
|
75
|
+
export async function syncChisel() {
|
|
76
|
+
const change = await syncChiselAuthfile();
|
|
77
|
+
const { restartOk, restartError } = await applyAuthfileChange(change);
|
|
78
|
+
if (!restartOk) {
|
|
79
|
+
throw new Error(`Chisel restart failed after withdrawing a port grant: ${restartError}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile, rename, open } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const ConfigSchema = z.object({
|
|
8
|
+
ip: z.string().min(1),
|
|
9
|
+
domain: z.string().nullable(),
|
|
10
|
+
email: z.string().email().nullable(),
|
|
11
|
+
dataDir: z.string().min(1),
|
|
12
|
+
serverId: z.string().uuid().optional(),
|
|
13
|
+
staticDir: z.string().optional(),
|
|
14
|
+
maxSiteSize: z
|
|
15
|
+
.number()
|
|
16
|
+
.optional()
|
|
17
|
+
.default(500 * 1024 * 1024),
|
|
18
|
+
adminAuthMode: z.enum(['p12', 'hardware-bound']).optional().default('p12'),
|
|
19
|
+
panel2fa: z
|
|
20
|
+
.object({
|
|
21
|
+
enabled: z.boolean(),
|
|
22
|
+
secret: z.string().nullable(),
|
|
23
|
+
setupComplete: z.boolean(),
|
|
24
|
+
})
|
|
25
|
+
.optional()
|
|
26
|
+
.default({ enabled: false, secret: null, setupComplete: false }),
|
|
27
|
+
sessionSecret: z.string().nullable().optional().default(null),
|
|
28
|
+
// Per-user invalidation epochs (Unix seconds). When a user is deleted from
|
|
29
|
+
// Authelia, their epoch is bumped so any in-flight user-access session token
|
|
30
|
+
// issued before that timestamp is rejected by the session middleware. The
|
|
31
|
+
// entry is preserved (never deleted) so that re-creating the same username
|
|
32
|
+
// does not resurrect old sessions.
|
|
33
|
+
userEpochs: z.record(z.string(), z.number().int().nonnegative()).optional().default({}),
|
|
34
|
+
onboarding: z.object({
|
|
35
|
+
status: z.enum(['FRESH', 'DOMAIN_SET', 'DNS_READY', 'PROVISIONING', 'COMPLETED']),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
40
|
+
const __dirname = path.dirname(__filename);
|
|
41
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
42
|
+
|
|
43
|
+
let config = null;
|
|
44
|
+
let configPath = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Promise-chain mutex serializing all panel.json writes so concurrent
|
|
48
|
+
* updateConfig() calls cannot race (e.g. two simultaneous user deletes both
|
|
49
|
+
* bumping userEpochs).
|
|
50
|
+
*/
|
|
51
|
+
let configWriteLock = Promise.resolve();
|
|
52
|
+
function withConfigLock(fn) {
|
|
53
|
+
const prev = configWriteLock;
|
|
54
|
+
let resolve;
|
|
55
|
+
configWriteLock = new Promise((r) => {
|
|
56
|
+
resolve = r;
|
|
57
|
+
});
|
|
58
|
+
return prev.then(fn).finally(resolve);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveConfigPath() {
|
|
62
|
+
if (process.env.LAMALIBRE_LAMASTE_CONFIG) {
|
|
63
|
+
return process.env.LAMALIBRE_LAMASTE_CONFIG;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isDev = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
|
|
67
|
+
|
|
68
|
+
if (isDev) {
|
|
69
|
+
return path.resolve(packageRoot, 'dev', 'panel.json');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return '/etc/lamalibre/lamaste/panel.json';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadConfig() {
|
|
76
|
+
configPath = resolveConfigPath();
|
|
77
|
+
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = await readFile(configPath, 'utf-8');
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err.code === 'ENOENT') {
|
|
83
|
+
throw new Error(`Config file not found at ${configPath}. Run create-lamaste to initialize.`);
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Failed to read config file at ${configPath}: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(raw);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`Config file at ${configPath} contains invalid JSON.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const validated = ConfigSchema.parse(parsed);
|
|
96
|
+
|
|
97
|
+
// Auto-generate serverId if missing — used as bucket prefix for multi-server isolation
|
|
98
|
+
if (!validated.serverId) {
|
|
99
|
+
validated.serverId = crypto.randomUUID();
|
|
100
|
+
const tmpPath = `${configPath}.tmp`;
|
|
101
|
+
await writeFile(tmpPath, JSON.stringify(validated, null, 2) + '\n', {
|
|
102
|
+
encoding: 'utf-8',
|
|
103
|
+
mode: 0o600,
|
|
104
|
+
});
|
|
105
|
+
const fd = await open(tmpPath, 'r');
|
|
106
|
+
await fd.sync();
|
|
107
|
+
await fd.close();
|
|
108
|
+
await rename(tmpPath, configPath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
config = validated;
|
|
112
|
+
return structuredClone(config);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getConfig() {
|
|
116
|
+
if (config === null) {
|
|
117
|
+
throw new Error('Config not loaded. Call loadConfig() first.');
|
|
118
|
+
}
|
|
119
|
+
return structuredClone(config);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function updateConfig(patch) {
|
|
123
|
+
if (config === null) {
|
|
124
|
+
throw new Error('Config not loaded. Call loadConfig() first.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return withConfigLock(async () => {
|
|
128
|
+
const merged = structuredClone(config);
|
|
129
|
+
|
|
130
|
+
for (const key of Object.keys(patch)) {
|
|
131
|
+
if (
|
|
132
|
+
key === 'onboarding' &&
|
|
133
|
+
typeof patch.onboarding === 'object' &&
|
|
134
|
+
patch.onboarding !== null
|
|
135
|
+
) {
|
|
136
|
+
merged.onboarding = { ...merged.onboarding, ...patch.onboarding };
|
|
137
|
+
} else if (
|
|
138
|
+
key === 'panel2fa' &&
|
|
139
|
+
typeof patch.panel2fa === 'object' &&
|
|
140
|
+
patch.panel2fa !== null
|
|
141
|
+
) {
|
|
142
|
+
merged.panel2fa = { ...merged.panel2fa, ...patch.panel2fa };
|
|
143
|
+
} else if (
|
|
144
|
+
key === 'userEpochs' &&
|
|
145
|
+
typeof patch.userEpochs === 'object' &&
|
|
146
|
+
patch.userEpochs !== null
|
|
147
|
+
) {
|
|
148
|
+
merged.userEpochs = { ...(merged.userEpochs ?? {}), ...patch.userEpochs };
|
|
149
|
+
} else {
|
|
150
|
+
merged[key] = patch[key];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const validated = ConfigSchema.parse(merged);
|
|
155
|
+
|
|
156
|
+
const tmpPath = `${configPath}.tmp`;
|
|
157
|
+
await writeFile(tmpPath, JSON.stringify(validated, null, 2) + '\n', {
|
|
158
|
+
encoding: 'utf-8',
|
|
159
|
+
mode: 0o600,
|
|
160
|
+
});
|
|
161
|
+
const fd = await open(tmpPath, 'r');
|
|
162
|
+
await fd.sync();
|
|
163
|
+
await fd.close();
|
|
164
|
+
await rename(tmpPath, configPath);
|
|
165
|
+
|
|
166
|
+
config = validated;
|
|
167
|
+
return structuredClone(config);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Bump a user's session-invalidation epoch to "now" (Unix seconds). Any
|
|
173
|
+
* user-access session whose `iat` (issued-at) is older than this epoch
|
|
174
|
+
* is considered revoked by the session middleware.
|
|
175
|
+
*
|
|
176
|
+
* Idempotent: calling twice in quick succession is safe (the later
|
|
177
|
+
* timestamp simply replaces the earlier one).
|
|
178
|
+
*
|
|
179
|
+
* @param {string} username
|
|
180
|
+
* @returns {Promise<number>} The new epoch (Unix seconds)
|
|
181
|
+
*/
|
|
182
|
+
export async function bumpUserEpoch(username) {
|
|
183
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
184
|
+
await updateConfig({ userEpochs: { [username]: nowSeconds } });
|
|
185
|
+
return nowSeconds;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getConfigPath() {
|
|
189
|
+
return configPath ?? resolveConfigPath();
|
|
190
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export core constants from @lamalibre/lamaste.
|
|
3
|
+
*
|
|
4
|
+
* During the transition, existing imports within lamaste-serverd continue
|
|
5
|
+
* to resolve via this file. New code should import directly from the core
|
|
6
|
+
* library.
|
|
7
|
+
*/
|
|
8
|
+
export { RESERVED_API_PREFIXES } from '@lamalibre/lamaste';
|