@codions/portta 0.0.0-develop.149.sha-240655c → 0.0.0-develop.150.sha-ed65081

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.
@@ -182,9 +182,11 @@ PORTTA_WEB_READ_ONLY=false
182
182
 
183
183
  # The panel's own front door, which is now the panel's own business.
184
184
  #
185
- # disabled every request is the local operator. Only legal on loopback: the
186
- # panel refuses to start any other way, because reaching it there
187
- # already means having the machine.
185
+ # disabled every request is the local operator. Legal wherever the set of
186
+ # people who can reach the panel is already an authenticated set:
187
+ # `local` (this machine), `tailscale` (a device somebody enrolled)
188
+ # and `vpn` (a client of the VPN). Refused at boot with `public`
189
+ # and `domain`, which answer whoever finds the address.
188
190
  # required the panel signs people in. Accounts, roles, sessions, tokens and
189
191
  # an optional second factor, all in its database. The first person
190
192
  # is created at /setup, once, and creates everyone else.
@@ -192,6 +194,14 @@ PORTTA_WEB_READ_ONLY=false
192
194
  # See docs/development/adr/0035-authentication-lives-in-the-panel.md and docs/product/guides/authentication.md.
193
195
  PORTTA_AUTH_MODE=disabled
194
196
 
197
+ # The one case the access mode does not settle: `local` bound to something
198
+ # other than loopback is the LAN, and "everyone on this Wi-Fi" is not an
199
+ # authenticated set. Turning the login off there needs this said out loud.
200
+ #
201
+ # `portta doctor` warns while it is true, and refuses the combination while it
202
+ # is not.
203
+ PORTTA_AUTH_ALLOW_LAN=false
204
+
195
205
  # Signs sessions, for the panel and for the ForwardAuth service in front of
196
206
  # project hostnames. Generated by `portta bootstrap` and `portta web up` without
197
207
  # ever being printed. Changing it signs everybody out.
@@ -224,18 +234,15 @@ PORTTA_RUNTIME_DOCS=true
224
234
  # always available.
225
235
  PORTTA_RUNTIME_API_DOCS=
226
236
 
227
- # Panel persistence. `bootstrap` or `web up` generates the password; it is
228
- # never printed or returned by the API. In managed mode the application derives
229
- # its URL from these fields. A URL is accepted only in explicit external mode.
230
- PORTTA_DB_NETWORK=portta-data
231
- PORTTA_DB_VOLUME=portta-db
232
- # managed: db:5432 is fixed internal DNS/port, with no host publication.
233
- # external: only PORTTA_RUNTIME_DATABASE_URL determines the connection.
234
- PORTTA_RUNTIME_DB_MODE=managed
235
- PORTTA_RUNTIME_DB_NAME=portta
236
- PORTTA_RUNTIME_DB_USER=portta
237
- PORTTA_RUNTIME_DB_PASSWORD=
238
- PORTTA_RUNTIME_DATABASE_URL=
237
+ # Panel persistence: one SQLite file under this installation, at
238
+ # `state/panel/portta.db`. There is nothing to configure, no password to
239
+ # generate and no service to start — the file is created on first open
240
+ # (docs/development/adr/0037-sqlite-is-the-panel-database.md).
241
+ #
242
+ # This is the path INSIDE the panel container, and `state/panel` is bind-mounted
243
+ # onto it. Changing it is for a different mount, not for a different database;
244
+ # `portta db` operates the one this points at.
245
+ PORTTA_RUNTIME_DATABASE_FILE=/app/state/panel/portta.db
239
246
 
240
247
  # The panel image. A normal installation pulls this; it never builds, because
241
248
  # an installed PORTTA_HOME holds no application source. Pinned like every other
@@ -410,43 +417,21 @@ CF_DNS_API_TOKEN=
410
417
  CLOUDFLARE_ZONE=
411
418
 
412
419
  # ----------------------------------------------------------------------------
413
- # GitHub App: optional, off by default
420
+ # Issues: GitHub through `gh`, and Linear
414
421
  # ----------------------------------------------------------------------------
415
- # The panel's only outbound network access, and its only long-lived secret that
416
- # is not a Traefik credential. With GITHUB_APP_ENABLED=false the panel makes no
417
- # request to github.com and behaves exactly as it did without this section.
422
+ # Nothing is configured here, and that is the point.
418
423
  #
