208 lines
9.4 KiB
Markdown
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. |
|