starmux 0.0.0-stage → 0.10.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/CONTRIBUTING.md +18 -0
- package/LICENSE +21 -0
- package/README.md +123 -2
- package/SECURITY.md +7 -0
- package/app/README.md +25 -0
- package/bin/starmux.mjs +2 -0
- package/dist/src/access-control.js +222 -0
- package/dist/src/access-control.js.map +1 -0
- package/dist/src/access-dashboard.js +71 -0
- package/dist/src/access-dashboard.js.map +1 -0
- package/dist/src/authorized-workspace-roots.js +77 -0
- package/dist/src/authorized-workspace-roots.js.map +1 -0
- package/dist/src/cli/access.js +53 -0
- package/dist/src/cli/access.js.map +1 -0
- package/dist/src/cli/community-remote-revoke.js +32 -0
- package/dist/src/cli/community-remote-revoke.js.map +1 -0
- package/dist/src/cli/instances.js +451 -0
- package/dist/src/cli/instances.js.map +1 -0
- package/dist/src/cli/live-home-settings.js +35 -0
- package/dist/src/cli/live-home-settings.js.map +1 -0
- package/dist/src/cli/mac-mcp-gateway.js +256 -0
- package/dist/src/cli/mac-mcp-gateway.js.map +1 -0
- package/dist/src/cli/oauth.js +74 -0
- package/dist/src/cli/oauth.js.map +1 -0
- package/dist/src/cli/permissions-owner.js +27 -0
- package/dist/src/cli/permissions-owner.js.map +1 -0
- package/dist/src/cli/service-setup.js +26 -0
- package/dist/src/cli/service-setup.js.map +1 -0
- package/dist/src/cli/setup-wizard.js +149 -0
- package/dist/src/cli/setup-wizard.js.map +1 -0
- package/dist/src/cli/starmux-supervisor.js +11 -0
- package/dist/src/cli/starmux-supervisor.js.map +1 -0
- package/dist/src/cli/starmux.js +550 -0
- package/dist/src/cli/starmux.js.map +1 -0
- package/dist/src/cli/verify-setup.js +53 -0
- package/dist/src/cli/verify-setup.js.map +1 -0
- package/dist/src/community-remote-revocations.js +135 -0
- package/dist/src/community-remote-revocations.js.map +1 -0
- package/dist/src/config-schemas.js +14 -0
- package/dist/src/config-schemas.js.map +1 -0
- package/dist/src/configured-mcp-upstreams.js +194 -0
- package/dist/src/configured-mcp-upstreams.js.map +1 -0
- package/dist/src/contracts.js +2 -0
- package/dist/src/contracts.js.map +1 -0
- package/dist/src/current-permission-probe.js +64 -0
- package/dist/src/current-permission-probe.js.map +1 -0
- package/dist/src/dc-workspace-http.js +388 -0
- package/dist/src/dc-workspace-http.js.map +1 -0
- package/dist/src/deny-dashboard.js +92 -0
- package/dist/src/deny-dashboard.js.map +1 -0
- package/dist/src/desktop-commander-facade.js +60 -0
- package/dist/src/desktop-commander-facade.js.map +1 -0
- package/dist/src/explicit-deny-rules.js +172 -0
- package/dist/src/explicit-deny-rules.js.map +1 -0
- package/dist/src/gateway-status-format.js +66 -0
- package/dist/src/gateway-status-format.js.map +1 -0
- package/dist/src/global-core-control.js +348 -0
- package/dist/src/global-core-control.js.map +1 -0
- package/dist/src/home-write-sandbox.js +50 -0
- package/dist/src/home-write-sandbox.js.map +1 -0
- package/dist/src/instance-migration.js +138 -0
- package/dist/src/instance-migration.js.map +1 -0
- package/dist/src/instance-runtime.js +387 -0
- package/dist/src/instance-runtime.js.map +1 -0
- package/dist/src/instance-store.js +267 -0
- package/dist/src/instance-store.js.map +1 -0
- package/dist/src/instance-worker.js +9 -0
- package/dist/src/instance-worker.js.map +1 -0
- package/dist/src/invocation.js +80 -0
- package/dist/src/invocation.js.map +1 -0
- package/dist/src/live-profile-home-settings.js +80 -0
- package/dist/src/live-profile-home-settings.js.map +1 -0
- package/dist/src/local-desktop-read-probe.js +202 -0
- package/dist/src/local-desktop-read-probe.js.map +1 -0
- package/dist/src/managed-workspace-policy.js +165 -0
- package/dist/src/managed-workspace-policy.js.map +1 -0
- package/dist/src/mcp-results.js +12 -0
- package/dist/src/mcp-results.js.map +1 -0
- package/dist/src/mcp-upstream-router.js +81 -0
- package/dist/src/mcp-upstream-router.js.map +1 -0
- package/dist/src/oauth-keycloak.js +26 -0
- package/dist/src/oauth-keycloak.js.map +1 -0
- package/dist/src/oauth-resource.js +159 -0
- package/dist/src/oauth-resource.js.map +1 -0
- package/dist/src/operator-cli.js +559 -0
- package/dist/src/operator-cli.js.map +1 -0
- package/dist/src/ops-dashboard.js +200 -0
- package/dist/src/ops-dashboard.js.map +1 -0
- package/dist/src/permissions-dashboard.js +97 -0
- package/dist/src/permissions-dashboard.js.map +1 -0
- package/dist/src/platform.js +102 -0
- package/dist/src/platform.js.map +1 -0
- package/dist/src/privacy-diagnostics.js +155 -0
- package/dist/src/privacy-diagnostics.js.map +1 -0
- package/dist/src/private-mcp-activity.js +63 -0
- package/dist/src/private-mcp-activity.js.map +1 -0
- package/dist/src/private-store-lock.js +27 -0
- package/dist/src/private-store-lock.js.map +1 -0
- package/dist/src/project-dev-presets.js +90 -0
- package/dist/src/project-dev-presets.js.map +1 -0
- package/dist/src/project-git-workflow.js +142 -0
- package/dist/src/project-git-workflow.js.map +1 -0
- package/dist/src/project-workspace-policy.js +161 -0
- package/dist/src/project-workspace-policy.js.map +1 -0
- package/dist/src/read-scope.js +44 -0
- package/dist/src/read-scope.js.map +1 -0
- package/dist/src/release1-onboarding.js +120 -0
- package/dist/src/release1-onboarding.js.map +1 -0
- package/dist/src/release1-service.js +131 -0
- package/dist/src/release1-service.js.map +1 -0
- package/dist/src/remote-auth.js +47 -0
- package/dist/src/remote-auth.js.map +1 -0
- package/dist/src/runtime-identity.js +41 -0
- package/dist/src/runtime-identity.js.map +1 -0
- package/dist/src/runtime-paths.js +11 -0
- package/dist/src/runtime-paths.js.map +1 -0
- package/dist/src/starmux-migration.js +172 -0
- package/dist/src/starmux-migration.js.map +1 -0
- package/dist/src/tool-dispatch.js +182 -0
- package/dist/src/tool-dispatch.js.map +1 -0
- package/dist/src/validation.js +35 -0
- package/dist/src/validation.js.map +1 -0
- package/dist/src/workspace-grants.js +159 -0
- package/dist/src/workspace-grants.js.map +1 -0
- package/docs/ACCESS_CONTROL.md +51 -0
- package/docs/ARCHITECTURE.md +51 -0
- package/docs/CLIENTS.md +30 -0
- package/docs/CONNECT_CHATGPT.md +150 -0
- package/docs/INSTALL.md +58 -0
- package/docs/INSTANCES.md +80 -0
- package/docs/MIGRATION.md +37 -0
- package/docs/OAUTH.md +124 -0
- package/docs/PLATFORMS.md +29 -0
- package/docs/README.md +25 -0
- package/docs/RELEASE_STATUS.md +22 -0
- package/docs/REMOTE_SETUP.md +280 -0
- package/docs/SECURITY.md +15 -0
- package/docs/TYPESCRIPT_MIGRATION.md +56 -0
- package/docs/VALIDATION_OAUTH.md +79 -0
- package/docs/VALIDATION_TUNNEL.md +39 -0
- package/docs/VALIDATION_WORKBUDDY.md +59 -0
- package/docs/assets/mcp-connection-route.svg +104 -0
- package/docs/assets/mcp-gateway-simple.svg +62 -0
- package/docs/assets/starmux-app-en.png +0 -0
- package/package.json +58 -4
- package/scripts/prepare.mjs +5 -0
- package/src/dc-workspace-http.mjs +3 -0
- package/test/fixtures/p1-fixture.txt +2 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for helping build an independent, self-hosted MCP Gateway.
|
|
4
|
+
|
|
5
|
+
- File reproducible issues with version, host OS and *sanitized* steps. Never share credentials, local IPs, Tunnel IDs, original MCP messages containing personal files or private logs.
|
|
6
|
+
- Please open a discussion before large architectural changes. Keep local Core tests and authorization rules passing.
|
|
7
|
+
- Fixes must preserve upstream Desktop Commander tool schemas and accurately separate local health from remote caller identity.
|
|
8
|
+
- Feature ideas for programmable hooks, broader multi-upstream routing or multi-computer operation are welcome, but should be labeled **proposals** until independently implemented and tested.
|
|
9
|
+
|
|
10
|
+
See [security](SECURITY.md) and [current preview status](docs/RELEASE_STATUS.md).
|
|
11
|
+
|
|
12
|
+
## TypeScript development
|
|
13
|
+
|
|
14
|
+
Use Node 22.12 or newer. See [platform coverage](docs/PLATFORMS.md) before testing on another host. Run `npm ci`, `npm run typecheck`, and `npm test`. Tests execute compiled JavaScript and use temporary configuration directories and isolated ports. `npm run build` emits Core, CLI and tests to `dist/`; do not edit generated files. `npm pack` rebuilds and includes only the explicit runtime allowlist. No business logic belongs in the compatibility `.mjs` adapters.
|
|
15
|
+
|
|
16
|
+
Keep `strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `verbatimModuleSyntax` and `noEmitOnError` enabled. Treat JSON, CLI options, socket messages and environment input as unknown until checked. Use MCP SDK types for protocol objects; retain upstream metadata and extensions. Internal middleware changes must test refusal before execution and observer failure after execution, including tools/list versus tools/call consistency.
|
|
17
|
+
|
|
18
|
+
The App builder installs locked build dependencies in a temporary staging directory, compiles there, then installs production dependencies in the bundle. Swift sources and the bundle launcher/helper paths are independent of this TypeScript build. Public signing and independent first-user acceptance remain separate release gates; remote authentication results are tracked per client and deployment in [client compatibility](docs/CLIENTS.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mac MCP Gateway contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Starmux
|
|
4
|
+
|
|
5
|
+
### Your computer. Your tools. Connected to your AI.
|
|
6
|
+
|
|
7
|
+
Connect remote AI clients to files, terminals and development tools on your own computer.
|
|
8
|
+
|
|
9
|
+
**Self-hosted MCP · macOS / Linux / Windows CLI preview · MIT**
|
|
10
|
+
|
|
11
|
+
[Quick Start](#quick-start) · [Connect a client](docs/REMOTE_SETUP.md) · [Documentation](docs/README.md)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Install
|
|
18
|
+
|
|
19
|
+
Requires Node.js **22.12 or newer**.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g starmux
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Start in your project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd /path/to/your/project
|
|
29
|
+
|
|
30
|
+
# Safe read-only mode (default)
|
|
31
|
+
starmux up
|
|
32
|
+
|
|
33
|
+
# Full coding mode: file edits, Shell and Git
|
|
34
|
+
starmux up --code
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Starmux initializes itself and runs in the background. Coding mode asks for confirmation the first time; `--code` and `--write` are equivalent. Coding tools use your OS account's permissions, with the project as their default working directory.
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
Starmux · default · running
|
|
41
|
+
Workspace /path/to/your/project
|
|
42
|
+
Mode Read-only
|
|
43
|
+
Runtime background
|
|
44
|
+
MCP http://127.0.0.1:18893/mcp
|
|
45
|
+
Dashboard http://127.0.0.1:18893/access
|
|
46
|
+
Logs starmux logs -f -i "default"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Create an API key (optional)
|
|
50
|
+
|
|
51
|
+
For clients that support Bearer authentication:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
starmux auth key "My AI Client"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This creates a key and enables API-key authentication immediately. Copy the key into your client's private configuration.
|
|
58
|
+
|
|
59
|
+
## Connect your AI client
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
AI client
|
|
63
|
+
→ HTTPS / protected Tunnel
|
|
64
|
+
→ authentication: ingress | API key | OAuth
|
|
65
|
+
→ Starmux at 127.0.0.1:<port>/mcp
|
|
66
|
+
→ your files and development tools
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The local service is ready after `up`. For remote access, choose a connection route:
|
|
70
|
+
|
|
71
|
+
| Client or deployment | Next step |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| WorkBuddy or a client accepting Bearer headers | [HTTPS + API key](docs/REMOTE_SETUP.md#2-https--bearer-api-key) |
|
|
74
|
+
| ChatGPT with browser login | [OAuth provider and connection setup](docs/OAUTH.md) |
|
|
75
|
+
| An existing protected ingress or MCP Tunnel | [Remote connection guide](docs/REMOTE_SETUP.md) |
|
|
76
|
+
|
|
77
|
+
The guide connects the pieces: DNS, HTTPS/proxy or Tunnel, authentication, and a real client tool call. These network services are configured separately from Starmux.
|
|
78
|
+
|
|
79
|
+
## Everyday commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
starmux status
|
|
83
|
+
starmux verify # Local MCP handshake and directory read
|
|
84
|
+
starmux doctor # Environment and configuration checks
|
|
85
|
+
starmux dashboard # Open the local Access console
|
|
86
|
+
starmux logs -f
|
|
87
|
+
starmux down # Alias: stop
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
After initialization, `up` reuses your saved workspace. Use `starmux up .` to switch it to your current directory, or pass another path. `--foreground` keeps the process in your terminal.
|
|
91
|
+
|
|
92
|
+
## More than one workspace
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
starmux up ~/work/repo-a -i work --code
|
|
96
|
+
starmux up ~/personal/blog -i blog --read
|
|
97
|
+
starmux ls
|
|
98
|
+
starmux down -i work
|
|
99
|
+
|
|
100
|
+
starmux config edit -i blog
|
|
101
|
+
starmux config apply -i blog
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Instance names are your labels. Each instance has its own port, configuration, credentials, policies and logs. [Instances and configuration →](docs/INSTANCES.md)
|
|
105
|
+
|
|
106
|
+
## Documentation and support
|
|
107
|
+
|
|
108
|
+
- [Installation](docs/INSTALL.md) and [documentation index](docs/README.md)
|
|
109
|
+
- [Authentication](docs/ACCESS_CONTROL.md) and [permission boundaries](docs/SECURITY.md)
|
|
110
|
+
- [Migrate from Mac MCP Gateway](docs/MIGRATION.md)
|
|
111
|
+
- [Client compatibility](docs/CLIENTS.md), [platform support](docs/PLATFORMS.md), and [release status](docs/RELEASE_STATUS.md)
|
|
112
|
+
- [Contributing](CONTRIBUTING.md) and [architecture](docs/ARCHITECTURE.md)
|
|
113
|
+
|
|
114
|
+
Starmux 0.10.0 is a preview. The CLI supports macOS, Linux and Windows; the native macOS App is available as a development build. See the linked compatibility pages for tested combinations.
|
|
115
|
+
|
|
116
|
+

|
|
117
|
+
|
|
118
|
+
## Built with Desktop Commander
|
|
119
|
+
|
|
120
|
+
Starmux includes a pinned [Desktop Commander](https://github.com/wonderwhy-er/DesktopCommanderMCP) dependency, installed automatically with npm. You do not need to install it separately. Starmux supplies its own self-hosted MCP connection and lifecycle controls.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
Starmux original code is [MIT licensed](LICENSE). Desktop Commander retains its own license and notices. This independent project is not endorsed by Desktop Commander or AI client vendors.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
**Status:** Community preview, version 0.9.0. Optional Gateway API key authentication and revocation are tested locally. Broad external-client compatibility and public Shell deployment acceptance remain unverified.
|
|
4
|
+
|
|
5
|
+
Please **do not post security vulnerabilities, credentials or user data in public GitHub issues**. Before opening the new community repository, its owner should enable **GitHub Private Vulnerability Reporting** or provide another vetted confidential reporting contact. Until that private route is published, avoid sending exploit details publicly.
|
|
6
|
+
|
|
7
|
+
Do not expose the raw Gateway HTTP port to the public internet. Read [security and trust boundaries](docs/SECURITY.md).
|
package/app/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# App source (Apple Silicon)
|
|
2
|
+
|
|
3
|
+
The menu bar App is written in Swift and controls the same local Gateway Core
|
|
4
|
+
as the CLI. The individual Swift sources and build script are included here,
|
|
5
|
+
without historical internal research reports.
|
|
6
|
+
|
|
7
|
+
**Build a UI-only preview (not a self-contained public App)**:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python3 app/scripts/build_ui.py --output-dir /private/tmp/mac-mcp-ui-preview
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Build a self-contained, ad-hoc signed developer candidate**:
|
|
14
|
+
|
|
15
|
+
1. Obtain the **official pinned** Node.js v26.10.0 darwin-arm64 tar.xz archive
|
|
16
|
+
from nodejs.org, verifying its SHA-256 (enforced by the build script).
|
|
17
|
+
2. Run `npm ci`, then
|
|
18
|
+
`python3 build/build_arm64_app.py --node-archive /path/to/node-v26.10.0-darwin-arm64.tar.xz --output /private/tmp/mac-mcp-app-build`.
|
|
19
|
+
The builder creates a separate temporary TypeScript build directory, runs a locked development install and compilation, and copies `dist/src` plus the stable `.mjs` launchers into the App. It then installs only locked production dependencies. The bundled launcher continues to use the pinned internal Node executable; Swift sources and helper paths are unchanged.
|
|
20
|
+
|
|
21
|
+
3. Check codesign and test in a non-production macOS user profile.
|
|
22
|
+
|
|
23
|
+
Ad-hoc signing is for developers. **No Developer ID / Apple notarization is
|
|
24
|
+
provided**. Do not advertise this candidate as a notarized public binary;
|
|
25
|
+
the App ZIP for GitHub Releases needs separate signing and end-user validation.
|
package/bin/starmux.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { oauthConfigSchema, oauthConnectionSchema, validateOAuthConfig, createOAuthVerifier, OAuthError } from './oauth-resource.js';
|
|
2
|
+
import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { lstat, readFile, writeFile, rename, rm, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { createPrivateDirectory, ownedByUser } from './platform.js';
|
|
8
|
+
const credential = z.object({ id: z.string().regex(/^[a-f0-9]{24}$/), name: z.string().trim().min(1).max(80).regex(/^[^\x00-\x1f\x7f]+$/),
|
|
9
|
+
digest: z.string().regex(/^[a-f0-9]{64}$/), createdAt: z.string().datetime(), expiresAt: z.string().datetime().nullable(),
|
|
10
|
+
revokedAt: z.string().datetime().nullable(), lastUsedAt: z.string().datetime().nullable(), permission: z.literal('owner-default') }).strict();
|
|
11
|
+
const schema = z.object({ version: z.literal(1), mode: z.enum(['ingress', 'api-key', 'oauth']), credentials: z.array(credential).max(256),
|
|
12
|
+
oauth: oauthConfigSchema.optional(), oauthConnections: z.array(oauthConnectionSchema).max(512).default([]) }).strict();
|
|
13
|
+
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
14
|
+
const equal = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
15
|
+
export const defaultAccessFile = () => process.env.MAC_MCP_ACCESS_FILE || path.join(homedir(), '.config', 'starmux', 'access-v1.json');
|
|
16
|
+
const active = (c) => !c.revokedAt && (!c.expiresAt || Date.parse(c.expiresAt) > Date.now());
|
|
17
|
+
export async function createAccessControl(file = defaultAccessFile()) {
|
|
18
|
+
if (!path.isAbsolute(file))
|
|
19
|
+
throw Error('ACCESS_FILE_ABSOLUTE_REQUIRED');
|
|
20
|
+
const folder = path.dirname(file);
|
|
21
|
+
createPrivateDirectory(folder);
|
|
22
|
+
async function readPrivate(target) {
|
|
23
|
+
const st = await lstat(target);
|
|
24
|
+
if (!st.isFile() || st.nlink !== 1 || !ownedByUser(target, st, true) || st.size > 262144)
|
|
25
|
+
throw Error('ACCESS_FILE_UNSAFE');
|
|
26
|
+
return readFile(target, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
async function save(data) {
|
|
29
|
+
schema.parse(data);
|
|
30
|
+
const temp = file + '.' + randomBytes(12).toString('hex');
|
|
31
|
+
try {
|
|
32
|
+
await writeFile(temp, JSON.stringify(data, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
33
|
+
await rename(temp, file);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rm(temp, { force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// A filesystem lock serializes mutations across CLI and Core processes.
|
|
40
|
+
async function locked(action) {
|
|
41
|
+
const lock = file + '.lock';
|
|
42
|
+
let acquired = false;
|
|
43
|
+
for (let i = 0; i < 100; i++) {
|
|
44
|
+
try {
|
|
45
|
+
await mkdir(lock, { mode: 0o700 });
|
|
46
|
+
acquired = true;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
if (!(e instanceof Error && 'code' in e && e.code === 'EEXIST'))
|
|
51
|
+
throw e;
|
|
52
|
+
await new Promise(r => setTimeout(r, 20));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!acquired)
|
|
56
|
+
throw Error('ACCESS_STORE_BUSY');
|
|
57
|
+
try {
|
|
58
|
+
return await action();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await rm(lock, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function load() {
|
|
65
|
+
if (!ownedByUser(folder, await lstat(folder), true))
|
|
66
|
+
throw Error('ACCESS_DIRECTORY_UNSAFE');
|
|
67
|
+
const data = schema.parse(JSON.parse(await readPrivate(file)));
|
|
68
|
+
if (data.oauth)
|
|
69
|
+
validateOAuthConfig(data.oauth);
|
|
70
|
+
if (data.mode === 'oauth' && !data.oauth)
|
|
71
|
+
throw Error('OAUTH_CONFIGURATION_REQUIRED');
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
await locked(async () => {
|
|
75
|
+
try {
|
|
76
|
+
await load();
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
if (!(e instanceof Error && 'code' in e && e.code === 'ENOENT'))
|
|
80
|
+
throw e;
|
|
81
|
+
// A persistent marker prevents a deleted store from silently resetting
|
|
82
|
+
// enabled authentication to ingress mode after a CLI call or restart.
|
|
83
|
+
try {
|
|
84
|
+
await lstat(file + '.initialized');
|
|
85
|
+
throw Error('ACCESS_STORE_MISSING');
|
|
86
|
+
}
|
|
87
|
+
catch (markerError) {
|
|
88
|
+
if (!(markerError instanceof Error && 'code' in markerError && markerError.code === 'ENOENT'))
|
|
89
|
+
throw markerError;
|
|
90
|
+
}
|
|
91
|
+
await writeFile(file + '.initialized', '1\n', { flag: 'wx', mode: 0o600 });
|
|
92
|
+
await save({ version: 1, mode: 'ingress', credentials: [], oauthConnections: [] });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const ownerFile = file + '.owner-token';
|
|
96
|
+
async function ownerToken() {
|
|
97
|
+
return locked(async () => {
|
|
98
|
+
try {
|
|
99
|
+
return (await readPrivate(ownerFile)).trim();
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (!(e instanceof Error && 'code' in e && e.code === 'ENOENT'))
|
|
103
|
+
throw e;
|
|
104
|
+
const token = randomBytes(32).toString('hex');
|
|
105
|
+
await writeFile(ownerFile, token + '\n', { flag: 'wx', mode: 0o600 });
|
|
106
|
+
return token;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function authorized(token) {
|
|
111
|
+
if (typeof token !== 'string' || !/^[a-f0-9]{64}$/.test(token))
|
|
112
|
+
return false;
|
|
113
|
+
try {
|
|
114
|
+
return equal(token, (await readPrivate(ownerFile)).trim());
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function snapshot() {
|
|
121
|
+
const data = await load();
|
|
122
|
+
return { mode: data.mode, permission: 'owner-default', oauth: data.oauth ?? null, oauthConnections: data.oauthConnections,
|
|
123
|
+
credentials: data.credentials.map(({ digest: _, ...c }) => ({ ...c, status: c.revokedAt ? 'revoked' : active(c) ? 'active' : 'expired' })) };
|
|
124
|
+
}
|
|
125
|
+
async function create(name, expiresAt = null, activate = false) {
|
|
126
|
+
const secret = 'smx_' + randomBytes(32).toString('base64url');
|
|
127
|
+
const c = credential.parse({ id: randomBytes(12).toString('hex'), name, digest: digest(secret), createdAt: new Date().toISOString(),
|
|
128
|
+
expiresAt, revokedAt: null, lastUsedAt: null, permission: 'owner-default' });
|
|
129
|
+
if (!active(c))
|
|
130
|
+
throw Error('ACCESS_EXPIRY_MUST_BE_FUTURE');
|
|
131
|
+
await locked(async () => { const data = await load(); data.credentials.push(c); if (activate)
|
|
132
|
+
data.mode = 'api-key'; await save(data); });
|
|
133
|
+
return { id: c.id, name: c.name, key: secret, permission: c.permission };
|
|
134
|
+
}
|
|
135
|
+
async function setMode(mode) {
|
|
136
|
+
await locked(async () => {
|
|
137
|
+
const data = await load();
|
|
138
|
+
if (mode === 'api-key' && !data.credentials.some(active))
|
|
139
|
+
throw Error('ACCESS_ACTIVE_KEY_REQUIRED');
|
|
140
|
+
if (mode === 'oauth' && !data.oauth)
|
|
141
|
+
throw Error('OAUTH_CONFIGURATION_REQUIRED');
|
|
142
|
+
data.mode = mode;
|
|
143
|
+
await save(data);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function revoke(id) {
|
|
147
|
+
await locked(async () => {
|
|
148
|
+
const data = await load();
|
|
149
|
+
const c = data.credentials.find(c => c.id === id);
|
|
150
|
+
if (!c)
|
|
151
|
+
throw Error('ACCESS_CREDENTIAL_NOT_FOUND');
|
|
152
|
+
c.revokedAt ??= new Date().toISOString();
|
|
153
|
+
await save(data);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function configureOAuth(value) {
|
|
157
|
+
const config = validateOAuthConfig(value);
|
|
158
|
+
await locked(async () => { const data = await load(); data.oauth = config; await save(data); });
|
|
159
|
+
}
|
|
160
|
+
async function revokeOAuth(id) {
|
|
161
|
+
await locked(async () => {
|
|
162
|
+
const data = await load();
|
|
163
|
+
const c = data.oauthConnections.find(c => c.id === id);
|
|
164
|
+
if (!c)
|
|
165
|
+
throw Error('OAUTH_CONNECTION_NOT_FOUND');
|
|
166
|
+
c.revokedAt ??= new Date().toISOString();
|
|
167
|
+
await save(data);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function oauthSettings() { const data = await load(); return data.mode === 'oauth' ? data.oauth : null; }
|
|
171
|
+
let verifier;
|
|
172
|
+
async function authenticateOAuth(config, header) {
|
|
173
|
+
const signature = JSON.stringify(config);
|
|
174
|
+
if (verifier?.signature !== signature)
|
|
175
|
+
verifier = { signature, verify: createOAuthVerifier(config) };
|
|
176
|
+
const verified = await verifier.verify(header);
|
|
177
|
+
await locked(async () => {
|
|
178
|
+
const data = await load();
|
|
179
|
+
if (data.mode !== 'oauth' || JSON.stringify(data.oauth) !== signature)
|
|
180
|
+
throw new OAuthError(401, 'OAUTH_CONFIGURATION_CHANGED');
|
|
181
|
+
const c = data.oauthConnections.find(c => c.id === verified.id);
|
|
182
|
+
if (c?.revokedAt)
|
|
183
|
+
throw new OAuthError(401, 'OAUTH_CONNECTION_REVOKED');
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
if (c)
|
|
186
|
+
c.lastUsedAt = now;
|
|
187
|
+
else {
|
|
188
|
+
if (data.oauthConnections.length >= 512)
|
|
189
|
+
throw new OAuthError(503, 'OAUTH_CONNECTION_LIMIT');
|
|
190
|
+
data.oauthConnections.push({ id: verified.id, subjectHash: verified.subjectHash, clientId: verified.clientId,
|
|
191
|
+
createdAt: now, lastUsedAt: now, revokedAt: null, permission: 'owner-default' });
|
|
192
|
+
}
|
|
193
|
+
await save(data);
|
|
194
|
+
});
|
|
195
|
+
return { kind: 'verified-oauth', connectionId: verified.id, subjectHash: verified.subjectHash, scopes: verified.scopes, permission: 'owner-default' };
|
|
196
|
+
}
|
|
197
|
+
async function authenticate(header) {
|
|
198
|
+
const data = await load();
|
|
199
|
+
if (data.mode === 'ingress')
|
|
200
|
+
return { kind: 'unverified-local' };
|
|
201
|
+
if (data.mode === 'oauth')
|
|
202
|
+
return authenticateOAuth(data.oauth, header);
|
|
203
|
+
if (!header || !/^Bearer (?:mgw_|smx_)[A-Za-z0-9_-]{43}$/i.test(header))
|
|
204
|
+
throw Error('ACCESS_UNAUTHORIZED');
|
|
205
|
+
const hash = digest(header.slice(7));
|
|
206
|
+
const c = data.credentials.find(c => equal(c.digest, hash) && active(c));
|
|
207
|
+
if (!c)
|
|
208
|
+
throw Error('ACCESS_UNAUTHORIZED');
|
|
209
|
+
// Recheck under the mutation lock: revocation cannot be lost to a usage update.
|
|
210
|
+
await locked(async () => {
|
|
211
|
+
const current = await load();
|
|
212
|
+
const found = current.credentials.find(v => v.id === c.id);
|
|
213
|
+
if (current.mode !== 'api-key' || !found || !active(found))
|
|
214
|
+
throw Error('ACCESS_UNAUTHORIZED');
|
|
215
|
+
found.lastUsedAt = new Date().toISOString();
|
|
216
|
+
await save(current);
|
|
217
|
+
});
|
|
218
|
+
return { kind: 'verified-api-key', credentialId: c.id, permission: 'owner-default' };
|
|
219
|
+
}
|
|
220
|
+
return { file, snapshot, create, setMode, revoke, authenticate, ownerToken, authorized, configureOAuth, revokeOAuth, oauthSettings };
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=access-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/access-control.ts"],"names":[],"mappings":"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","sourcesContent":["import {oauthConfigSchema,oauthConnectionSchema,validateOAuthConfig,createOAuthVerifier,OAuthError,type OAuthConfig} from './oauth-resource.js';\nimport {randomBytes,createHash,timingSafeEqual} from 'node:crypto';\nimport {lstat,readFile,writeFile,rename,rm,mkdir} from 'node:fs/promises';\nimport {homedir} from 'node:os';\nimport path from 'node:path';\nimport {z} from 'zod';\nimport {createPrivateDirectory,ownedByUser} from './platform.js';\nimport type {RequestContext} from './contracts.js';\n\nconst credential=z.object({id:z.string().regex(/^[a-f0-9]{24}$/),name:z.string().trim().min(1).max(80).regex(/^[^\\x00-\\x1f\\x7f]+$/),\n digest:z.string().regex(/^[a-f0-9]{64}$/),createdAt:z.string().datetime(),expiresAt:z.string().datetime().nullable(),\n revokedAt:z.string().datetime().nullable(),lastUsedAt:z.string().datetime().nullable(),permission:z.literal('owner-default')}).strict();\nconst schema=z.object({version:z.literal(1),mode:z.enum(['ingress','api-key','oauth']),credentials:z.array(credential).max(256),\n oauth:oauthConfigSchema.optional(),oauthConnections:z.array(oauthConnectionSchema).max(512).default([])}).strict();\ntype State=z.infer<typeof schema>;\nconst digest=(value:string)=>createHash('sha256').update(value).digest('hex');\nconst equal=(a:string,b:string)=>a.length===b.length&&timingSafeEqual(Buffer.from(a),Buffer.from(b));\nexport const defaultAccessFile=()=>process.env.MAC_MCP_ACCESS_FILE||path.join(homedir(),'.config','starmux','access-v1.json');\nconst active=(c:Pick<z.infer<typeof credential>,'revokedAt'|'expiresAt'>)=>!c.revokedAt&&(!c.expiresAt||Date.parse(c.expiresAt)>Date.now());\nexport async function createAccessControl(file=defaultAccessFile()){\n if(!path.isAbsolute(file))throw Error('ACCESS_FILE_ABSOLUTE_REQUIRED');\n const folder=path.dirname(file);createPrivateDirectory(folder);\n async function readPrivate(target:string){\n const st=await lstat(target);\n if(!st.isFile()||st.nlink!==1||!ownedByUser(target,st,true)||st.size>262144)throw Error('ACCESS_FILE_UNSAFE');\n return readFile(target,'utf8');\n }\n async function save(data:State){\n schema.parse(data);\n const temp=file+'.'+randomBytes(12).toString('hex');\n try{await writeFile(temp,JSON.stringify(data,null,2)+'\\n',{flag:'wx',mode:0o600});await rename(temp,file);}\n finally{await rm(temp,{force:true});}\n }\n // A filesystem lock serializes mutations across CLI and Core processes.\n async function locked<T>(action:()=>Promise<T>):Promise<T>{\n const lock=file+'.lock';let acquired=false;\n for(let i=0;i<100;i++){\n try{await mkdir(lock,{mode:0o700});acquired=true;break;}\n catch(e){if(!(e instanceof Error&&'code' in e&&e.code==='EEXIST'))throw e;await new Promise(r=>setTimeout(r,20));}\n }\n if(!acquired)throw Error('ACCESS_STORE_BUSY');\n try{return await action();}finally{await rm(lock,{recursive:true,force:true});}\n }\n async function load(){\n if(!ownedByUser(folder,await lstat(folder),true))throw Error('ACCESS_DIRECTORY_UNSAFE');\n const data=schema.parse(JSON.parse(await readPrivate(file)) as unknown);\n if(data.oauth)validateOAuthConfig(data.oauth);\n if(data.mode==='oauth'&&!data.oauth)throw Error('OAUTH_CONFIGURATION_REQUIRED');\n return data;\n }\n await locked(async()=>{\n try{await load();}catch(e){\n if(!(e instanceof Error&&'code' in e&&e.code==='ENOENT'))throw e;\n // A persistent marker prevents a deleted store from silently resetting\n // enabled authentication to ingress mode after a CLI call or restart.\n try{await lstat(file+'.initialized');throw Error('ACCESS_STORE_MISSING');}\n catch(markerError){\n if(!(markerError instanceof Error&&'code' in markerError&&markerError.code==='ENOENT'))throw markerError;\n }\n await writeFile(file+'.initialized','1\\n',{flag:'wx',mode:0o600});\n await save({version:1,mode:'ingress',credentials:[],oauthConnections:[]});\n }\n });\n const ownerFile=file+'.owner-token';\n async function ownerToken(){\n return locked(async()=>{\n try{return (await readPrivate(ownerFile)).trim();}catch(e){\n if(!(e instanceof Error&&'code' in e&&e.code==='ENOENT'))throw e;\n const token=randomBytes(32).toString('hex');\n await writeFile(ownerFile,token+'\\n',{flag:'wx',mode:0o600});return token;\n }\n });\n }\n async function authorized(token:unknown){\n if(typeof token!=='string'||! /^[a-f0-9]{64}$/.test(token))return false;\n try{return equal(token,(await readPrivate(ownerFile)).trim());}catch{return false;}\n }\n async function snapshot(){\n const data=await load();return {mode:data.mode,permission:'owner-default' as const,oauth:data.oauth??null,oauthConnections:data.oauthConnections,\n credentials:data.credentials.map(({digest:_,...c})=>({...c,status:c.revokedAt?'revoked':active(c)?'active':'expired'}))};\n }\n async function create(name:string,expiresAt:string|null=null,activate=false){\n const secret='smx_'+randomBytes(32).toString('base64url');\n const c=credential.parse({id:randomBytes(12).toString('hex'),name,digest:digest(secret),createdAt:new Date().toISOString(),\n expiresAt,revokedAt:null,lastUsedAt:null,permission:'owner-default'});\n if(!active(c))throw Error('ACCESS_EXPIRY_MUST_BE_FUTURE');\n await locked(async()=>{const data=await load();data.credentials.push(c);if(activate)data.mode='api-key';await save(data);});\n return {id:c.id,name:c.name,key:secret,permission:c.permission};\n }\n async function setMode(mode:'ingress'|'api-key'|'oauth'){\n await locked(async()=>{const data=await load();\n if(mode==='api-key'&&!data.credentials.some(active))throw Error('ACCESS_ACTIVE_KEY_REQUIRED');\n if(mode==='oauth'&&!data.oauth)throw Error('OAUTH_CONFIGURATION_REQUIRED');\n data.mode=mode;await save(data);});\n }\n async function revoke(id:string){\n await locked(async()=>{const data=await load();const c=data.credentials.find(c=>c.id===id);\n if(!c)throw Error('ACCESS_CREDENTIAL_NOT_FOUND');c.revokedAt??=new Date().toISOString();await save(data);});\n }\n async function configureOAuth(value:unknown){\n const config=validateOAuthConfig(value);\n await locked(async()=>{const data=await load();data.oauth=config;await save(data);});\n }\n async function revokeOAuth(id:string){\n await locked(async()=>{const data=await load();const c=data.oauthConnections.find(c=>c.id===id);\n if(!c)throw Error('OAUTH_CONNECTION_NOT_FOUND');c.revokedAt??=new Date().toISOString();await save(data);});\n }\n async function oauthSettings(){const data=await load();return data.mode==='oauth'?data.oauth!:null;}\n let verifier:{signature:string;verify:ReturnType<typeof createOAuthVerifier>}|undefined;\n async function authenticateOAuth(config:OAuthConfig,header:string|undefined):Promise<RequestContext['identity']>{\n const signature=JSON.stringify(config);\n if(verifier?.signature!==signature)verifier={signature,verify:createOAuthVerifier(config)};\n const verified=await verifier.verify(header);\n await locked(async()=>{\n const data=await load();\n if(data.mode!=='oauth'||JSON.stringify(data.oauth)!==signature)throw new OAuthError(401,'OAUTH_CONFIGURATION_CHANGED');\n const c=data.oauthConnections.find(c=>c.id===verified.id);\n if(c?.revokedAt)throw new OAuthError(401,'OAUTH_CONNECTION_REVOKED');\n const now=new Date().toISOString();\n if(c)c.lastUsedAt=now;\n else{\n if(data.oauthConnections.length>=512)throw new OAuthError(503,'OAUTH_CONNECTION_LIMIT');\n data.oauthConnections.push({id:verified.id,subjectHash:verified.subjectHash,clientId:verified.clientId,\n createdAt:now,lastUsedAt:now,revokedAt:null,permission:'owner-default'});\n }\n await save(data);\n });\n return {kind:'verified-oauth',connectionId:verified.id,subjectHash:verified.subjectHash,scopes:verified.scopes,permission:'owner-default'};\n }\n async function authenticate(header:string|undefined):Promise<RequestContext['identity']>{\n const data=await load();\n if(data.mode==='ingress')return {kind:'unverified-local'};\n if(data.mode==='oauth')return authenticateOAuth(data.oauth!,header);\n if(!header||!/^Bearer (?:mgw_|smx_)[A-Za-z0-9_-]{43}$/i.test(header))throw Error('ACCESS_UNAUTHORIZED');\n const hash=digest(header.slice(7));const c=data.credentials.find(c=>equal(c.digest,hash)&&active(c));\n if(!c)throw Error('ACCESS_UNAUTHORIZED');\n // Recheck under the mutation lock: revocation cannot be lost to a usage update.\n await locked(async()=>{const current=await load();const found=current.credentials.find(v=>v.id===c.id);\n if(current.mode!=='api-key'||!found||!active(found))throw Error('ACCESS_UNAUTHORIZED');\n found.lastUsedAt=new Date().toISOString();await save(current);});\n return {kind:'verified-api-key',credentialId:c.id,permission:'owner-default'};\n }\n return {file,snapshot,create,setMode,revoke,authenticate,ownerToken,authorized,configureOAuth,revokeOAuth,oauthSettings};\n}\nexport type AccessControl=Awaited<ReturnType<typeof createAccessControl>>;\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { discoverOAuth, metadataUrl } from './oauth-resource.js';
|
|
2
|
+
import { permissionHeaders } from './permissions-dashboard.js';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
const loginScript = `(()=>{const k='starmux-owner-token';const q=new URLSearchParams(location.hash.slice(1));const t=q.get('token');if(t){history.replaceState(null,'',location.pathname);sessionStorage.setItem(k,t);}const token=sessionStorage.getItem(k);if(token)for(const input of document.querySelectorAll('input[name="token"]'))input.value=token;})();`;
|
|
5
|
+
const scriptHash = createHash('sha256').update(loginScript).digest('base64');
|
|
6
|
+
export const accessHeaders = Object.freeze({ ...permissionHeaders, 'content-security-policy': permissionHeaders['content-security-policy'] + `; script-src 'sha256-${scriptHash}'` });
|
|
7
|
+
const e = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
8
|
+
export function accessPage(state, created) {
|
|
9
|
+
const form = (op, fields, label) => `<form method="post" action="/access"><input type="hidden" name="op" value="${op}">${fields}<label>Owner management token <input type="password" name="token" required autocomplete="off"></label><button>${label}</button></form>`;
|
|
10
|
+
return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Gateway · Access control</title><style>body{font:16px system-ui;max-width:960px;margin:40px auto;padding:0 24px;background:#f5f7fa;color:#243249}section{background:white;border:1px solid #dae0e8;border-radius:12px;padding:24px;margin:20px 0}input,select,button{font:inherit;padding:8px;margin:8px}label{display:block}code{overflow-wrap:anywhere}td,th{text-align:left;padding:10px;border-bottom:1px solid #ddd}table{width:100%}</style><h1>Access control</h1>
|
|
11
|
+
<p><a href="/dashboard">Dashboard</a> · <a href="/permissions">Workspace permissions</a></p>
|
|
12
|
+
<section><h2>${state.mode === 'ingress' ? 'Managed by ingress' : state.mode === 'oauth' ? 'OAuth' : 'Gateway API key'}</h2><p>Permission: Owner default. Credentials do not change workspace, tool or operating-system permissions.</p><p>${state.mode === 'ingress' ? 'Gateway does not verify caller identity. Restrict remote access through your ingress. HTTPS encryption alone does not authenticate callers.' : 'Every MCP request requires an active credential. Missing, invalid, expired or revoked credentials are rejected.'}</p></section>
|
|
13
|
+
${created ? `<section><h2>Copy this key now</h2><code>${e(created.key)}</code><p>This key is shown only in this response. Keep it private; it grants the owner-approved MCP capabilities.</p></section>` : ''}
|
|
14
|
+
<section><h2>Client credentials</h2><table><tr><th>Name / ID</th><th>Status / Revoked</th><th>Created / Last used / Expiry</th></tr>${state.credentials.map(c => `<tr><td>${e(c.name)}<br><code>${e(c.id)}</code></td><td>${e(c.status)}<br>${e(c.revokedAt || '—')}</td><td>${e(c.createdAt)}<br>${e(c.lastUsedAt || 'Never')}<br>${e(c.expiresAt || 'No expiry')}</td></tr>`).join('') || '<tr><td colspan="3">No credentials</td></tr>'}</table></section>
|
|
15
|
+
<section><h2>Create credential</h2>${form('create', '<label>Name <input name="name" maxlength="80" required></label><label>Expiry (optional, UTC ISO 8601) <input name="expiresAt" placeholder="2027-01-01T00:00:00.000Z"></label>', 'Create')}</section>
|
|
16
|
+
<section><h2>Authentication mode</h2>${form('mode', '<select name="mode"><option value="api-key">Require Gateway API key</option><option value="oauth">Require OAuth (configure first)</option><option value="ingress">Managed by ingress (no Gateway key required)</option></select>', 'Apply mode')}<p>Switching to ingress mode permits requests without a Gateway key. Existing owner permissions still apply.</p></section>
|
|
17
|
+
<section><h2>OAuth configuration</h2><p>Use an existing authorization server. Configuration does not enable OAuth until you change the mode above.</p>
|
|
18
|
+
${state.oauth ? `<p>Issuer: <code>${e(state.oauth.issuer)}</code><br>Resource: <code>${e(state.oauth.resource)}</code><br>Discovery: <code>${e(metadataUrl(state.oauth))}</code><br>Client: ${e(state.oauth.clientId)} · ${e(state.oauth.clientRegistration)}<br>Scope: ${e(state.oauth.scope)}</p>` : '<p>Not configured</p>'}
|
|
19
|
+
${form('oauth-configure', '<label>Issuer URL <input name="issuer" type="url" required></label><label>Public MCP resource URL <input name="resource" type="url" required></label><label>Registered client ID <input name="clientId" required></label><label>Allowed owner subject IDs (comma separated) <input name="subjects" required></label><label>Token endpoint authentication <select name="tokenAuth"><option value="none">none (public client + PKCE)</option><option value="client_secret_basic">client_secret_basic</option><option value="client_secret_post">client_secret_post</option></select></label>', 'Discover and save')}
|
|
20
|
+
<p>This form uses a predefined client and HTTPS. Client secrets stay with the authorization server and ChatGPT; do not enter them here. Advanced registration and local development are available through the CLI.</p></section>
|
|
21
|
+
<section><h2>OAuth connections</h2><p>Revocation blocks the session, including tokens refreshed from it. A new owner-approved login may establish a new session.</p>
|
|
22
|
+
<table><tr><th>Connection ID / Client</th><th>Status</th><th>Last used</th></tr>${state.oauthConnections.map(c => `<tr><td><code>${e(c.id)}</code><br>${e(c.clientId)}</td><td>${c.revokedAt ? 'Revoked' : 'Active'}</td><td>${e(c.lastUsedAt)}</td></tr>`).join('') || '<tr><td colspan="3">No OAuth connections</td></tr>'}</table>
|
|
23
|
+
${form('oauth-revoke', '<label>Connection ID <input name="id" required></label>', 'Revoke OAuth connection')}</section>
|
|
24
|
+
<section><h2>Revoke credential</h2>${form('revoke', '<label>Credential ID <input name="id" required></label>', 'Revoke')}<p>Revocation blocks subsequent requests; it does not cancel an already executing tool.</p></section>
|
|
25
|
+
<p>Get the separate owner management token locally: <code>starmux dashboard --token</code>. Do not give this token to AI clients.</p><script>${loginScript}</script></html>`;
|
|
26
|
+
}
|
|
27
|
+
export async function accessPost(req, access) {
|
|
28
|
+
if (req.headers.origin !== 'http://' + req.headers.host || (req.headers['content-type'] || '').split(';')[0] !== 'application/x-www-form-urlencoded')
|
|
29
|
+
return { status: 403 };
|
|
30
|
+
const size = Number(req.headers['content-length']);
|
|
31
|
+
if (!Number.isSafeInteger(size) || size < 1 || size > 4096)
|
|
32
|
+
return { status: 413 };
|
|
33
|
+
let body = '';
|
|
34
|
+
for await (const chunk of req) {
|
|
35
|
+
body += String(chunk);
|
|
36
|
+
if (Buffer.byteLength(body) > 4096)
|
|
37
|
+
return { status: 413 };
|
|
38
|
+
}
|
|
39
|
+
const q = new URLSearchParams(body);
|
|
40
|
+
for (const key of q.keys())
|
|
41
|
+
if (!['op', 'token', 'name', 'expiresAt', 'mode', 'id', 'issuer', 'resource', 'clientId', 'subjects', 'tokenAuth'].includes(key) || q.getAll(key).length !== 1)
|
|
42
|
+
return { status: 400 };
|
|
43
|
+
if (!await access.authorized(q.get('token')))
|
|
44
|
+
return { status: 403 };
|
|
45
|
+
try {
|
|
46
|
+
let created;
|
|
47
|
+
if (q.get('op') === 'create')
|
|
48
|
+
created = await access.create(q.get('name') || '', q.get('expiresAt') || null);
|
|
49
|
+
else if (q.get('op') === 'revoke')
|
|
50
|
+
await access.revoke(q.get('id') || '');
|
|
51
|
+
else if (q.get('op') === 'mode' && (q.get('mode') === 'ingress' || q.get('mode') === 'api-key' || q.get('mode') === 'oauth'))
|
|
52
|
+
await access.setMode(q.get('mode'));
|
|
53
|
+
else if (q.get('op') === 'oauth-revoke')
|
|
54
|
+
await access.revokeOAuth(q.get('id') || '');
|
|
55
|
+
else if (q.get('op') === 'oauth-configure') {
|
|
56
|
+
const method = q.get('tokenAuth') || 'none';
|
|
57
|
+
if (!['none', 'client_secret_basic', 'client_secret_post'].includes(method))
|
|
58
|
+
return { status: 400 };
|
|
59
|
+
await access.configureOAuth(await discoverOAuth({ issuer: q.get('issuer') || '', resource: q.get('resource') || '',
|
|
60
|
+
clientId: q.get('clientId') || '', allowedSubjects: (q.get('subjects') || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
61
|
+
tokenEndpointAuthMethod: method }));
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
return { status: 400 };
|
|
65
|
+
return { status: 200, html: accessPage(await access.snapshot(), created) };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { status: 400 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=access-dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-dashboard.js","sourceRoot":"","sources":["../../src/access-dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import {discoverOAuth,metadataUrl} from './oauth-resource.js';\nimport type {IncomingMessage} from 'node:http';\nimport type {AccessControl} from './access-control.js';\nimport {permissionHeaders} from './permissions-dashboard.js';\nimport {createHash} from 'node:crypto';\nconst loginScript=`(()=>{const k='starmux-owner-token';const q=new URLSearchParams(location.hash.slice(1));const t=q.get('token');if(t){history.replaceState(null,'',location.pathname);sessionStorage.setItem(k,t);}const token=sessionStorage.getItem(k);if(token)for(const input of document.querySelectorAll('input[name=\"token\"]'))input.value=token;})();`;\nconst scriptHash=createHash('sha256').update(loginScript).digest('base64');\nexport const accessHeaders=Object.freeze({...permissionHeaders,'content-security-policy':permissionHeaders['content-security-policy']+`; script-src 'sha256-${scriptHash}'`});\nconst e=(s:unknown)=>String(s??'').replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]!));\nexport function accessPage(state:Awaited<ReturnType<AccessControl['snapshot']>>,created?:{key:string}){\n const form=(op:string,fields:string,label:string)=>`<form method=\"post\" action=\"/access\"><input type=\"hidden\" name=\"op\" value=\"${op}\">${fields}<label>Owner management token <input type=\"password\" name=\"token\" required autocomplete=\"off\"></label><button>${label}</button></form>`;\n return `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Gateway · Access control</title><style>body{font:16px system-ui;max-width:960px;margin:40px auto;padding:0 24px;background:#f5f7fa;color:#243249}section{background:white;border:1px solid #dae0e8;border-radius:12px;padding:24px;margin:20px 0}input,select,button{font:inherit;padding:8px;margin:8px}label{display:block}code{overflow-wrap:anywhere}td,th{text-align:left;padding:10px;border-bottom:1px solid #ddd}table{width:100%}</style><h1>Access control</h1>\n <p><a href=\"/dashboard\">Dashboard</a> · <a href=\"/permissions\">Workspace permissions</a></p>\n <section><h2>${state.mode==='ingress'?'Managed by ingress':state.mode==='oauth'?'OAuth':'Gateway API key'}</h2><p>Permission: Owner default. Credentials do not change workspace, tool or operating-system permissions.</p><p>${state.mode==='ingress'?'Gateway does not verify caller identity. Restrict remote access through your ingress. HTTPS encryption alone does not authenticate callers.':'Every MCP request requires an active credential. Missing, invalid, expired or revoked credentials are rejected.'}</p></section>\n ${created?`<section><h2>Copy this key now</h2><code>${e(created.key)}</code><p>This key is shown only in this response. Keep it private; it grants the owner-approved MCP capabilities.</p></section>`:''}\n <section><h2>Client credentials</h2><table><tr><th>Name / ID</th><th>Status / Revoked</th><th>Created / Last used / Expiry</th></tr>${state.credentials.map(c=>`<tr><td>${e(c.name)}<br><code>${e(c.id)}</code></td><td>${e(c.status)}<br>${e(c.revokedAt||'—')}</td><td>${e(c.createdAt)}<br>${e(c.lastUsedAt||'Never')}<br>${e(c.expiresAt||'No expiry')}</td></tr>`).join('')||'<tr><td colspan=\"3\">No credentials</td></tr>'}</table></section>\n <section><h2>Create credential</h2>${form('create','<label>Name <input name=\"name\" maxlength=\"80\" required></label><label>Expiry (optional, UTC ISO 8601) <input name=\"expiresAt\" placeholder=\"2027-01-01T00:00:00.000Z\"></label>','Create')}</section>\n <section><h2>Authentication mode</h2>${form('mode','<select name=\"mode\"><option value=\"api-key\">Require Gateway API key</option><option value=\"oauth\">Require OAuth (configure first)</option><option value=\"ingress\">Managed by ingress (no Gateway key required)</option></select>','Apply mode')}<p>Switching to ingress mode permits requests without a Gateway key. Existing owner permissions still apply.</p></section>\n <section><h2>OAuth configuration</h2><p>Use an existing authorization server. Configuration does not enable OAuth until you change the mode above.</p>\n ${state.oauth?`<p>Issuer: <code>${e(state.oauth.issuer)}</code><br>Resource: <code>${e(state.oauth.resource)}</code><br>Discovery: <code>${e(metadataUrl(state.oauth))}</code><br>Client: ${e(state.oauth.clientId)} · ${e(state.oauth.clientRegistration)}<br>Scope: ${e(state.oauth.scope)}</p>`:'<p>Not configured</p>'}\n ${form('oauth-configure','<label>Issuer URL <input name=\"issuer\" type=\"url\" required></label><label>Public MCP resource URL <input name=\"resource\" type=\"url\" required></label><label>Registered client ID <input name=\"clientId\" required></label><label>Allowed owner subject IDs (comma separated) <input name=\"subjects\" required></label><label>Token endpoint authentication <select name=\"tokenAuth\"><option value=\"none\">none (public client + PKCE)</option><option value=\"client_secret_basic\">client_secret_basic</option><option value=\"client_secret_post\">client_secret_post</option></select></label>','Discover and save')}\n <p>This form uses a predefined client and HTTPS. Client secrets stay with the authorization server and ChatGPT; do not enter them here. Advanced registration and local development are available through the CLI.</p></section>\n <section><h2>OAuth connections</h2><p>Revocation blocks the session, including tokens refreshed from it. A new owner-approved login may establish a new session.</p>\n <table><tr><th>Connection ID / Client</th><th>Status</th><th>Last used</th></tr>${state.oauthConnections.map(c=>`<tr><td><code>${e(c.id)}</code><br>${e(c.clientId)}</td><td>${c.revokedAt?'Revoked':'Active'}</td><td>${e(c.lastUsedAt)}</td></tr>`).join('')||'<tr><td colspan=\"3\">No OAuth connections</td></tr>'}</table>\n ${form('oauth-revoke','<label>Connection ID <input name=\"id\" required></label>','Revoke OAuth connection')}</section>\n <section><h2>Revoke credential</h2>${form('revoke','<label>Credential ID <input name=\"id\" required></label>','Revoke')}<p>Revocation blocks subsequent requests; it does not cancel an already executing tool.</p></section>\n <p>Get the separate owner management token locally: <code>starmux dashboard --token</code>. Do not give this token to AI clients.</p><script>${loginScript}</script></html>`;\n}\nexport async function accessPost(req:IncomingMessage,access:AccessControl){\n if(req.headers.origin!=='http://'+req.headers.host||(req.headers['content-type']||'').split(';')[0]!=='application/x-www-form-urlencoded')return {status:403};\n const size=Number(req.headers['content-length']);if(!Number.isSafeInteger(size)||size<1||size>4096)return {status:413};\n let body='';for await(const chunk of req){body+=String(chunk);if(Buffer.byteLength(body)>4096)return {status:413};}\n const q=new URLSearchParams(body);\n for(const key of q.keys())if(!['op','token','name','expiresAt','mode','id','issuer','resource','clientId','subjects','tokenAuth'].includes(key)||q.getAll(key).length!==1)return {status:400};\n if(!await access.authorized(q.get('token')))return {status:403};\n try{\n let created:{key:string}|undefined;\n if(q.get('op')==='create')created=await access.create(q.get('name')||'',q.get('expiresAt')||null);\n else if(q.get('op')==='revoke')await access.revoke(q.get('id')||'');\n else if(q.get('op')==='mode'&&(q.get('mode')==='ingress'||q.get('mode')==='api-key'||q.get('mode')==='oauth'))await access.setMode(q.get('mode') as 'ingress'|'api-key'|'oauth');\n else if(q.get('op')==='oauth-revoke')await access.revokeOAuth(q.get('id')||'');\n else if(q.get('op')==='oauth-configure'){\n const method=q.get('tokenAuth')||'none';\n if(!['none','client_secret_basic','client_secret_post'].includes(method))return {status:400};\n await access.configureOAuth(await discoverOAuth({issuer:q.get('issuer')||'',resource:q.get('resource')||'',\n clientId:q.get('clientId')||'',allowedSubjects:(q.get('subjects')||'').split(',').map(s=>s.trim()).filter(Boolean),\n tokenEndpointAuthMethod:method as 'none'|'client_secret_basic'|'client_secret_post'}));\n }\n else return {status:400};\n return {status:200,html:accessPage(await access.snapshot(),created)};\n }catch{return {status:400};}\n}\n"]}
|