starmux 0.0.0-stage → 0.11.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.
Files changed (148) hide show
  1. package/CONTRIBUTING.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +176 -2
  4. package/SECURITY.md +7 -0
  5. package/app/README.md +29 -0
  6. package/bin/starmux.mjs +2 -0
  7. package/dist/src/access-control.js +222 -0
  8. package/dist/src/access-control.js.map +1 -0
  9. package/dist/src/access-dashboard.js +71 -0
  10. package/dist/src/access-dashboard.js.map +1 -0
  11. package/dist/src/authorized-workspace-roots.js +77 -0
  12. package/dist/src/authorized-workspace-roots.js.map +1 -0
  13. package/dist/src/cli/access.js +53 -0
  14. package/dist/src/cli/access.js.map +1 -0
  15. package/dist/src/cli/community-remote-revoke.js +32 -0
  16. package/dist/src/cli/community-remote-revoke.js.map +1 -0
  17. package/dist/src/cli/instances.js +451 -0
  18. package/dist/src/cli/instances.js.map +1 -0
  19. package/dist/src/cli/live-home-settings.js +35 -0
  20. package/dist/src/cli/live-home-settings.js.map +1 -0
  21. package/dist/src/cli/mac-mcp-gateway.js +256 -0
  22. package/dist/src/cli/mac-mcp-gateway.js.map +1 -0
  23. package/dist/src/cli/oauth.js +74 -0
  24. package/dist/src/cli/oauth.js.map +1 -0
  25. package/dist/src/cli/permissions-owner.js +27 -0
  26. package/dist/src/cli/permissions-owner.js.map +1 -0
  27. package/dist/src/cli/service-setup.js +26 -0
  28. package/dist/src/cli/service-setup.js.map +1 -0
  29. package/dist/src/cli/setup-wizard.js +149 -0
  30. package/dist/src/cli/setup-wizard.js.map +1 -0
  31. package/dist/src/cli/starmux-supervisor.js +11 -0
  32. package/dist/src/cli/starmux-supervisor.js.map +1 -0
  33. package/dist/src/cli/starmux.js +550 -0
  34. package/dist/src/cli/starmux.js.map +1 -0
  35. package/dist/src/cli/verify-setup.js +53 -0
  36. package/dist/src/cli/verify-setup.js.map +1 -0
  37. package/dist/src/community-remote-revocations.js +135 -0
  38. package/dist/src/community-remote-revocations.js.map +1 -0
  39. package/dist/src/config-schemas.js +14 -0
  40. package/dist/src/config-schemas.js.map +1 -0
  41. package/dist/src/configured-mcp-upstreams.js +194 -0
  42. package/dist/src/configured-mcp-upstreams.js.map +1 -0
  43. package/dist/src/contracts.js +2 -0
  44. package/dist/src/contracts.js.map +1 -0
  45. package/dist/src/current-permission-probe.js +64 -0
  46. package/dist/src/current-permission-probe.js.map +1 -0
  47. package/dist/src/dc-workspace-http.js +388 -0
  48. package/dist/src/dc-workspace-http.js.map +1 -0
  49. package/dist/src/deny-dashboard.js +92 -0
  50. package/dist/src/deny-dashboard.js.map +1 -0
  51. package/dist/src/desktop-commander-facade.js +60 -0
  52. package/dist/src/desktop-commander-facade.js.map +1 -0
  53. package/dist/src/explicit-deny-rules.js +172 -0
  54. package/dist/src/explicit-deny-rules.js.map +1 -0
  55. package/dist/src/gateway-status-format.js +66 -0
  56. package/dist/src/gateway-status-format.js.map +1 -0
  57. package/dist/src/global-core-control.js +348 -0
  58. package/dist/src/global-core-control.js.map +1 -0
  59. package/dist/src/home-write-sandbox.js +50 -0
  60. package/dist/src/home-write-sandbox.js.map +1 -0
  61. package/dist/src/instance-migration.js +138 -0
  62. package/dist/src/instance-migration.js.map +1 -0
  63. package/dist/src/instance-runtime.js +387 -0
  64. package/dist/src/instance-runtime.js.map +1 -0
  65. package/dist/src/instance-store.js +267 -0
  66. package/dist/src/instance-store.js.map +1 -0
  67. package/dist/src/instance-worker.js +9 -0
  68. package/dist/src/instance-worker.js.map +1 -0
  69. package/dist/src/invocation.js +80 -0
  70. package/dist/src/invocation.js.map +1 -0
  71. package/dist/src/live-profile-home-settings.js +80 -0
  72. package/dist/src/live-profile-home-settings.js.map +1 -0
  73. package/dist/src/local-desktop-read-probe.js +202 -0
  74. package/dist/src/local-desktop-read-probe.js.map +1 -0
  75. package/dist/src/managed-workspace-policy.js +165 -0
  76. package/dist/src/managed-workspace-policy.js.map +1 -0
  77. package/dist/src/mcp-results.js +12 -0
  78. package/dist/src/mcp-results.js.map +1 -0
  79. package/dist/src/mcp-upstream-router.js +81 -0
  80. package/dist/src/mcp-upstream-router.js.map +1 -0
  81. package/dist/src/oauth-keycloak.js +26 -0
  82. package/dist/src/oauth-keycloak.js.map +1 -0
  83. package/dist/src/oauth-resource.js +159 -0
  84. package/dist/src/oauth-resource.js.map +1 -0
  85. package/dist/src/operator-cli.js +559 -0
  86. package/dist/src/operator-cli.js.map +1 -0
  87. package/dist/src/ops-dashboard.js +200 -0
  88. package/dist/src/ops-dashboard.js.map +1 -0
  89. package/dist/src/permissions-dashboard.js +97 -0
  90. package/dist/src/permissions-dashboard.js.map +1 -0
  91. package/dist/src/platform.js +102 -0
  92. package/dist/src/platform.js.map +1 -0
  93. package/dist/src/privacy-diagnostics.js +155 -0
  94. package/dist/src/privacy-diagnostics.js.map +1 -0
  95. package/dist/src/private-mcp-activity.js +63 -0
  96. package/dist/src/private-mcp-activity.js.map +1 -0
  97. package/dist/src/private-store-lock.js +27 -0
  98. package/dist/src/private-store-lock.js.map +1 -0
  99. package/dist/src/project-dev-presets.js +90 -0
  100. package/dist/src/project-dev-presets.js.map +1 -0
  101. package/dist/src/project-git-workflow.js +142 -0
  102. package/dist/src/project-git-workflow.js.map +1 -0
  103. package/dist/src/project-workspace-policy.js +161 -0
  104. package/dist/src/project-workspace-policy.js.map +1 -0
  105. package/dist/src/read-scope.js +44 -0
  106. package/dist/src/read-scope.js.map +1 -0
  107. package/dist/src/release1-onboarding.js +120 -0
  108. package/dist/src/release1-onboarding.js.map +1 -0
  109. package/dist/src/release1-service.js +131 -0
  110. package/dist/src/release1-service.js.map +1 -0
  111. package/dist/src/remote-auth.js +47 -0
  112. package/dist/src/remote-auth.js.map +1 -0
  113. package/dist/src/runtime-identity.js +41 -0
  114. package/dist/src/runtime-identity.js.map +1 -0
  115. package/dist/src/runtime-paths.js +11 -0
  116. package/dist/src/runtime-paths.js.map +1 -0
  117. package/dist/src/starmux-migration.js +172 -0
  118. package/dist/src/starmux-migration.js.map +1 -0
  119. package/dist/src/tool-dispatch.js +182 -0
  120. package/dist/src/tool-dispatch.js.map +1 -0
  121. package/dist/src/validation.js +35 -0
  122. package/dist/src/validation.js.map +1 -0
  123. package/dist/src/workspace-grants.js +159 -0
  124. package/dist/src/workspace-grants.js.map +1 -0
  125. package/docs/ACCESS_CONTROL.md +51 -0
  126. package/docs/ARCHITECTURE.md +51 -0
  127. package/docs/CLIENTS.md +30 -0
  128. package/docs/CONNECT_CHATGPT.md +150 -0
  129. package/docs/INSTALL.md +58 -0
  130. package/docs/INSTANCES.md +80 -0
  131. package/docs/MIGRATION.md +37 -0
  132. package/docs/OAUTH.md +124 -0
  133. package/docs/PLATFORMS.md +29 -0
  134. package/docs/README.md +25 -0
  135. package/docs/RELEASE_STATUS.md +31 -0
  136. package/docs/REMOTE_SETUP.md +280 -0
  137. package/docs/SECURITY.md +15 -0
  138. package/docs/TYPESCRIPT_MIGRATION.md +56 -0
  139. package/docs/VALIDATION_OAUTH.md +79 -0
  140. package/docs/VALIDATION_TUNNEL.md +39 -0
  141. package/docs/VALIDATION_WORKBUDDY.md +59 -0
  142. package/docs/assets/mcp-connection-route.svg +104 -0
  143. package/docs/assets/mcp-gateway-simple.svg +62 -0
  144. package/docs/assets/starmux-app-en.png +0 -0
  145. package/package.json +58 -4
  146. package/scripts/prepare.mjs +5 -0
  147. package/src/dc-workspace-http.mjs +3 -0
  148. package/test/fixtures/p1-fixture.txt +2 -0