419
- # The private key is a FILE, never a value here: the panel can write this .env,
420
- # and must not be able to write the key that authenticates it. Create the App,
421
- # download the .pem into `state/github/`, and `chmod 600` it. See
422
- # docs/product/guides/github.md for the exact permission set.
423
-
424
- GITHUB_APP_ENABLED=false
425
-
426
- # The numeric App id from the App's settings page. Not a secret.
427
- GITHUB_APP_ID=
428
-
429
- # The .pem inside the panel container. `state/github/` is the only directory
430
- # mounted there, so the path must begin /app/state/github/. The filename is
431
- # yours: keep the one GitHub gave the download if you like.
432
- GITHUB_APP_PRIVATE_KEY_FILE=/app/state/github/app.pem
433
-
434
- # Verifies webhook deliveries. Generate one when you create the App.
435
- GITHUB_APP_WEBHOOK_SECRET=
436
-
437
- # GitHub Enterprise Server users: your API root, e.g. https://ghe.example.com/api/v3
438
- GITHUB_API_URL=https://api.github.com
439
-
440
- # How often the panel re-reads what changed on GitHub, in minutes.
424
+ # The panel reads and writes issues through the host daemon, which runs the
425
+ # GitHub CLI as whoever is signed in on the host — so the credential is `gh
426
+ # auth`'s, kept where `gh` keeps it, and never in this file or in the panel's
427
+ # container (docs/development/adr/0018-github-issues-through-the-gh-cli.md).
441
428
  #
442
- # A loopback panel cannot receive webhook deliveries, so without this the
443
- # projection is only as fresh as the last time somebody pressed Sync. A pass
444
- # asks for issues updated since the stored cursor, so a quiet repository costs
445
- # one conditional request.
429
+ # gh auth login once, on the host
430
+ # portta host serve the daemon the panel reaches it through
446
431
  #
447
- # Set 0 on a panel that DOES receive webhooks, where the timer would do the
448
- # same work twice.
449
- GITHUB_SYNC_INTERVAL_MINUTES=15
432
+ # Linear is optional and works the same way: export LINEAR_API_KEY in the
433
+ # environment that runs `portta host serve`. It is deliberately not a variable
434
+ # here, because this file is one the panel itself can write.
450
435
 
451
436
  # ----------------------------------------------------------------------------
452
437
  # Cloudflare Tunnel: optional connector (credential stays in state/cloudflared)
