Files
openwork/docs/org-install-links.md

208 lines
9.4 KiB
Markdown

# Organization Install Links
Status: self-host operator guide
Owner: platform/self-host
Related: `ee/apps/den-api/src/install-links.ts`, `ee/apps/den-api/src/routes/org/install-links.ts`, `ee/apps/den-api/src/desktop-connect-grants.ts`, `packaging/helm/openwork-ee/README.md`
## What users get
An organization install link opens a three-step Den page:
1. Download and run the standard OpenWork installer.
2. Return to Den and click **Open OpenWork**.
3. Confirm the exact organization and server in the app, then complete normal
organization sign-in.
This flow does not build a customer-specific installer or ZIP. Branding and
server configuration are applied only after the user confirms the deep link.
Possession of the install link never signs a user in or grants workspace
access.
The guided flow is scoped to Den organization install links. The public
`/desktop` and landing-page downloads keep their existing behavior.
## Upgrade checklist
> **Default:** Install links are active for every organization on every
> deployment. `DEN_INSTALL_LINKS_GATING_ENABLED` is deprecated and inert;
> platform admins can turn an org dark from `/admin` as a kill switch.
### Helm
1. Keep `migrations.enabled=true` for the upgrade. The migration Job creates
both `install_link` and `desktop_connect_grant`.
2. Keep `config.public.connectLinkMode: exchange` (the default). No key is
required.
3. Restart or roll the deployment. No install-link capability toggle is
required for a normal self-hosted installation.
4. No hosted-style rollout flag is required: install links are on by default.
`DEN_INSTALL_LINKS_GATING_ENABLED` is deprecated and inert; configure
`config.public.bootstrapAdminEmails` only if platform admins need `/admin`
access to turn an org dark as a kill switch.
### Docker Compose
Run the migration once, then restart Den:
```bash
docker compose -f packaging/docker/docker-compose.den-dev.yml exec den sh -lc \
"node /app/ee/packages/den-db/dist/scripts/bootstrap.js"
```
If the stack has a custom Compose project name, include the same `-p <project>`
flag.
## Public origins
Set `DEN_BASE_URL` to the externally reachable Den web origin, for example
`https://openwork.example.com`. Den derives Better Auth, CORS/trusted origins,
web-app hosts, API defaults, and MCP resource defaults from that one value. Set
`DEN_API_PUBLIC_URL` only as a compatibility override when you publish a separate
Den API origin or an externally reachable proxy path, such as
`https://openwork.example.com/api/den`. If you publish a separate Den API origin,
the install-link exchange and external MCP clients must be able to reach it.
Invitation acceptance links use the first non-wildcard trusted origin. In the
single-origin setup this is derived from `DEN_BASE_URL`; set
`DEN_BETTER_AUTH_TRUSTED_ORIGINS` only when you need extra web origins during a
migration.
## Installer delivery
Customer-facing planning guidance for this section is published at
[`packages/docs/start-here/installer-delivery.mdx`](../packages/docs/start-here/installer-delivery.mdx).
This repository page remains authoritative for implementation and security
details.
Den first validates the organization install token. It then chooses exactly
one of these paths:
| Deployment | Download behavior | Required client access |
|---|---|---|
| Internet-connected | Immediate `302` to the exact standard asset under `OPENWORK_INSTALLER_RELEASE_REPO` and `OPENWORK_INSTALLER_RELEASE_TAG`. The organization token is not forwarded. | Den web/API, `github.com`, `release-assets.githubusercontent.com`, and `objects.githubusercontent.com` for legacy or rollback paths. |
| Semi-air-gapped | Stream the matching standard installer from `OPENWORK_INSTALLER_ARTIFACTS_DIR`. There is no ZIP or in-memory whole-file buffer. | Den web/API only. |
| Fully internal installer delivery | Same mounted-artifact path, with Den web/API and the installer artifact available entirely inside the isolated network. This describes installer bytes only, not full product isolation. | Internal Den web/API only. |
The standard filenames use the release tag without a leading `v`:
- `openwork-mac-arm64-<version>.dmg`
- `openwork-mac-x64-<version>.dmg`
- `openwork-win-x64-<version>.exe`
- `openwork-linux-x86_64-<version>.AppImage`
- `openwork-linux-arm64-<version>.AppImage`
There is no first-request GitHub download inside Den, artifact lookup API call,
ZIP creation, per-pod cold cache, or different repeated-download path. Every
internet-connected request redirects immediately. Every mounted request streams
the same provisioned file. With more than one Den API replica, mount the same
read-only PVC on every replica.
## Default connection handoff (no key)
`DEN_CONNECT_LINK_MODE=exchange` is the default. When the user clicks **Open
OpenWork**, Den mints a fresh five-minute bearer code and stores only its
SHA-256 hash. The app:
1. posts the code back to the exact HTTPS API origin carried in the deep link
to preview the organization and server;
2. shows an explicit confirmation without changing local configuration; and
3. posts the code to the exchange endpoint after confirmation, consuming it
exactly once before writing `desktop-bootstrap.json`.
The grant is stored in MySQL, not pod memory, so preview and consumption can
land on different Kubernetes replicas. A conditional database update makes
concurrent exchange attempts single-use. Expired rows are removed while new
grants are minted.
The web page fetches a new code at the moment the user clicks **Open**, rather
than relying on the code loaded before a potentially long installer run. If a
code expires or was already used, the user can return to the same Den page and
click **Open** again.
## Optional signed handoff
Signed handoffs remain an explicit upgrade path. They are disabled unless all
of the following are true:
- `DEN_CONNECT_LINK_MODE=signed`;
- `DEN_CONNECT_LINK_KEY_ID` is configured;
- `DEN_CONNECT_LINK_PRIVATE_KEY` contains the dedicated Ed25519 private key;
- the matching public key is already embedded in the desktop build.
For Helm:
```yaml
config:
public:
connectLinkMode: signed
connectLinkKeyId: "owc-2026-07"
secret:
values:
connectLinkPrivateKey: |-
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
```
Generate a pair with `scripts/generate-connect-link-keypair.mjs`. Do not enable
signed mode until the production release embeds the corresponding public key.
This optional mode changes only the configuration handoff; it still downloads
the same standard installer through the same direct or mounted route.
## MDM alternative
Customer-facing guidance for this path is published at
[`packages/docs/start-here/enterprise-desktop-deployment.mdx`](../packages/docs/start-here/enterprise-desktop-deployment.mdx).
Managed deployments can skip the deep-link handoff by deploying a standard
binary — typically the enterprise distribution
(`openwork-enterprise-<os>-<arch>-<version>.<ext>`, published on the
`enterprise` release channel), or the public installer — and writing
`desktop-bootstrap.json` directly:
| OS | Canonical path |
|---|---|
| Windows | `%LOCALAPPDATA%\openwork\desktop-bootstrap.json` (`%XDG_CONFIG_HOME%\openwork\desktop-bootstrap.json` wins if set) |
| macOS/Linux | `$XDG_CONFIG_HOME/openwork/desktop-bootstrap.json`, falling back to `~/.config/openwork/desktop-bootstrap.json` |
```json
{
"baseUrl": "https://openwork.example.com",
"apiBaseUrl": "https://api.openwork.example.com",
"requireSignin": true,
"writtenAt": "2026-07-14T12:00:00.000Z"
}
```
Current builds still read the older `~/.config/openwork` path for
compatibility. When both files exist, the valid configuration with the newest
`writtenAt` wins.
## Security properties
- Install-link tokens and exchange codes are stored only as SHA-256 hashes.
- Connection codes expire after five minutes and are consumed exactly once in
MySQL across all Den API replicas.
- The desktop requires HTTPS outside explicit development loopback mode and
refuses redirects during preview and exchange.
- Returned claims must identify the same API origin carried by the deep link.
- The app displays the exact organization and server before changing local
configuration; cancel and every validation failure leave it untouched.
- Branding URLs from the keyless exchange are not loaded before confirmation.
- The standard release installer bytes are never modified, preserving normal
macOS and Windows signature verification.
## Troubleshooting
| Symptom | Fix |
|---|---|
| Download redirects to GitHub | Expected for internet-connected deployments. Allow `github.com` and its release-asset redirect/CDN host on the user's network. |
| Download returns `404` | The install token is invalid, expired, or revoked. Ask an org member for the current install page link. |
| Mounted download fails | Verify the filename exactly matches the configured release tag and every Den replica mounts the same readable PVC path. |
| **Open OpenWork** cannot prepare a connection | Verify the browser can reach Den API and `DEN_API_PUBLIC_URL` is the correct public HTTPS origin. |
| The app refuses an expired or used link | Return to the same Den install page and click **Open OpenWork** again to mint a fresh code. |
| Signed mode reports an unknown key | Return to `exchange` mode, or ship a desktop build containing the matching public key before re-enabling `signed`. |
| Install links point at the wrong web host | Correct `BETTER_AUTH_URL` and `DEN_BETTER_AUTH_TRUSTED_ORIGINS`, then restart Den. |