@@ -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,177 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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. **Keep your development environment. Choose what AI can do. Start, stop and revoke access on your terms.**
8
+
9
+ **Self-hosted MCP · Native macOS menu bar App · Cross-platform 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
+ ## Your AI client here. Your tools there.
16
+
17
+ Your repositories, runtimes and development tools already live on your computer. Starmux lets your AI client work with that environment through a self-hosted MCP connection: inspect a project in read-only mode, or enable coding to edit files, run tests and use Git.
18
+
19
+ - **Keep the tools you know.** Work with the files and software already installed on the host.
20
+ - **Choose your AI client.** Use a compatible remote MCP client through HTTPS or a protected Tunnel.
21
+ - **Stay in control.** Start safely with read-only access. Enable coding deliberately, give clients their own credentials and revoke access when needed.
22
+
23
+ Install Starmux on the **computer whose files and tools you want to use**. Your AI client can run elsewhere.
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Install
28
+
29
+ Requires Node.js **22.12 or newer**.
30
+
31
+ ```bash
32
+ npm install -g starmux
33
+ ```
34
+
35
+ ### 2. Start in your project
36
+
37
+ ```bash
38
+ cd /path/to/your/project
39
+
40
+ # Safe read-only mode (default)
41
+ starmux up
42
+
43
+ # Full coding mode: file edits, Shell and Git
44
+ starmux up --code
45
+ ```
46
+
47
+ 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.
48
+
49
+ ```text
50
+ Starmux · default · running
51
+ Workspace /path/to/your/project
52
+ Mode Read-only
53
+ Runtime background
54
+ MCP http://127.0.0.1:18893/mcp
55
+ Dashboard http://127.0.0.1:18893/access
56
+ Logs starmux logs -f -i "default"
57
+ ```
58
+
59
+ ### 3. Create an API key (optional)
60
+
61
+ For clients that support Bearer authentication:
62
+
63
+ ```bash
64
+ starmux auth key "My AI Client"
65
+ ```
66
+
67
+ This creates a key and enables API-key authentication immediately. Copy the key into your client's private configuration.
68
+
69
+ ## How it connects
70
+
71
+ ![AI clients connect through HTTPS or a protected Tunnel to Starmux and workspace tools.](docs/assets/mcp-gateway-simple.svg)
72
+
73
+
74
+ ```text
75
+ AI client
76
+ → HTTPS / protected Tunnel
77
+ → authentication: ingress | API key | OAuth
78
+ → Starmux at 127.0.0.1:<port>/mcp
79
+ → your files and development tools
80
+ ```
81
+
82
+ HTTPS or a protected Tunnel carries the connection; authentication determines who can connect; the permission mode controls what tools can do. Start with the route your client supports:
83
+
84
+
85
+ | Client or deployment | Next step |
86
+ | --- | --- |
87
+ | WorkBuddy or a client accepting Bearer headers | [HTTPS + API key](docs/REMOTE_SETUP.md#2-https--bearer-api-key) |
88
+ | ChatGPT with browser login | [OAuth provider and connection setup](docs/OAUTH.md) |
89
+ | An existing protected ingress or MCP Tunnel | [Remote connection guide](docs/REMOTE_SETUP.md) |
90
+
91
+ 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.
92
+
93
+ ## Everyday commands
94
+
95
+ ```bash
96
+ starmux status
97
+ starmux verify # Local MCP handshake and directory read
98
+ starmux doctor # Environment and configuration checks
99
+ starmux dashboard # Open the local Access console
100
+ starmux logs -f
101
+ starmux down # Alias: stop
102
+ ```
103
+
104
+ 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.
105
+
106
+ ## A quiet place to keep an eye on things
107
+
108
+ The native macOS menu bar App puts service health, workspace permissions, connection topology and recent tool activity in one compact panel. Start or stop the selected workspace, open its Access console, and keep the details close without leaving a management window open.
109
+
110
+ <div align="center">
111
+ <img src="docs/assets/starmux-app-en.png" width="430" alt="Starmux menu bar showing service status, controls, connection topology and recent tool activity" />
112
+ <p><sub>Native macOS menu bar App · English and Chinese · Development build</sub></p>
113
+ </div>
114
+
115
+ The App and CLI control the same instances. Quitting the App stops only instances it started; CLI background instances keep running. [App build instructions](app/README.md)
116
+
117
+ ## More than one workspace
118
+
119
+ ```bash
120
+ starmux up ~/work/repo-a -i work --code
121
+ starmux up ~/personal/blog -i blog --read
122
+ starmux ls
123
+ starmux down -i work
124
+
125
+ starmux config edit -i blog
126
+ starmux config apply -i blog
127
+ ```
128
+
129
+ Instance names are your labels. Each instance has its own port, configuration, credentials, policies and logs. [Instances and configuration →](docs/INSTANCES.md)
130
+
131
+ ## AI client compatibility
132
+
133
+ These are recorded project workflows, including those validated before the Starmux rename. They are not a claim that every client, authentication mode and platform combination has passed the new release.
134
+
135
+ | Client | Recorded validation |
136
+ | --- | --- |
137
+ | **ChatGPT web** | Tunnel + OAuth, including coding and session revocation |
138
+ | **Codex** | Installed Gateway plugin through protected Tunnel |
139
+ | **WorkBuddy** | HTTPS + IPv6 + API-key file reads and writes on macOS |
140
+ | **Doubao (豆包)** | Owner-reported successful use; connection details not recorded |
141
+ | **Claude App** | Compatibility target; end-to-end validation pending |
142
+
143
+ [Exact test scope and client setup guides](docs/CLIENTS.md)
144
+
145
+ ## Platform support
146
+
147
+ | Tool host | Core / CLI | Native App |
148
+ | --- | --- | --- |
149
+ | **macOS** | Starmux 0.10.0 local and published-package workflow tested | SwiftUI development build |
150
+ | **Linux** | Preview; earlier Core tests passed on Ubuntu arm64; new CLI release acceptance pending | Not available |
151
+ | **Windows** | Preview implementation; native validation pending | Not available |
152
+
153
+ [Platform details](docs/PLATFORMS.md) · [Release status](docs/RELEASE_STATUS.md). The macOS App is currently an ad-hoc developer build; a signed, notarized public download is not yet available.
154
+
155
+ ## Where this is going
156
+
157
+ Starmux began with the [community request for a self-hosted Desktop Commander workbench](https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/714). The first priority is a clear path from a local workspace to a real tool call in your preferred AI client.
158
+
159
+ The longer-term direction is a gateway for more MCP servers and clients, with programmable middleware. A public Middleware SDK and user-script plugins are not shipped. Contributions to first-user setup, client compatibility and platform testing are welcome.
160
+
161
+ ## Documentation and support
162
+
163
+ - [Installation](docs/INSTALL.md) and [documentation index](docs/README.md)
164
+ - [Authentication](docs/ACCESS_CONTROL.md) and [permission boundaries](docs/SECURITY.md)
165
+ - [Migrate from Mac MCP Gateway](docs/MIGRATION.md)
166
+ - [Client compatibility](docs/CLIENTS.md), [platform support](docs/PLATFORMS.md), and [release status](docs/RELEASE_STATUS.md)
167
+ - [Contributing](CONTRIBUTING.md) and [architecture](docs/ARCHITECTURE.md)
168
+
169
+ Starmux 0.11.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.
170
+
171
+ ## Built with Desktop Commander
172
+
173
+ 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.
174
+
175
+ ## License
176
+
177
+ 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,29 @@
1
+ # App source (Apple Silicon)
2
+
3
+ The Starmux menu bar App is written in Swift and controls the same local Core
4
+ as the CLI. Its main panel shows the selected workspace, service health, permissions,
5
+ connection topology and recent tool activity. Advanced instance management opens
6
+ from **Workspaces & settings**; launch does not open a management window. The individual Swift sources and build script are included here,
7
+ without historical internal research reports.
8
+
9
+ **Build a UI-only preview (not a self-contained public App)**:
10
+
11
+ ```bash
12
+ python3 app/scripts/build_ui.py --output-dir /private/tmp/mac-mcp-ui-preview
13
+ ```
14
+
15
+ **Build a self-contained, ad-hoc signed developer candidate**:
16
+
17
+ 1. Obtain the **official pinned** Node.js v26.10.0 darwin-arm64 tar.xz archive
18
+ from nodejs.org, verifying its SHA-256 (enforced by the build script).
19
+ 2. Run `npm ci`, then
20
+ `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`.
21
+ 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.
22
+
23
+ 3. Check codesign and test in a non-production macOS user profile.
24
+
25
+ Ad-hoc signing is for developers. **No Developer ID / Apple notarization is
26
+ provided**. Do not advertise this candidate as a notarized public binary;
27
+ the App ZIP for GitHub Releases needs separate signing and end-user validation.
28
+
29
+ For visual acceptance, launch the built executable with `--show-monitor` to open the same menu view in a window. Normal launch stays in the menu bar; reopening the App also shows the monitor.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ await import("../dist/src/cli/starmux.js");
@@ -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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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