@@ -1 +1 @@
1
- 0.0.0-develop.149.sha-240655c
1
+ 0.0.0-develop.150.sha-ed65081
@@ -0,0 +1,44 @@
1
+ # ============================================================================
2
+ # Portta: the panel's way to the host daemon
3
+ # ============================================================================
4
+ # Applied whenever the panel is on.
5
+ #
6
+ # The panel is a container with no `gh`, no git, no tmux, no agent CLI and no
7
+ # project directory, and ADR 0001 keeps it that way. Work that needs the host
8
+ # runs in `portta host serve` instead, and the panel reaches it over HTTP and
9
+ # WebSocket through its authorised proxy
10
+ # (docs/development/adr/0047-host-daemon-and-panel-proxy.md).
11
+ #
12
+ # This used to ride with Task Flow, because Task Flow was the only thing that
13
+ # needed the host. Issues need it too, and Issues are the panel's work surface
14
+ # now (docs/development/adr/0018-github-issues-through-the-gh-cli.md) — so the
15
+ # connection is part of having a panel, and Task Flow is left as the switch for
16
+ # its own routes.
17
+ #
18
+ # What this adds to the panel, and nothing else
19
+ # ----------------------------------------------
20
+ # host.docker.internal a name for the host. Docker Desktop resolves it on
21
+ # its own; on Linux `host-gateway` maps it to the
22
+ # bridge gateway, which is where the daemon must bind
23
+ # (PORTTA_HOST_BIND=172.17.0.1, or the address of the
24
+ # bridge in use). The daemon's loopback default is not
25
+ # reachable from a container on Linux.
26
+ # state/host/token the daemon's token, read-only. `portta up` and
27
+ # `portta web up` create it first: a bind mount of a
28
+ # file that does not exist would become a directory.
29
+ #
30
+ # The panel never receives a project directory, a socket or the daemon's own
31
+ # databases: only a URL and a secret. A daemon that is not running is not a
32
+ # failure to start — it is a panel that reports which of its surfaces are
33
+ # unavailable and why.
34
+ # ============================================================================
35
+
36
+ services:
37
+ web:
38
+ extra_hosts:
39
+ - "host.docker.internal:host-gateway"
40
+ environment:
41
+ PORTTA_HOST_URL: ${PORTTA_HOST_URL:-http://host.docker.internal:${PORTTA_HOST_PORT:-5111}}
42
+ PORTTA_RUNTIME_HOST_TOKEN_FILE: /app/state/host/token
43
+ volumes:
44
+ - ./state/host/token:/app/state/host/token:ro
@@ -1,38 +1,15 @@
1
1
  # ============================================================================
2
- # Portta: the panel's way to the host daemon (Task Flow)
2
+ # Portta: Task Flow
3
3
  # ============================================================================
4
4
  # Applied when PORTTA_TASKFLOW=true and the panel is on.
5
5
  #
6
- # Task Flow's work needs the host: git worktrees, tmux, agent CLIs and the
7
- # project directories. The panel is a container with none of those, and ADR
8
- # 0001 keeps it that way. So the work runs in `portta host serve` on the host,
9
- # and the panel reaches it over HTTP and WebSocket through its authorised
10
- # module proxy. See docs/development/adr/0047-host-daemon-and-panel-proxy.md.
11
- #
12
- # What this adds to the panel, and nothing else
13
- # ----------------------------------------------
14
- # host.docker.internal a name for the host. Docker Desktop resolves it on
15
- # its own; on Linux `host-gateway` maps it to the
16
- # bridge gateway, which is where the daemon must bind
17
- # (PORTTA_HOST_BIND=172.17.0.1, or the address of the
18
- # bridge in use). The daemon's loopback default is not
19
- # reachable from a container on Linux.
20
- # state/host/token the daemon's token, read-only. `portta up` and
21
- # `portta web up` create it first: a bind mount of a
22
- # file that does not exist would become a directory.
23
- #
24
- # The panel never receives a project directory, a socket or the daemon's
25
- # database: only a URL and a secret, and every request it forwards is checked
26
- # against a Portta permission first.
6
+ # One variable, because that is all this module needs now: the connection to
7
+ # the host daemon moved to `panel-host.yaml`, which the panel always gets, and
8
+ # what is left here is the switch that decides whether the panel mounts Task
9
+ # Flow's routes and sockets at all (ADR 0046).
27
10
  # ============================================================================
28
11
 
29
12
  services:
30
13
  web:
31
- extra_hosts:
32
- - "host.docker.internal:host-gateway"
33
14
  environment:
34
15
  PORTTA_TASKFLOW: ${PORTTA_TASKFLOW:-false}
35
- PORTTA_HOST_URL: ${PORTTA_HOST_URL:-http://host.docker.internal:${PORTTA_HOST_PORT:-5111}}
36
- PORTTA_RUNTIME_HOST_TOKEN_FILE: /app/state/host/token
37
- volumes:
38
- - ./state/host/token:/app/state/host/token:ro
@@ -118,11 +118,10 @@ services:
118
118
  # right just when the host uid happens to be 1000.
119
119
  user: ${PORTTA_WEB_USER:-node}
120
120
  environment:
121
- PORTTA_RUNTIME_DB_MODE: ${PORTTA_RUNTIME_DB_MODE:-managed}
122
- PORTTA_RUNTIME_DB_NAME: ${PORTTA_RUNTIME_DB_NAME:-portta}
123
- PORTTA_RUNTIME_DB_USER: ${PORTTA_RUNTIME_DB_USER:-portta}
124
- PORTTA_RUNTIME_DB_PASSWORD: ${PORTTA_RUNTIME_DB_PASSWORD:-}
125
- PORTTA_RUNTIME_DATABASE_URL: ${PORTTA_RUNTIME_DATABASE_URL:-}
121
+ # The panel's database is a file under $PORTTA_HOME, bind-mounted below.
122
+ # There is nothing to configure and no service to wait for: the file is
123
+ # created on first open (ADR 0037).
124
+ PORTTA_RUNTIME_DATABASE_FILE: /app/state/panel/portta.db
126
125
  PORTTA_AUTH_SIGNIN_ATTEMPTS: ${PORTTA_AUTH_SIGNIN_ATTEMPTS:-}
127
126
  ACME_CHALLENGE: ${ACME_CHALLENGE:-dns}
128
127
  ACME_DNS_RESOLVERS: ${ACME_DNS_RESOLVERS:-}
@@ -209,24 +208,6 @@ services:
209
208
  PUBLIC_DOMAIN: ${PUBLIC_DOMAIN:-}
210
209
  CLOUDFLARE_ENABLED: ${CLOUDFLARE_ENABLED:-false}
211
210
  CLOUDFLARE_ZONE: ${CLOUDFLARE_ZONE:-}
212
- # The panel's only outbound integration, and its only long-lived secret
213
- # that is not a Traefik credential. Off by default: with this false the
214
- # panel makes no request to github.com. The private key is passed as a
215
- # PATH, never as a value, because the panel can write this .env and must
216
- # not be able to write the key that authenticates it.
217
- #
218
- # The path is the operator's: the Settings field writes it here, and this
219
- # is the value the panel reads. What is fixed is the DIRECTORY, because
220
- # `./state/github` below is the only place the key is mounted from; the
221
- # filename is whatever GitHub called the .pem. The panel refuses a path
222
- # outside that directory on save, and `portta doctor` fails on one.
223
- # See docs/product/guides/github.md and docs/product/concepts/security.md.
224
- GITHUB_APP_ENABLED: ${GITHUB_APP_ENABLED:-false}
225
- GITHUB_APP_ID: ${GITHUB_APP_ID:-}
226
- GITHUB_APP_PRIVATE_KEY_FILE: ${GITHUB_APP_PRIVATE_KEY_FILE:-/app/state/github/app.pem}
227
- GITHUB_APP_WEBHOOK_SECRET: ${GITHUB_APP_WEBHOOK_SECRET:-}
228
- GITHUB_API_URL: ${GITHUB_API_URL:-https://api.github.com}
229
- GITHUB_SYNC_INTERVAL_MINUTES: ${GITHUB_SYNC_INTERVAL_MINUTES:-15}
230
211
  volumes:
231
212
  # Explicit paths from the host, and nothing else. No project directory is ever
232
213
  # mounted here: see docs/development/adr/0001-decoupled-infrastructure.md.
@@ -237,7 +218,7 @@ services:
237
218
  # state/environment host readiness report from the same collector
238
219
  # state/runner the `{ verb, project }` request the runner reads
239
220
  # state/access tunnel records cleaned when a project is removed
240
- # state/github the GitHub App private key, read-only
221
+ # state/panel the panel's SQLite database, read-write and its own
241
222
  # state/auth credentials the panel manages, never served directly
242
223
  # state/ssh SSH credentials the panel manages, never served directly
243
224
  # traefik/dynamic read-write, and the ONE place the panel configures
@@ -252,8 +233,11 @@ services:
252
233
  - ./state/environment:/app/state/environment:ro
253
234
  - ./state/runner:/app/state/runner
254
235
  - ./state/access:/app/state/access
255
- # The GitHub App private key, read-only. Empty unless you put one there.
256
- - ./state/github:/app/state/github:ro
236
+ # The panel's own database. A directory rather than the file, because
237
+ # SQLite in WAL mode is three files and creates two of them itself — a
238
+ # bind mount of one path would leave the -wal and -shm inside the
239
+ # container, which is how a restart loses the most recent writes.
240
+ - ./state/panel:/app/state/panel
257
241
  - ./state/auth:/app/state/auth
258
242
  - ./state/ssh:/app/state/ssh
259
243
  - ./config/traefik/dynamic:/app/state/traefik-dynamic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codions/portta",
3
- "version": "0.0.0-develop.149.sha-240655c",
3
+ "version": "0.0.0-develop.150.sha-ed65081",
4
4
  "description": "Machine-first CLI for a shared Docker development gateway",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "@clack/prompts": "^1.1.0",
42
42
  "@devcontainers/cli": "0.89.0",
43
43
  "@modelcontextprotocol/sdk": "^1.30.0",
44
- "better-sqlite3": "^13.0.3",
44
+ "better-sqlite3": "^12.11.1",
45
45
  "commander": "14.0.2",
46
46
  "execa": "9.6.1",
47
47
  "pi-acp": "^0.0.33",
@@ -1,63 +0,0 @@
1
- # ============================================================================
2
- # Portta: panel persistence (always, with the panel)
3
- # ============================================================================
4
- # PostgreSQL is a private implementation detail of the panel, and a required
5
- # one: the panel refuses to start without it, so this overlay is selected
6
- # wherever web.yaml is (packages/core/src/config.ts). It has no host port, never
7
- # joins the shared HTTP network, and keeps its data in a named volume that
8
- # `portta down` does not remove — only `portta reset` does.
9
- # ============================================================================
10
-
11
- networks:
12
- webdata:
13
- name: ${PORTTA_DB_NETWORK:-portta-data}
14
- internal: true
15
- labels:
16
- portta.managed: "true"
17
- portta.component: data-network
18
-
19
- volumes:
20
- portta-db:
21
- name: ${PORTTA_DB_VOLUME:-portta-db}
22
- labels:
23
- portta.managed: "true"
24
- portta.component: db-volume
25
-
26
- services:
27
- db:
28
- image: postgres:18.6-alpine
29
- restart: unless-stopped
30
- environment:
31
- POSTGRES_DB: ${PORTTA_RUNTIME_DB_NAME:-portta}
32
- POSTGRES_USER: ${PORTTA_RUNTIME_DB_USER:-portta}
33
- # An empty value makes PostgreSQL refuse to initialise; it never makes it
34
- # trust unauthenticated clients. bootstrap/web up normally fill this.
35
- POSTGRES_PASSWORD: ${PORTTA_RUNTIME_DB_PASSWORD:-}
36
- volumes:
37
- # PostgreSQL 18+ stores data in a major-version subdirectory. Mount the
38
- # parent so upgrades fail explicitly instead of creating nested data.
39
- - portta-db:/var/lib/postgresql
40
- networks:
41
- - webdata
42
- healthcheck:
43
- test: ["CMD-SHELL", 'pg_isready -h 127.0.0.1 -p 5432 -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
44
- interval: 5s
45
- timeout: 3s
46
- retries: 12
47
- start_period: 5s
48
- labels:
49
- traefik.enable: "false"
50
- portta.managed: "true"
51
- portta.component: db
52
- # No `ports:`. Only the panel can reach this service.
53
-
54
- web:
55
- environment:
56
- PORTTA_DB_NETWORK: ${PORTTA_DB_NETWORK:-portta-data}
57
- networks:
58
- - webdata
59
- # The panel exits if Postgres is not accepting connections yet. Compose
60
- # merges this with the socket-proxy wait in web.yaml.
61
- depends_on:
62
- db:
63
- condition: service_healthy
@@ -1,46 +0,0 @@
1
- # ============================================================================
2
- # Portta: the one panel path that authenticates itself
3
- # ============================================================================
4
- # Applied when GITHUB_APP_ENABLED=true and the panel is routed on a domain.
5
- #
6
- # The problem
7
- # -----------
8
- # A GitHub App delivers webhooks to a URL of yours. Every panel path sits
9
- # behind ForwardAuth, which expects a session cookie or a Basic credential --
10
- # GitHub sends neither, so a delivery is refused before the panel ever sees it.
11
- # The panel cannot be told about a push, an issue, or a new installation.
12
- #
13
- # Why an exemption is safe here, and only here
14
- # --------------------------------------------
15
- # This is not a hole in the panel's authentication. It is one path that carries
16
- # a *different* and, for a machine-to-machine callback, stronger one: GitHub
17
- # signs the raw body with HMAC-SHA256 under a secret only it and this host
18
- # know, and `verifySignature` compares in constant time, refusing a missing
19
- # secret, a missing header and a wrong digest alike. Nothing is parsed before
20
- # that check passes.
21
- #
22
- # So the request is authenticated, just not by a cookie. A session cookie would
23
- # be the wrong instrument: GitHub has no session, and any scheme that let it in
24
- # by origin or by address would trust something forgeable.
25
- #
26
- # What this deliberately is not
27
- # -----------------------------
28
- # It is not a general "these URLs are public" list. Every other panel path
29
- # authenticates by session and by nothing else, so exempting one would be an
30
- # unauthenticated door into an API that can start, stop and remove containers.
31
- # This router names one exact path with `Path(...)`, not a prefix -- there is
32
- # no `/api/integrations/github/webhook/../projects` to walk out of.
33
- #
34
- # The priority is above the panel's own router so the exact path wins over
35
- # `Host(...)`; both name the same service, so the panel process is unchanged.
36
- # ============================================================================
37
-
38
- services:
39
- web:
40
- labels:
41
- traefik.enable: "true"
42
- traefik.http.routers.portta-panel-webhook.rule: >-
43
- Host(`${PORTTA_PANEL_ADVERTISED_HOST:-portta-web}`) && Path(`/api/integrations/github/webhook`)
44
- traefik.http.routers.portta-panel-webhook.priority: "20000"
45
- traefik.http.routers.portta-panel-webhook.service: portta-panel
46
- traefik.http.services.portta-panel.loadbalancer.server.port: "8081"