@lamalibre/create-lamaste 4.6.0 → 4.7.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/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +22 -3
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -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 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- 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 +5 -5
- 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/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- 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 +15 -5
- package/vendor/docs/03-architecture/overview.md +5 -5
- 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 +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +17 -13
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +35 -7
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +5 -0
- package/vendor/docs/04-api-reference/tunnels.md +15 -10
- package/vendor/docs/04-api-reference/users.md +41 -29
- 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 +6 -6
- package/vendor/docs/06-reference/config-files.md +1 -1
- 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 +8 -8
- package/vendor/docs/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- 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/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +5 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +28 -2
- 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 +225 -36
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugins.js +112 -8
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +3 -0
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/{index-BZwQ_aQc.css → index-Cb-ADsbB.css} +1 -1
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +42 -16
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +20 -4
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +318 -140
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +1 -0
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugins.js +45 -5
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +11 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +29 -4
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +149 -41
- package/vendor/serverd/src/lib/web-surface.js +49 -32
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugins.js +4 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +50 -11
- package/vendor/serverd/src/routes/management/tunnels.js +29 -5
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +2 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +0 -84
|
@@ -10,9 +10,9 @@ The client has two modes. On first visit, it shows an onboarding wizard that wal
|
|
|
10
10
|
|
|
11
11
|
## Shared Panel Packages
|
|
12
12
|
|
|
13
|
-
The management-mode admin pages (Dashboard, Tunnels, Sites, Users, Certificates, Services, Plugins, Tickets, Logs, Settings) live in `@lamalibre/lamaste-server-ui@4.
|
|
13
|
+
The management-mode admin pages (Dashboard, Tunnels, Sites, Users, Certificates, Services, Plugins, Tickets, Logs, Settings) live in `@lamalibre/lamaste-server-ui@4.7.0`. This package provides both the browser SPA build and the shared admin components consumed by `lamaste-desktop` (Tauri app).
|
|
14
14
|
|
|
15
|
-
The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are extracted into a separate shared package: `@lamalibre/lamaste-agent-ui@4.
|
|
15
|
+
The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are extracted into a separate shared package: `@lamalibre/lamaste-agent-ui@4.7.0`. This package is consumed by `lamaste-desktop` for agent mode.
|
|
16
16
|
|
|
17
17
|
The shared package exposes an `AdminClientContext` abstraction that decouples admin UI pages from their data transport. Each host provides its own client implementation:
|
|
18
18
|
|
|
@@ -118,7 +118,7 @@ The shell renders the appropriate step component and shows a progress indicator
|
|
|
118
118
|
|
|
119
119
|
### Management Mode
|
|
120
120
|
|
|
121
|
-
React Router handles navigation within the `Layout` component (sidebar + content area). Management page components are imported from the shared `@lamalibre/lamaste-server-ui@4.
|
|
121
|
+
React Router handles navigation within the `Layout` component (sidebar + content area). Management page components are imported from the shared `@lamalibre/lamaste-server-ui@4.7.0` package and wrapped with the `AdminClientProvider` (which supplies the web-specific data client):
|
|
122
122
|
|
|
123
123
|
| Path | Component | Description |
|
|
124
124
|
| ------------------------ | ------------------- | ------------------------------------------------------------------- |
|
|
@@ -350,9 +350,9 @@ The app root wraps all content in five providers:
|
|
|
350
350
|
```
|
|
351
351
|
|
|
352
352
|
1. **QueryClientProvider** — react-query cache with global defaults (`retry: 1`, `refetchOnWindowFocus: false`)
|
|
353
|
-
2. **AdminClientProvider** — from `@lamalibre/lamaste-server-ui@4.
|
|
354
|
-
3. **ToastProvider** — from `@lamalibre/lamaste-server-ui@4.
|
|
355
|
-
4. **TwoFaProvider** — from `@lamalibre/lamaste-server-ui@4.
|
|
353
|
+
2. **AdminClientProvider** — from `@lamalibre/lamaste-server-ui@4.7.0`, supplies the host-specific data client (`webAdminClient` for browser fetch, Tauri `invoke` for desktop)
|
|
354
|
+
3. **ToastProvider** — from `@lamalibre/lamaste-server-ui@4.7.0`, notification toast system
|
|
355
|
+
4. **TwoFaProvider** — from `@lamalibre/lamaste-server-ui@4.7.0`, handles 2FA verification prompts when the panel has 2FA enabled
|
|
356
356
|
5. **BrowserRouter** — React Router for client-side navigation
|
|
357
357
|
|
|
358
358
|
## Documentation Viewer
|
|
@@ -453,7 +453,7 @@ See [nginx-configuration.md](./nginx-configuration.md) for detailed coverage.
|
|
|
453
453
|
|
|
454
454
|
### chisel-users.js — Chisel Credentials and Grants
|
|
455
455
|
|
|
456
|
-
- Thin shim over `@lamalibre/lamaste@4.
|
|
456
|
+
- Thin shim over `@lamalibre/lamaste@4.7.0/server`: per-agent credentials in `chisel-credentials.json` (0600)
|
|
457
457
|
- Renders the authfile `chisel-users` from the credentials and `tunnels.json`: each `agent-<label>` may bind exactly `^R:127\.0\.0\.1:<port>$` for its enabled tunnels
|
|
458
458
|
- Adds the `lamaste-no-grants` sentinel user (password in `chisel-sentinel`, 0600) so the authfile is never empty — Chisel disables authentication on an empty authfile
|
|
459
459
|
- Never grants a reserved port (3100, 9090, 9091, 9292, 9294) or a port claimed by two tunnels
|
|
@@ -536,7 +536,7 @@ Agent-to-agent authorization with scopes, instances, tickets, and sessions. Prov
|
|
|
536
536
|
- **Periodic cleanup** — stale/dead instances, expired tickets, dead sessions
|
|
537
537
|
- **Concurrency** — promise-chain mutex with atomic file writes (temp → fsync → rename)
|
|
538
538
|
|
|
539
|
-
Client SDK: `@lamalibre/lamaste-tickets@4.
|
|
539
|
+
Client SDK: `@lamalibre/lamaste-tickets@4.7.0` (TypeScript, undici) provides `TicketClient`, `TicketInstanceManager` (source side), and `TicketSessionManager` (target side) for plugin integration.
|
|
540
540
|
|
|
541
541
|
State files: `/etc/lamalibre/lamaste/ticket-scopes.json` (scopes, instances, assignments) and `/etc/lamalibre/lamaste/tickets.json` (tickets, sessions).
|
|
542
542
|
|
|
@@ -329,7 +329,7 @@ Read returns `[]` for missing files (ENOENT), enabling the system to start with
|
|
|
329
329
|
5. `DELETE /api/certs/agent/:label` (revocation) — the agent's app and plugin tunnels lose their `agentLabel` (unassigned), and its `agent-<label>` panel tunnel is removed
|
|
330
330
|
6. At startup (background, fail closed): an ownerless `agent-<label>` panel tunnel is bound to the agent it names when that agent is active; other ownerless tunnels (from versions before tunnel ownership) are bound to the sole active agent, or left unassigned when there are several. Then the pinned Chisel binary, the unit and the authfile are re-established — and Chisel is stopped if they cannot be
|
|
331
331
|
|
|
332
|
-
Every one of these workflows runs under the single tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.
|
|
332
|
+
Every one of these workflows runs under the single tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.7.0/server`), with its validation inside the lock.
|
|
333
333
|
|
|
334
334
|
## Site State (`sites.json`)
|
|
335
335
|
|
|
@@ -391,7 +391,7 @@ The Panel Server writes Authelia's files itself, without sudo (`writeAutheliaFil
|
|
|
391
391
|
2. `rename()`s it over the destination — atomic on the same filesystem, so Authelia never reads a partial file
|
|
392
392
|
3. The mode decides Authelia's access: `users.yml` `0660` (Authelia rewrites it when a user changes a password), `configuration.yml` `0640`, `.secrets.json` `0600` (the panel only)
|
|
393
393
|
|
|
394
|
-
**Critical invariant:**
|
|
394
|
+
**Critical invariant:** Authelia must see every write of `users.yml`. Authelia watches `users.yml` (`authentication_backend.file.watch: true`) and reloads it in place; with the watch it re-reads a user's details at each of their requests, so a change applies at the user's next request without a restart and without ending anyone else's session. It skips a reload within 0.5 s of the previous one, so the panel writes the file once more 0.6 s after its last write. Every write goes through one file lock in the panel (`lib/authelia.js`), which also keeps the groups Lamaste owns as the last rendering decided.
|
|
395
395
|
|
|
396
396
|
**Safety rule:** The Panel Server prevents deleting the last user in the Authelia database. Without at least one user, no one could authenticate to access tunneled apps.
|
|
397
397
|
|
|
@@ -482,7 +482,7 @@ The PKI directory belongs to `lamaste` because the panel is the CA (it signs age
|
|
|
482
482
|
|
|
483
483
|
The Panel Server is a single-process Node.js application, but a single-threaded event loop does not serialize asynchronous workflows: two requests that each read `tunnels.json`, await a certificate or an nginx test, and then write it back would interleave, and both could pass the same uniqueness check. Serialization is therefore explicit:
|
|
484
484
|
|
|
485
|
-
**Tunnel state** is guarded by one promise-chain lock, `withTunnelLock` in `@lamalibre/lamaste@4.
|
|
485
|
+
**Tunnel state** is guarded by one promise-chain lock, `withTunnelLock` in `@lamalibre/lamaste@4.7.0/server`'s `tunnels.ts`. Every tunnel workflow — create, delete, toggle, assign, reconfigure, the combined `PATCH /api/tunnels/:id`, releasing a revoked agent's tunnels, and startup reconciliation — takes it for its whole duration, and does its validation (subdomain and port uniqueness, reserved ports, agent checks, authorization against the current entry) inside it.
|
|
486
486
|
|
|
487
487
|
**Chisel authfile renders** are serialized by a promise-chain mutex in `chisel-users.ts`, keyed by the credentials file, around every render (enroll, revoke, rotate, tunnel change). The daemon's `syncChisel()` renders after the tunnel state is written and restarts Chisel (`systemctl try-restart`) only when the render withdrew something.
|
|
488
488
|
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
│ │ │ │
|
|
59
59
|
│ │ Lamaste Desktop (Tauri v2) — dual mode │ │
|
|
60
60
|
│ │ ├─ Agent mode: service discovery + tunnel management │ │
|
|
61
|
-
│ │ │ (pages from @lamalibre/lamaste-agent-ui@4.
|
|
61
|
+
│ │ │ (pages from @lamalibre/lamaste-agent-ui@4.7.0) │ │
|
|
62
62
|
│ │ ├─ Server mode: full admin panel (via lamaste-server-ui) │ │
|
|
63
63
|
│ │ ├─ Multi-server registry (~/.lamalibre/lamaste/servers.json) │ │
|
|
64
64
|
│ │ ├─ Cloud provisioning (DigitalOcean via lamaste-cloud) │ │
|
|
@@ -142,7 +142,7 @@ Every app job is signed by the relay with its signing key when it is created, an
|
|
|
142
142
|
|
|
143
143
|
### Host facts
|
|
144
144
|
|
|
145
|
-
Each agent reports its host on its sync when the facts change, when the relay does not hold them, and once a day: `PUT /api/agents/me/host-facts` (agent certificates only; 10 per minute, 4 KiB). What the relay holds it tells the agent in every [`GET /api/tunnels/agent-config`](tunnels.md#get-api-tunnels-agent-config) answer: `hostFactsHash`, the SHA-256 of the facts stored on the agent's record (`hostFactsHash` in `@lamalibre/lamaste@4.
|
|
145
|
+
Each agent reports its host on its sync when the facts change, when the relay does not hold them, and once a day: `PUT /api/agents/me/host-facts` (agent certificates only; 10 per minute, 4 KiB). What the relay holds it tells the agent in every [`GET /api/tunnels/agent-config`](tunnels.md#get-api-tunnels-agent-config) answer: `hostFactsHash`, the SHA-256 of the facts stored on the agent's record (`hostFactsHash` in `@lamalibre/lamaste@4.7.0`, the same function the agent hashes its own with), `null` when it stores none. When that is not the hash of the agent's facts, the sync reports them whatever it last reported — so an agent enrolled again on its label (a new record, without host facts) or a relay restored from a backup can host again after one sync, not a day later. Behind a relay that does not send the field, the agent reports as before.
|
|
146
146
|
|
|
147
147
|
```json
|
|
148
148
|
{
|
|
@@ -346,7 +346,7 @@ What the agent's host offers — see [Host facts](#host-facts).
|
|
|
346
346
|
|
|
347
347
|
`lamaste-agent sync` (the 30-second timer) fetches a plugin's config when it does not hold the secret issued at `secretIssuedAt`, writes `~/.lamalibre/lamaste/agents/<label>/app-plugins/<route>.json` (0600, atomic), acknowledges it, and removes the file of a plugin the relay no longer lists. For an agent half Lamaste installed it first writes the service's own copy, `/etc/lamalibre/apps/<route>/plugin-config.json`, and acknowledges only once both are written. `sync.log` records `plugin config fetched: <route>` and `plugin config delivered to its service: <route>`.
|
|
348
348
|
|
|
349
|
-
The agent half reads its plugin config — `LAMASTE_APP_CONFIG` for an agent half Lamaste installs, the file above for one installed by hand (`LAMALIBRE_LAMASTE_APP_PLUGIN_CONFIG` by convention) — and verifies every request on `/admin/*` with `createCallerAssertionVerifier` from `@lamalibre/lamaste@4.
|
|
349
|
+
The agent half reads its plugin config — `LAMASTE_APP_CONFIG` for an agent half Lamaste installs, the file above for one installed by hand (`LAMALIBRE_LAMASTE_APP_PLUGIN_CONFIG` by convention) — and verifies every request on `/admin/*` with `createCallerAssertionVerifier` from `@lamalibre/lamaste@4.7.0/agent`, which re-reads the file when a signature does not verify, so a rotation needs no restart. It listens where `agentHalfListenTarget` says: for an agent half Lamaste installs, systemd holds the port in a socket unit and passes the listening socket (descriptor 3), so the port stays bound while the agent half restarts or is updated and no other local process can take it in between — such an agent half must listen this way, and one that binds its port itself fails with `EADDRINUSE`; for one installed by hand, the config's port on `127.0.0.1` ([App Host](app-host.md#writing-an-agent-half)). Every answer it gives a verified request carries the responder signature ([below](#authentication-between-the-relay-and-the-agent-half)):
|
|
350
350
|
|
|
351
351
|
```js
|
|
352
352
|
import {
|
|
@@ -354,7 +354,7 @@ import {
|
|
|
354
354
|
createCallerAssertionVerifier,
|
|
355
355
|
createHealthHandler,
|
|
356
356
|
responderHeaderFor,
|
|
357
|
-
} from '@lamalibre/lamaste@4.
|
|
357
|
+
} from '@lamalibre/lamaste@4.7.0/agent';
|
|
358
358
|
|
|
359
359
|
const verifier = createCallerAssertionVerifier({ configPath, plugin: 'market-indicators' });
|
|
360
360
|
const health = createHealthHandler({ verifier, check: () => db.isOpen() });
|
|
@@ -420,7 +420,7 @@ Answers given before or instead of a verification — a `401` for a bad assertio
|
|
|
420
420
|
|
|
421
421
|
**Secret settings travel sealed** to the one request that carries them ([Between the relay and the agent half](#between-the-relay-and-the-agent-half)).
|
|
422
422
|
|
|
423
|
-
**Agent halves built against `@lamalibre/lamaste@4.
|
|
423
|
+
**Agent halves built against `@lamalibre/lamaste@4.7.0` 4.3 must be rebuilt** against this version: an agent half that does not sign its answers is down (`app`) to the health monitor, its pages and API answer `502`, and its settings and hooks fail as `unauthenticated`; one that expects secrets in clear refuses sealed ones.
|
|
424
424
|
|
|
425
425
|
## Health
|
|
426
426
|
|
|
@@ -570,13 +570,13 @@ The panel checks the change against the installed schema — without `pattern`
|
|
|
570
570
|
| 422 | — | The agent half refused it: `errors` per field — its `pattern`, its own schema, or a sealed secret that did not open ("Could not be decrypted") |
|
|
571
571
|
| 502 | `settings_unsupported` | The agent half does not answer `/admin/settings` (`404`) — it predates settings; update the plugin |
|
|
572
572
|
| 502 | `refused` | Nothing answers on the internal tunnel's port |
|
|
573
|
-
| 502 | `unauthenticated` | The answer did not carry a valid responder signature: something else may hold the tunnel's port, or the agent half was built against an older `@lamalibre/lamaste@4.
|
|
573
|
+
| 502 | `unauthenticated` | The answer did not carry a valid responder signature: something else may hold the tunnel's port, or the agent half was built against an older `@lamalibre/lamaste@4.7.0` and must be rebuilt. The change may or may not have been stored — read the settings again |
|
|
574
574
|
| 502 | `invalid`, `other` | The agent half answered too large a body or in another shape (reload the settings to see what it stored), or could not be reached |
|
|
575
575
|
| 502 | `agent_half_error` | The agent half answered another status (`status`) |
|
|
576
576
|
| 503 | `hosting_offline` | The plugin is not hosted, its hosting is not `active`, or its health is `down` — with `hosting` as in `GET` (`null` when not hosted) |
|
|
577
577
|
| 504 | `timeout` | No answer within 15 s — the change may or may not have been stored; read the settings again |
|
|
578
578
|
|
|
579
|
-
Once the plugin and its settings block are found, the checks run in this order: the body (`400`), a running operation (`409`), the hosting (`503`), then the agent half. A refusal answers `{ error, code?, errors?, revision?, operationId?, hosting?, status? }`, built from what Lamaste found and never from the request body. The agent half's per-field messages are passed on only under the keys of the installed schema (or `_`), and only when they are messages Lamaste's own validation produces — the fixed ones (`Is required`, `Does not match the required format`, …) and the templated ones (`Must be at most 64 characters`, `Must be at least 0`, `2 unknown settings`), matched exactly; anything else reads `Invalid value` (`normalizeSettingsErrorMessage` in `@lamalibre/lamaste@4.
|
|
579
|
+
Once the plugin and its settings block are found, the checks run in this order: the body (`400`), a running operation (`409`), the hosting (`503`), then the agent half. A refusal answers `{ error, code?, errors?, revision?, operationId?, hosting?, status? }`, built from what Lamaste found and never from the request body. The agent half's per-field messages are passed on only under the keys of the installed schema (or `_`), and only when they are messages Lamaste's own validation produces — the fixed ones (`Is required`, `Does not match the required format`, …) and the templated ones (`Must be at most 64 characters`, `Must be at least 0`, `2 unknown settings`), matched exactly; anything else reads `Invalid value` (`normalizeSettingsErrorMessage` in `@lamalibre/lamaste@4.7.0`). The agent half is Lamaste code that never repeats a value, but the relay does not answer an administrator with text it cannot vouch for.
|
|
580
580
|
|
|
581
581
|
A stored change is recorded in the [audit log](audit-and-notifications.md) as `plugin.settings.updated` with key names only — `changed`, `cleared`, `secretsSet`, `secretsCleared` — the new `revision` and the certificate serial. A change whose answer was lost (`504 timeout`, `502 unauthenticated` or `invalid`) is remembered by its key names; when a later read finds the agent half at exactly its revision, changed in those keys only, it is recorded then, with `confirmedLater: true`.
|
|
582
582
|
|
|
@@ -602,7 +602,7 @@ An update's preview — `pendingUpdate.settings`, in `GET /api/plugins/:name/hoo
|
|
|
602
602
|
|
|
603
603
|
### Between the relay and the agent half
|
|
604
604
|
|
|
605
|
-
The relay calls the agent half with `callAgentHalf` (`@lamalibre/lamaste@4.
|
|
605
|
+
The relay calls the agent half with `callAgentHalf` (`@lamalibre/lamaste@4.7.0`, also exported from `@lamalibre/lamaste@4.7.0/server`): one JSON request to `http://127.0.0.1:<port>/admin/…` — the relay end of the internal tunnel — with a caller assertion naming Lamaste itself (`{ "type": "lamaste" }`) that binds the body's SHA-256, answered with whatever status the agent half gives once the answer carries a valid responder signature over that status and its exact body ([Authentication](#authentication-between-the-relay-and-the-agent-half)). It calls only paths under `/admin/` without `.` or `..` segments, with `GET`, `PUT`, `POST` or `DELETE`, and throws before sending anything otherwise; it sends a body as `application/json` — `body` may be a function of the assertion's nonce, chosen before anything is signed, which is how a change seals its secrets to the one call — bounds the time and the response size, and resolves `{ ok: true, status, body }` (the parsed JSON, or `null`) or `{ ok: false, code, error }` — `refused` (nothing listens), `timeout`, `invalid` (a response over the size limit), `unauthenticated` (no valid responder signature: not the agent half, or one built against an older core) or `other` — with an error that never contains the request body. The hook call `callAgentHalfHook` is built on the same exchange.
|
|
606
606
|
|
|
607
607
|
| Call | Body | Answer |
|
|
608
608
|
| ----------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -625,7 +625,7 @@ The state, which every answer of the relay's reads and changes is built from:
|
|
|
625
625
|
|
|
626
626
|
`values` are the explicitly stored non-secret values — no defaults; `secrets` names the secrets that are set, and when — never a value; `setAt` must be an ISO 8601 UTC timestamp as `Date.prototype.toISOString` writes it (`2026-10-16T10:00:00.000Z`), so not even that field can carry free text. In a change, `schemaVersion` is the relay's installed manifest's, `baseRevision` the revision the administrator edited, `values` the non-secret change the panel validated and `secrets` each secret **sealed** (`null` clears). Bodies and responses are 2 MiB at most — the largest valid change, 50 fields of 4096 characters that JSON escapes to six bytes apiece, is about 1.23 MB. The relay parses every state with `parseAgentHalfSettingsState`, which builds a fresh object of the contract's fields: nothing else the agent half sent — a secret's value in particular — survives into the cache.
|
|
627
627
|
|
|
628
|
-
**Sealed secrets.** Each secret of a change crosses the tunnel as `{ "v": 1, "iv": "<base64url, 12 bytes>", "ct": "<base64url ciphertext and 16-byte tag>" }` — AES-256-GCM under a key derived from the installation secret (HKDF-SHA256 over its 32 bytes, salt `lamaste/settings-secrets/v1`, info the plugin's route), a random IV per value, and as additional data the JSON array `[plugin, nonce, key, baseRevision]`: the plugin, the call's assertion nonce, the field and the revision the change was made on. `encryptSettingsSecret` and `decryptSettingsSecret` in `@lamalibre/lamaste@4.
|
|
628
|
+
**Sealed secrets.** Each secret of a change crosses the tunnel as `{ "v": 1, "iv": "<base64url, 12 bytes>", "ct": "<base64url ciphertext and 16-byte tag>" }` — AES-256-GCM under a key derived from the installation secret (HKDF-SHA256 over its 32 bytes, salt `lamaste/settings-secrets/v1`, info the plugin's route), a random IV per value, and as additional data the JSON array `[plugin, nonce, key, baseRevision]`: the plugin, the call's assertion nonce, the field and the revision the change was made on. `encryptSettingsSecret` and `decryptSettingsSecret` in `@lamalibre/lamaste@4.7.0` are the two halves; the relay seals with the secret it signs the call with, and the agent half opens with the secret that verified it (`decryptSettingsSecret` on the verify result — the secret never leaves the verifier). A sealed secret opens for no other plugin, call, field or revision; one that does not open is a `422` for its key, and an error never repeats it. Whatever takes the tunnel's port receives no secret in clear — the non-secret values of a change, which the relay stores in clear anyway, are not sealed.
|
|
629
629
|
|
|
630
630
|
Only Lamaste: a caller assertion that does not verify is `401`, one naming an administrator or an agent `403`; another path under `/admin/settings` is `404`, a method a path does not take `405`. The **forwarder reserves `/admin/settings`** and everything under it as it reserves `/admin/hooks/*`: `/api/<route>/settings`, `/api/<route>/Settings` and `/api/<route>/%73ettings/x` all answer `404` and reach nothing. No forwarded caller — an administrator, or an agent holding one of the plugin's capabilities — reads or changes the settings behind the panel's validation, and the relay's cache cannot drift from the agent half. An agent half's own admin routes cannot live there.
|
|
631
631
|
|
|
@@ -633,7 +633,7 @@ Only Lamaste: a caller assertion that does not verify is `401`, one naming an ad
|
|
|
633
633
|
|
|
634
634
|
### In the agent half
|
|
635
635
|
|
|
636
|
-
`@lamalibre/lamaste@4.
|
|
636
|
+
`@lamalibre/lamaste@4.7.0/agent` holds the store and the handler, so that no plugin reimplements how a secret is stored:
|
|
637
637
|
|
|
638
638
|
- **`settingsFromManifest(manifest)`** — the `settings` block of the agent half's own copy of its manifest (bundled with it), validated — here each string default is also tested against its field's `pattern`, which the relay never runs; throws `ManifestValidationError` when it is missing or invalid. The agent half checks values against this schema, not the relay's.
|
|
639
639
|
- **`createSettingsStore({ dataDir, settings })`** — the store over two files in `dataDir`, which must exist: for an agent half Lamaste installs, `LAMASTE_APP_DATA_DIR`, the only directory its unit may write ([App Host](app-host.md)). One store per process. `load()` reads and reconciles the files and must run before anything else; then `state()` is what `GET` answers, `values()` the values the plugin runs with (defaults overlaid by the stored values, never a secret), `secret(key)` a secret's value for the agent half's own use (`undefined` when it is not set), `configured()`, `update(change)`, `reload()`, and `onChange(listener)` — called with the new state after every update and reload, returning its unsubscribe function; a listener that throws does not fail the update, which is stored.
|
|
@@ -656,7 +656,7 @@ import {
|
|
|
656
656
|
createSettingsHandler,
|
|
657
657
|
createSettingsStore,
|
|
658
658
|
settingsFromManifest,
|
|
659
|
-
} from '@lamalibre/lamaste@4.
|
|
659
|
+
} from '@lamalibre/lamaste@4.7.0/agent';
|
|
660
660
|
import manifest from '../lamaste-plugin.json' with { type: 'json' }; // bundled into the agent half
|
|
661
661
|
|
|
662
662
|
const verifier = createCallerAssertionVerifier({
|
|
@@ -807,7 +807,7 @@ POST /admin/identity/changes { protocol: 1, directoryId, from, to, reset, recon
|
|
|
807
807
|
→ 409 { error: "cursor_mismatch", protocol: 1, directoryId, cursor }
|
|
808
808
|
```
|
|
809
809
|
|
|
810
|
-
- **The changes travel sealed.** The internal tunnel's ends are loopback ports — the chisel server's on the relay, the agent's on the agent — and whatever holds one sees what crosses it. A batch's envelope (directory, cursors, flags, pass) is no one's personal data; its `changes` array is encrypted with AES-256-GCM under `HKDF-SHA256(installation secret, salt "lamaste/identity-changes/v1", info route)` — a key of its own, never the settings secrets' — with 12 random IV bytes and, as additional data, the route, the caller assertion's nonce and the whole envelope (`JSON.stringify([plugin, nonce, directoryId, from, to, reset, reconcile, pass, final])`): sealed changes open only in the one request that carries them, under that envelope (`sealIdentityChanges`, `openIdentityChanges` in `@lamalibre/lamaste@4.
|
|
810
|
+
- **The changes travel sealed.** The internal tunnel's ends are loopback ports — the chisel server's on the relay, the agent's on the agent — and whatever holds one sees what crosses it. A batch's envelope (directory, cursors, flags, pass) is no one's personal data; its `changes` array is encrypted with AES-256-GCM under `HKDF-SHA256(installation secret, salt "lamaste/identity-changes/v1", info route)` — a key of its own, never the settings secrets' — with 12 random IV bytes and, as additional data, the route, the caller assertion's nonce and the whole envelope (`JSON.stringify([plugin, nonce, directoryId, from, to, reset, reconcile, pass, final])`): sealed changes open only in the one request that carries them, under that envelope (`sealIdentityChanges`, `openIdentityChanges` in `@lamalibre/lamaste@4.7.0`; the verified request's `openIdentityChanges` in an agent half). Notifications and answers carry no member and stay as they are. The 1 MiB limit is the sealed request's.
|
|
811
811
|
- `seq` in a notification is the plugin's head; `directoryId` the relay directory's id (a UUID v4 made with `state.db`).
|
|
812
812
|
- A batch carries the changes of `(from, to]` in strictly increasing `seq`, each one of the [changes above](#entitlements-and-the-audience) with its `seq` — at most 500 changes and 1 MiB. The half applies it and moves its cursor to `to` in one transaction; one that does not continue from its cursor (another `from`, or another directory without `reset`) is answered `409` with what it holds, and the panel continues from there (or replays) — up to three times per sync.
|
|
813
813
|
- **A replay from 0 is the snapshot.** The panel replays when the half follows another directory or none, its cursor is ahead of the head (a restored relay), it never completed a replay since the plugin was installed or hosted, a replay was cut short — and once a day as a **reconcile**. Every batch of a replay carries the same `pass`; the first has `reset: true` (the half adopts `directoryId` and restarts its cursor), the last `final: true`, when the half marks every row the pass did not mention as removed. Batches with `reconcile: true` — every batch of the daily reconcile, and every batch after the first of any replay — have the half count every change that differs from its row (and, at the final batch, every row the pass did not mention) and answer `mismatches`. For the daily reconcile a non-zero count on a healthy system is a feed bug: it is recorded on the plugin's sync state and audited as `identity.reconcile.mismatches`.
|
|
@@ -825,6 +825,10 @@ POST /admin/identity/changes { protocol: 1, directoryId, from, to, reset, recon
|
|
|
825
825
|
| `not_hosted` | The plugin is not hosted, or its hosting is not active |
|
|
826
826
|
| `not_answered` | Its agent half has not applied the feed up to the head (protocol 1) for the installed version |
|
|
827
827
|
|
|
828
|
+
### The plugin's domain
|
|
829
|
+
|
|
830
|
+
On a relay with several domains a plugin belongs to one ([`PUT /api/plugins/:name/domain`](domains.md#put-apipluginsnamedomain); the primary until moved). Its audience is the entitled users who are members of that domain — a member leaving becomes `remove/unentitled`, so agent halves see no new reason; a tunnel bound as its user surface and its web surface must be on that domain; its OIDC clients' `{domain}` and sign-in policy are that domain's. Moving it is refused while a surface is served (409 `surface_bound`).
|
|
831
|
+
|
|
828
832
|
### The user surface
|
|
829
833
|
|
|
830
834
|
A plugin's users reach it at `/api/<route>/app` and everything under it, on the host of an app tunnel **bound** as the plugin's user surface ([Tunnels API](tunnels.md#user-surface-binding)) — a tunnel its hosting agent carries — or on the site the plugin owns for its [web surface](#web-surface), which serves the same paths the same way (where the steps below say tunnel, read site; a site's grant is a `site` grant). The bound host serves those paths only while the plugin's installed, approved version declares `identity` and the tunnel's agent hosts the plugin; before that, its requests go down the tunnel as every other path does — which is how a plugin moves its users' API to the relay with a plain update. Each request:
|
|
@@ -867,7 +871,7 @@ An agent half verifies `/app` requests with its `createCallerAssertionVerifier`,
|
|
|
867
871
|
|
|
868
872
|
### In the agent half
|
|
869
873
|
|
|
870
|
-
`@lamalibre/lamaste@4.
|
|
874
|
+
`@lamalibre/lamaste@4.7.0/agent` keeps the projection and answers the feed:
|
|
871
875
|
|
|
872
876
|
```js
|
|
873
877
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -875,7 +879,7 @@ import {
|
|
|
875
879
|
createCallerAssertionVerifier,
|
|
876
880
|
createIdentityHandlers,
|
|
877
881
|
createIdentityProjection,
|
|
878
|
-
} from '@lamalibre/lamaste@4.
|
|
882
|
+
} from '@lamalibre/lamaste@4.7.0/agent';
|
|
879
883
|
|
|
880
884
|
const db = new DatabaseSync(`${dataDir}/app.db`);
|
|
881
885
|
const projection = createIdentityProjection({
|
|
@@ -15,12 +15,12 @@ The helper does only what the relay **signed**. When it is enabled it remembers
|
|
|
15
15
|
Once per agent machine (Linux with systemd; macOS agents cannot host an agent half Lamaste installs):
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
sudo npm install -g @lamalibre/lamaste-agent@4.
|
|
18
|
+
sudo npm install -g @lamalibre/lamaste-agent@4.7.0 # a root-owned install
|
|
19
19
|
sudo lamaste-agent apphost enable # the agent runs as the user who ran sudo
|
|
20
20
|
sudo lamaste-agent apphost enable --user root # the agent runs as root: no sudoers rule
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`enable` refuses to act from an install another user could have changed: the package, the helper it ships and the core library it loads — and every directory above them — must be root-owned and writable by no group or other user. A user-level install (`nvm`, a home-directory prefix) is not; install it root-owned with `sudo npm install -g @lamalibre/lamaste-agent@4.
|
|
23
|
+
`enable` refuses to act from an install another user could have changed: the package, the helper it ships and the core library it loads — and every directory above them — must be root-owned and writable by no group or other user. A user-level install (`nvm`, a home-directory prefix) is not; install it root-owned with `sudo npm install -g @lamalibre/lamaste-agent@4.7.0` and run `enable` from that install. It also needs `/usr/bin/node` (the helper's interpreter and the Node.js every agent half runs on — from the distribution or NodeSource), systemd as the system manager (`/run/systemd/system`), and `sudo` and `visudo` unless the agent runs as root.
|
|
24
24
|
|
|
25
25
|
What `enable` does:
|
|
26
26
|
|
|
@@ -357,7 +357,7 @@ The contract:
|
|
|
357
357
|
|
|
358
358
|
```js
|
|
359
359
|
import Fastify from 'fastify';
|
|
360
|
-
import { agentHalfListenTarget, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.
|
|
360
|
+
import { agentHalfListenTarget, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.7.0/agent'; // bundled
|
|
361
361
|
|
|
362
362
|
const verifier = createCallerAssertionVerifier({
|
|
363
363
|
configPath: process.env.LAMASTE_APP_CONFIG,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Domains API
|
|
2
|
+
|
|
3
|
+
> The relay's base domains and who is a member of each — one relay, one user directory, a sign-in and a member list per domain.
|
|
4
|
+
|
|
5
|
+
## In Plain English
|
|
6
|
+
|
|
7
|
+
A relay serves one **primary** domain (the panel's, set at onboarding) and any number of **added** domains. Every domain has its own sign-in page, `auth.<domain>`, and its own member list. A person has one account — one password, one TOTP — and reaches a domain's protected hosts (`authenticated` and `restricted` tunnels, protected sites, plugin user and web surfaces, OIDC sign-ins of the domain's plugins) only as a member of it. Admin stays relay-wide: the admin certificate manages every domain. The panel (`panel.`) and the tunnel endpoint (`tunnel.`) stay on the primary.
|
|
8
|
+
|
|
9
|
+
A membership has a **kind**. A `member` reaches what the domain's members reach. An `external` member — a client's people, say — reaches only what names them: `restricted` tunnels, user surfaces and web surfaces that grant them (Gatekeeper grants, to them or a Lamaste group they belong to), static sites whose `allowedUsers` lists them, and OIDC clients granted to them. An external member holds no Authelia groups (`admins`, `feria-*`).
|
|
10
|
+
|
|
11
|
+
While the relay has one domain, every user is its member and nothing looks different — unless a user is made external. See [DNS & Domains](../01-concepts/dns-and-domains.md#multiple-domains) and the guide [Adding a Second Domain](../02-guides/second-domain.md).
|
|
12
|
+
|
|
13
|
+
## Authentication
|
|
14
|
+
|
|
15
|
+
Every endpoint requires an admin certificate (mTLS) — agents never manage domains.
|
|
16
|
+
|
|
17
|
+
## Domain Object
|
|
18
|
+
|
|
19
|
+
| Field | Type | Description |
|
|
20
|
+
| ----------------- | -------------- | --------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `name` | string | Lower case, two labels or more (`codelama.com.tr`) |
|
|
22
|
+
| `primary` | boolean | The panel's domain; exactly one; never removed |
|
|
23
|
+
| `status` | string | `pending` (added, sign-in not served) or `active` (`auth.<name>` issued, members admitted) |
|
|
24
|
+
| `createdAt` | string | ISO 8601 |
|
|
25
|
+
| `activatedAt` | string \| null | ISO 8601, once active |
|
|
26
|
+
| `members` | number | Members of kind `member`, disabled users included (`GET /api/domains` only) |
|
|
27
|
+
| `externalMembers` | number | External members, disabled users included (`GET /api/domains` only) |
|
|
28
|
+
| `usedBy` | object | `{ tunnels, sites, plugins, registry }` — hostnames, plugin names, whether the registry trusts its issuer |
|
|
29
|
+
|
|
30
|
+
Domains never overlap: `a.samplepark.tr` cannot be added beside `samplepark.tr` (a browser sends a domain's cookies to every host under it).
|
|
31
|
+
|
|
32
|
+
## Endpoints
|
|
33
|
+
|
|
34
|
+
### `GET /api/domains`
|
|
35
|
+
|
|
36
|
+
Every domain, the primary first, with its member count and what uses it, and `newUsersNeedDomains` — whether users and invitations must name their domains (the relay has two or more; clients ask for domains only then).
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"domains": [
|
|
41
|
+
{
|
|
42
|
+
"name": "samplepark.tr",
|
|
43
|
+
"primary": true,
|
|
44
|
+
"status": "active",
|
|
45
|
+
"createdAt": "2026-10-25T10:00:00.000Z",
|
|
46
|
+
"activatedAt": "2026-10-25T10:00:00.000Z",
|
|
47
|
+
"members": 5,
|
|
48
|
+
"externalMembers": 2,
|
|
49
|
+
"usedBy": {
|
|
50
|
+
"tunnels": ["gitlab.samplepark.tr"],
|
|
51
|
+
"sites": [],
|
|
52
|
+
"plugins": [],
|
|
53
|
+
"registry": false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"newUsersNeedDomains": false
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `POST /api/domains`
|
|
62
|
+
|
|
63
|
+
Add a domain, `pending`. Body `{ "name": "codelama.com.tr" }`. **201** `{ domain }`.
|
|
64
|
+
|
|
65
|
+
| Status | `code` | When |
|
|
66
|
+
| ------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| 400 | `invalid_domain` | Not a domain name |
|
|
68
|
+
| 400 | `public_suffix` | A public suffix (`com.tr`, `github.io`), not a domain |
|
|
69
|
+
| 409 | `domain_overlap` | The same as, or under or above, a domain |
|
|
70
|
+
| 409 | `same_registrable_domain` | Shares its registrable domain (eTLD+1, Public Suffix List) with a domain — an application on one could set cookies for the other |
|
|
71
|
+
| 409 | `relay_host_taken` | A tunnel, site or alias already holds one of its relay hostnames (`auth.`, `panel.`, `tunnel.`, `agent-*`); also refused at activation |
|
|
72
|
+
|
|
73
|
+
### `POST /api/domains/:name/verify-dns`
|
|
74
|
+
|
|
75
|
+
The DNS records the domain needs at its DNS host, each with what it resolves to now — `auth.<name>` (required) and the wildcard `*.<name>` (optional: one A record per hostname does as well; checked through a probe name). `ok` once every required record points at the relay. Clients show these records as they are; they derive none. Rate limited (30 a minute).
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"ok": true,
|
|
80
|
+
"expectedIp": "138.68.65.228",
|
|
81
|
+
"records": [
|
|
82
|
+
{
|
|
83
|
+
"type": "A",
|
|
84
|
+
"name": "auth.codelama.com.tr",
|
|
85
|
+
"value": "138.68.65.228",
|
|
86
|
+
"required": true,
|
|
87
|
+
"purpose": "the domain’s sign-in page",
|
|
88
|
+
"resolvedIps": ["138.68.65.228"],
|
|
89
|
+
"ok": true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "A",
|
|
93
|
+
"name": "*.codelama.com.tr",
|
|
94
|
+
"value": "138.68.65.228",
|
|
95
|
+
"required": false,
|
|
96
|
+
"purpose": "every tunnel and site hostname — or one A record per hostname instead",
|
|
97
|
+
"resolvedIps": [],
|
|
98
|
+
"ok": false
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `POST /api/domains/:name/activate`
|
|
105
|
+
|
|
106
|
+
Issue the certificate of `auth.<name>` (HTTP-01), write its vhost, add its Authelia session cookie and its rules to `access_control`, give its members and external members their groups in `users.yml` and write Gatekeeper's projection, restart Authelia — one operation, rolled back as a whole on failure. Rate limited (10 a minute). **200** `{ domain }`.
|
|
107
|
+
|
|
108
|
+
| Status | `code` | When |
|
|
109
|
+
| ------ | ------------------------------------------------ | -------------------------------------- |
|
|
110
|
+
| 404 | `domain_not_found` | |
|
|
111
|
+
| 502 | `cert_failed`, `nginx_failed`, `authelia_failed` | The step named failed; nothing changed |
|
|
112
|
+
|
|
113
|
+
### `DELETE /api/domains/:name`
|
|
114
|
+
|
|
115
|
+
Remove a domain: its memberships end (audited, reason `domain-removed`), its sign-in and session cookie go. **200** `{ removed: true }`.
|
|
116
|
+
|
|
117
|
+
| Status | `code` | When |
|
|
118
|
+
| ------ | ---------------- | ------------------------------------------------------------------------------- |
|
|
119
|
+
| 400 | `primary_domain` | The primary is never removed |
|
|
120
|
+
| 409 | `domain_in_use` | A tunnel, site, plugin or the registry credential uses it — `usedBy` lists them |
|
|
121
|
+
|
|
122
|
+
### `GET /api/domains/:name/members`
|
|
123
|
+
|
|
124
|
+
`{ members: [{ userId, username, status, kind, addedAt, addedBy }] }` — `status` is the directory's (`active`, `disabled`, `removed`); `kind` is `member` or `external`.
|
|
125
|
+
|
|
126
|
+
### `PUT /api/domains/:name/members/:user`
|
|
127
|
+
|
|
128
|
+
Make a user a member — `:user` is a directory `userId` or the username of a user the directory holds. Body `{ "kind": "member" | "external" }` (optional, `member`): a user who is a member already gets the kind named (audited `directory.membership.changed`). **200** `{ added, changed }` (both `false` when nothing changed), with `warnings` when Gatekeeper or the audiences could not be brought along. A pending domain takes members too: they are let in when it is activated.
|
|
129
|
+
|
|
130
|
+
A change applies at the user's next request — Authelia is not restarted (see [Where membership is enforced](#where-membership-is-enforced)).
|
|
131
|
+
|
|
132
|
+
| Status | `code` | When |
|
|
133
|
+
| ------ | ------------------------ | -------------------------------------------------------------------------- |
|
|
134
|
+
| 404 | `user_not_found` | |
|
|
135
|
+
| 409 | `user_removed` | The user is deleted |
|
|
136
|
+
| 409 | `external_member_groups` | Making a user external who holds Authelia groups — remove the groups first |
|
|
137
|
+
|
|
138
|
+
### `DELETE /api/domains/:name/members/:user`
|
|
139
|
+
|
|
140
|
+
End a membership. **200** `{ removed }`. When the user no longer reaches the registry's sign-in (a member of none of the registry's domains, nor an external member of one granted its client), their registry tokens are revoked too.
|
|
141
|
+
|
|
142
|
+
### `PUT /api/plugins/:name/domain`
|
|
143
|
+
|
|
144
|
+
Move a plugin to another active domain: `{ "domain": "codelama.com.tr" }`. A plugin belongs to one domain — its OIDC clients' `{domain}` and sign-in policy, which tunnels may be bound as its user surface, its web surface, its audience (entitled users who are members). **200** `{ name, domain, changed }`. **409** `surface_bound` while a tunnel is bound as its user surface or it owns a web surface; **400** `unknown_domain` for a domain that is not active.
|
|
145
|
+
|
|
146
|
+
## Memberships
|
|
147
|
+
|
|
148
|
+
- **Explicit, by `userId`.** A re-created username is a new user with no membership.
|
|
149
|
+
- **Kinds.** `member` or `external`, per membership: a user may be a member of one domain and an external member of another. Where a request names memberships (`domains` on users and invitations) each is a domain name — a `member` — or `{ "name": "codelama.com.tr", "kind": "external" }`; one domain named with two kinds is refused (400 `conflicting_membership`). Answers always carry `{ name, kind }`.
|
|
150
|
+
- **New users.** While the relay has one domain, every new user — created, invited, adopted from `users.yml` — becomes its member unless the request names it as `external`. With two or more, `POST /api/users` and `POST /api/invitations` take `domains` (required, and not empty: 400 `domains_required` — a user who is a member of nothing reaches nothing); a user adopted from `users.yml` gets none until an admin adds one. `PUT /api/users/:username` with `domains` sets a user's memberships exactly: the difference applied in one change, each membership audited.
|
|
151
|
+
- **External members hold no Authelia groups.** Giving one a group, making a user with groups external, or inviting with both is refused (`external_member_groups`). Lamaste groups (Gatekeeper's) are not Authelia groups: grants to them reach external members too.
|
|
152
|
+
- **Disable** keeps memberships; a disabled user is let in nowhere, and enabling restores them. **Delete** ends them.
|
|
153
|
+
- **Audit:** `domain.added`, `domain.activated`, `domain.removed`, `directory.membership.added` (with `kind`), `directory.membership.changed` (`from`, `to`), `directory.membership.removed` — by `userId` and domain name, never username.
|
|
154
|
+
|
|
155
|
+
## Where membership is enforced
|
|
156
|
+
|
|
157
|
+
1. **Authelia.** Who is let in lives in `users.yml` as groups Lamaste owns, which Authelia watches (`authentication_backend.file.watch: true`, `refresh_interval: always`, its own password change and reset disabled) and re-reads at every request — so a membership, a kind, a grant or a site's `allowedUsers` changes without restarting Authelia or ending anyone's session:
|
|
158
|
+
- `lamaste:m:<key>`, `lamaste:x:<key>` — the domain's members and external members, `<key>` a random key of the domain record (a group names no domain);
|
|
159
|
+
- `lamaste:s:<site id>` — a protected static site's listed users;
|
|
160
|
+
- `<hostname>` — the external members granted a `restricted` tunnel, user surface or web surface (enabled; a tunnel without an access mode is restricted);
|
|
161
|
+
- `lamaste:oidc@<client id>` — the external members granted a user-facing OIDC client.
|
|
162
|
+
|
|
163
|
+
`configuration.yml` names only those groups and changes only with the domains and protected sites (Authelia restarts then): one `session.cookies` entry per active domain; `access_control` per protected site (its site group; admins who are members), then per domain — its sign-in portal `auth.<domain>` for members and external members, members on every host, external members only on a host whose name is one of their groups (`domain_regex` with a `(?P<Group>…)` capture), deny — and `default_policy: deny`; one OIDC authorization policy per user-facing client (`lamaste_client_<id>`: members of its domain, external members with its group). The panel never shows these groups as groups, refuses assigning a name with `.`, `:` or `@` (400 `reserved_group`), and Gatekeeper strips them from `Remote-Groups`. Authelia's device-code flow checks a client's policy against the groups a session last refreshed, so a change that takes a device-code client away from someone restarts Authelia (every session ends) — the one change to a person that does.
|
|
164
|
+
|
|
165
|
+
2. **Gatekeeper** (`/etc/lamalibre/lamaste/domains.json`, version 2, 0600, written by the panel): checked again after Authelia on every tunnel, user-surface and shell request; a missing, unreadable or older projection refuses. A signed-in non-member gets a refusal page (JSON `{ "error": "not_a_member" }` on a user surface) — never the sign-in page again. An external member on an `authenticated` tunnel or web-surface shell gets the access-request page; on an `authenticated` user surface a 403 (`forbidden`); on a `restricted` host their grant decides, as for members. When Authelia itself refused an external member (no group names the host), Gatekeeper asks Authelia about the same session for `https://auth.<domain>/` to learn who it was, and answers them the same way; the request stays refused.
|
|
166
|
+
|
|
167
|
+
3. **Desktop user access** (the primary domain's): `authorize`, `exchange` and every session request require a `member` — an external member gets no desktop session.
|
|
168
|
+
|
|
169
|
+
## CLI
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
lamaste-server domains list
|
|
173
|
+
lamaste-server domains add codelama.com.tr
|
|
174
|
+
lamaste-server domains verify-dns codelama.com.tr
|
|
175
|
+
lamaste-server domains activate codelama.com.tr
|
|
176
|
+
lamaste-server domains members codelama.com.tr add onur
|
|
177
|
+
lamaste-server domains members codelama.com.tr add client-ali --external
|
|
178
|
+
lamaste-server domains members codelama.com.tr kind client-ali member
|
|
179
|
+
lamaste-server domains members codelama.com.tr remove onur
|
|
180
|
+
lamaste-server plugins oidc-clients corral grant corral-web --user client-ali
|
|
181
|
+
lamaste-server domains remove codelama.com.tr
|
|
182
|
+
lamaste-server plugins domain feria codelama.com.tr
|
|
183
|
+
lamaste-server tunnels create --subdomain feria --port 20060 --agent feria --domain codelama.com.tr
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Related
|
|
187
|
+
|
|
188
|
+
- [Users API](users.md) — `domains` on users and invitations
|
|
189
|
+
- [Tunnels API](tunnels.md), [Sites API](sites.md) — `domain` on creation
|
|
190
|
+
- [Registry and OIDC](registry-and-oidc.md) — one issuer per domain, `issuerDomain`
|
|
191
|
+
- [Gatekeeper](gatekeeper.md) — the membership check and refusal page
|
|
192
|
+
- Design: `packages/core/docs/multi-domain-relay-design.md`, `packages/core/docs/external-members-design.md`
|
|
@@ -272,7 +272,13 @@ Create a new authorization grant linking a principal (user or group) to a resour
|
|
|
272
272
|
|
|
273
273
|
**Plugin role grants.** For `resourceType: "plugin"` with `context.roles`, the panel checks the grant before Gatekeeper sees it: `resourceId` must be the route of an installed plugin that declares `identity` (`400`, `code: "no_identity"`) and `roles` a non-empty list of its role ids (`400`, `code: "invalid_roles"`, or `"unknown_roles"` with the `unknown` ones); they go on sorted and without repeats. The plugin's **People** panel and `lamaste-server plugins entitlements` manage these grants one principal at a time (`PUT /api/plugins/:name/entitlements` replaces a principal's role grants) — prefer them to creating one here. A plugin grant without `roles` (the desktop's local-install grants) never makes anyone a plugin user.
|
|
274
274
|
|
|
275
|
-
**
|
|
275
|
+
**OIDC client grants.** For `resourceType: "oidc-client"` the panel checks that `resourceId` is a client an installed plugin declares (`400`, `code: "unknown_oidc_client"`) that signs users in (`400`, `code: "service_oidc_client"`). Such a grant admits an **external member** of the plugin's domain to the client's sign-in ([Registry and OIDC](registry-and-oidc.md#several-domains-one-issuer-each)); members need none. Gatekeeper is not in the OIDC path — it only keeps the grant; the panel renders it into Authelia.
|
|
276
|
+
|
|
277
|
+
**Every grant or group change through the proxy re-renders** who may reach what: the groups Lamaste owns in `users.yml` (external members' host groups follow their grants on `restricted` hosts) — applied by Authelia without a restart, except a revocation that takes a device-code OIDC client away from someone (Authelia restarts: its device flow reads a session's groups as they were). A change Gatekeeper took whose rendering failed is answered with `warnings` and rendered again within seconds; a change made with Gatekeeper's own CLI is rendered by the identity tick within a minute.
|
|
278
|
+
|
|
279
|
+
**Grants are never consumed** but a local plugin grant (by its desktop enrolment); the 90-day sweep deletes only consumed local plugin grants (Gatekeeper migration `0004` un-consumed the tunnel, site, agent-side plugin and OIDC-client grants older versions created consumed).
|
|
280
|
+
|
|
281
|
+
**Errors:** 400 (invalid body; a role grant or an OIDC client grant as above), 409 (duplicate grant), 503 (Gatekeeper unavailable, or a removed user's principal under `principalId` cannot be cleared — `code: "gatekeeper_unavailable"`)
|
|
276
282
|
|
|
277
283
|
---
|
|
278
284
|
|
|
@@ -628,12 +634,12 @@ GET /authz/user-surface/:fqdn
|
|
|
628
634
|
|
|
629
635
|
The `auth_request` target of a plugin's user surface — `/api/<route>/app` and everything under it on the host of a tunnel bound to the plugin ([Tunnels API](tunnels.md#user-surface-binding)) or of the site the plugin owns ([Web surface](agent-hosted-plugins.md#web-surface)). Gatekeeper reads both kinds of host from `tunnels.json` and `sites.json`, re-read when either changes, the way the panel reads them (core `userSurfaceHosts`): a route two hosts would serve is served by neither, and a host whose name another host shares is no host. Where this section says tunnel, read the host; a site's grant is a `site` grant. The vhost names its tunnel in the path, `X-Original-URL` and `Host`, as for `/authz/tunnel/:fqdn`, and sends `X-Original-Method`. Gatekeeper checks, in order: the hostnames agree; the tunnel is known, enabled, not public and has a user surface; the raw request target — what follows `https://<fqdn>` in `X-Original-URL`, never re-encoded by a URL parser — lies on the user surface of a route bound to the tunnel, with no `.` or `..` segment, backslash, percent-encoded `.`, `/`, `\` or `%` (`%25`), or escape that does not decode; the tunnel's agent hosts the plugin — it carries the plugin's internal tunnel, the one `tunnels.json` records for the route (core `tunnelServedUserSurfaceRoutes`; refused `not_hosting_agent` otherwise: the tunnel moved or its agent revoked, the plugin hosted elsewhere or not at all); the Authelia session, read as for `/authz/tunnel/:fqdn` (one `authelia_session` only); on a `restricted` tunnel, the user's grant for the tunnel. Then it signs a **relay assertion for the panel**: Ed25519 with the relay signing key, audience `@panel`, port 3100, caller `{ type: "user", username }` — the username alone; the panel takes everything else about the user from its directory — bound to the method and the raw target (`{ route, path }`).
|
|
630
636
|
|
|
631
|
-
| Status | Meaning
|
|
632
|
-
| ------ |
|
|
633
|
-
| 200 | `X-Lamalibre-Relay-Assertion` for this one request; `Cache-Control: no-store`; no `Remote-*` headers
|
|
634
|
-
| 401 | No valid session (or more than one session cookie) — nginx answers with `GET /authz/user-surface-unauthenticated`: a JSON `401 { "error": "unauthenticated" }`, never a redirect (the caller is an application's request)
|
|
635
|
-
| 403 | Anything else refused
|
|
636
|
-
| 503 | The relay signing key is unusable
|
|
637
|
+
| Status | Meaning |
|
|
638
|
+
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
639
|
+
| 200 | `X-Lamalibre-Relay-Assertion` for this one request; `Cache-Control: no-store`; no `Remote-*` headers |
|
|
640
|
+
| 401 | No valid session (or more than one session cookie) — nginx answers with `GET /authz/user-surface-unauthenticated`: a JSON `401 { "error": "unauthenticated" }`, never a redirect (the caller is an application's request) |
|
|
641
|
+
| 403 | Anything else refused — never the access-request page; the reason is logged (`user-surface authz denied`) and named in `X-Lamalibre-Refusal`: `not_a_member` for a user who is not a member of the host's domain, `forbidden` otherwise. nginx keeps only the status and headers (`auth_request`) and answers the application itself: `403 { "error": "not_a_member" }` for that value, `403 { "error": "forbidden" }` for any other |
|
|
642
|
+
| 503 | The relay signing key is unusable |
|
|
637
643
|
|
|
638
644
|
### Web-Surface Shell Authorization (nginx auth_request)
|
|
639
645
|
|
|
@@ -653,6 +659,28 @@ The `auth_request` target of every path of a plugin-owned site but its user surf
|
|
|
653
659
|
|
|
654
660
|
Every answer is `Cache-Control: no-store`. A refusal for want of an entitlement or a grant is logged with the resource and the reason only, and recorded in the access-request log when it is on.
|
|
655
661
|
|
|
662
|
+
### Domain membership
|
|
663
|
+
|
|
664
|
+
Every authorization route (tunnel, user surface, shell) checks the user's membership of the host's domain after Authelia, from `/etc/lamalibre/lamaste/domains.json` (written by the panel, watched like `tunnels.json`): `{ "v": 2, "domains": [{ "name", "members": [username…], "externals": [username…] }] }`, active domains and active people only. A missing or unreadable projection, one of another version, or a host under no domain, refuses (fail closed). Authelia's own 403 — it renders the same membership — is no longer taken for "not signed in": a signed-in non-member gets **403** with the domain refusal page (the domain, the admin contact, a sign-out link to `https://auth.<domain>/logout`; no username), or JSON `{ "error": "not_a_member" }` on a user surface, instead of a 401 that sent the browser back to the sign-in page in a loop. Refusals are never cached; a membership change busts the session cache.
|
|
665
|
+
|
|
666
|
+
**External members** reach only what names them. After the membership check:
|
|
667
|
+
|
|
668
|
+
| Route | `authenticated` host | `restricted` host |
|
|
669
|
+
| ----------------- | -------------------------------------------------------- | ---------------------------------------- |
|
|
670
|
+
| Tunnel | access-request page (403), logged `external_not_granted` | their grant (to them or a Lamaste group) |
|
|
671
|
+
| User surface | 403, `X-Lamalibre-Refusal: forbidden` | the host's grant |
|
|
672
|
+
| Web-surface shell | access-request page (403) | the plugin's audience and the site grant |
|
|
673
|
+
|
|
674
|
+
Members are unchanged. **Groups Lamaste owns** (`lamaste:…`, and hostnames under the relay's domains — [Domains API](domains.md#where-membership-is-enforced)) are stripped from `Remote-Groups` and from a plugin tunnel's relay assertion: a backend sees only the groups an administrator assigned.
|
|
675
|
+
|
|
676
|
+
### Refusal Page (nginx `error_page 403`)
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
GET /authz/refusal/:fqdn
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
What an Authelia-protected static site answers a signed-in user Authelia refused with (the site asks Authelia directly, not Gatekeeper). Always 403 with the refusal page; reads no session and decides nothing.
|
|
683
|
+
|
|
656
684
|
### Principals (Gatekeeper's own API)
|
|
657
685
|
|
|
658
686
|
```
|
|
@@ -252,13 +252,13 @@ Relay hooks run in `lamalibre-lamaste-hooks@<route>.service`, started by its soc
|
|
|
252
252
|
|
|
253
253
|
`lamaste-priv hooks install <route> <sha256|none>` (module on stdin, checked against the approved digest) writes the module root-owned to `/opt/lamalibre/lamaste/hooks/<route>/<sha256>.mjs` and enables the socket; `hooks activate` points back at the previous module (an update's revert); `hooks restart` stops a stuck runner; `hooks remove` disables it and deletes its modules. The panel reinstalls an approved runner at start-up if it is missing.
|
|
254
254
|
|
|
255
|
-
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.
|
|
255
|
+
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.7.0/hooks`, `protocol.ts`).
|
|
256
256
|
|
|
257
257
|
## Writing hooks
|
|
258
258
|
|
|
259
259
|
```js
|
|
260
260
|
// hooks/relay.mjs — bundled into one file (esbuild), Node built-ins only
|
|
261
|
-
import { defineRelayHooks } from '@lamalibre/lamaste@4.
|
|
261
|
+
import { defineRelayHooks } from '@lamalibre/lamaste@4.7.0/hooks';
|
|
262
262
|
|
|
263
263
|
export default defineRelayHooks({
|
|
264
264
|
'install:pre-tunnel': async (input) =>
|
|
@@ -287,10 +287,10 @@ Every decision may carry `effects` (at most 10): `{ type: 'audit', event, data?
|
|
|
287
287
|
|
|
288
288
|
### Agent hooks
|
|
289
289
|
|
|
290
|
-
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.
|
|
290
|
+
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.7.0/agent` does the checking — the caller assertion must name Lamaste itself (`{ type: 'lamaste' }`), which no forwarded caller can, and bind the SHA-256 of the exact body (`wrong_body` otherwise), so it needs the body **as received** (a `Buffer` or a string; a parsed object is refused `400` "The body must be passed as received"); the forwarder never reaches `/admin/hooks*`. Send the `status`, `headers` and `bodyText` it returns, `bodyText` byte for byte: its `X-Lamalibre-Responder` header signs those bytes, and the relay counts a hook answer without a valid one as a failure (`unauthenticated`) — see [Authentication between the relay and the agent half](agent-hosted-plugins.md#authentication-between-the-relay-and-the-agent-half):
|
|
291
291
|
|
|
292
292
|
```js
|
|
293
|
-
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.
|
|
293
|
+
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.7.0/agent';
|
|
294
294
|
|
|
295
295
|
const hooks = createAgentHookHandler({
|
|
296
296
|
verifier: createCallerAssertionVerifier({ configPath, plugin: 'fx' }),
|
|
@@ -255,7 +255,7 @@ The wildcard check probes `test-lamaste-check.<domain>`. Wildcard DNS is optiona
|
|
|
255
255
|
|
|
256
256
|
### `POST /api/onboarding/provision`
|
|
257
257
|
|
|
258
|
-
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.
|
|
258
|
+
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.7.0` on the server to repair the installation." (likewise for Authelia). Task IDs are unchanged from earlier versions; only the titles changed.
|
|
259
259
|
|
|
260
260
|
Provisioning runs asynchronously. This endpoint returns immediately with a 202 status. Use the WebSocket stream endpoint to follow progress in real time.
|
|
261
261
|
|