@2digits/tlo-mcp 1.0.0 → 2.0.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/README.md CHANGED
@@ -1,65 +1,111 @@
1
1
  # @2digits/tlo-mcp
2
2
 
3
- Local MCP proxy for Teamleader Orbit. OAuth runs locally; your MCP client only needs stdio.
3
+ One personal Teamleader Orbit gateway: local OAuth → authenticated HTTP MCP → OpenTunnel → Executor Cloud.
4
+ Only Orbit tools are exposed. No local Executor, cookie adapter, Amp plugin, Vercel deployment or hosted OAuth bridge.
4
5
 
5
- ## Installation
6
+ ## Run
7
+
8
+ Requires Node.js 24+. In this repository, the root `pitchfork.toml` runs the gateway in the background.
9
+ With `mise activate` enabled in your shell, entering the repository installs the pinned Pitchfork version and
10
+ starts the gateway automatically. It keeps running after you leave the repository or close the terminal.
11
+ The Mac must stay awake. Pitchfork retries failed processes up to three times and waits for the public tunnel
12
+ to be ready, not just the local HTTP port. Each start builds the CLI; source edits need a restart.
13
+
14
+ From the repository root:
6
15
 
7
16
  ```bash
8
- vp add @2digits/tlo-mcp
17
+ mise exec -- pitchfork status tlo-mcp
18
+ mise exec -- pitchfork logs tlo-mcp --tail
19
+ mise exec -- pitchfork restart tlo-mcp
20
+ mise exec -- pitchfork stop tlo-mcp
9
21
  ```
10
22
 
11
- ## Usage
23
+ The public endpoint and Executor connection prompt are in the logs; the gateway key is never printed.
24
+ Stopping the log viewer does not stop the gateway. Stop any manually started gateway before enabling Pitchfork,
25
+ since both use port `4790`. First-time Orbit login still requires completing the browser flow.
26
+ To keep the gateway stopped across repository visits, use `mise exec -- pitchfork disable tlo-mcp`;
27
+ resume with `mise exec -- pitchfork enable tlo-mcp` followed by `mise exec -- pitchfork start tlo-mcp`.
28
+ This setup starts on repository entry, not automatically at macOS login.
12
29
 
13
- ### Login once
30
+ To run in the foreground instead, first stop/disable the Pitchfork daemon. From this package:
14
31
 
15
32
  ```bash
16
- vp exec tlo-mcp login
33
+ vp run run
17
34
  ```
18
35
 
19
- Login opens your browser and listens on an ephemeral `127.0.0.1` callback. Each login uses fresh state and PKCE S256.
20
- The proxy registers its own OAuth client; never reuse an existing authorization URL or another application's client ID.
21
-
22
- ### Start the MCP server
36
+ Or, after installation/build:
23
37
 
24
38
  ```bash
25
39
  vp exec tlo-mcp
26
40
  ```
27
41
 
28
- Configure your MCP client to run `tlo-mcp` without arguments, or `node /absolute/path/to/dist/bin.mjs`.
29
- No OAuth configuration, cookies or tokens belong in the client's config.
42
+ The command:
30
43
 
31
- - Tools, descriptions, input/output schemas and annotations are discovered from the official MCP, including all pages.
32
- - Access tokens refresh before expiry; rotated refresh tokens are saved before use.
33
- - Official server instructions, tool content, structured results and `isError` are preserved.
34
- - JSON and SSE responses are supported. Tool calls are never automatically replayed after failures or rate limits.
35
- - Only tools are proxied; resources, prompts, sampling and upstream notification subscriptions are not currently forwarded.
36
- - Tool discovery runs at startup; restart the proxy to discover changes upstream.
37
- - Orbit's own availability and account permissions still apply.
44
+ 1. Creates/reuses a private gateway API key.
45
+ 2. Opens Orbit login if no local OAuth session exists (fresh client registration, state and PKCE).
46
+ 3. Discovers the official Orbit tools and starts `/mcp` on `127.0.0.1:4790`.
47
+ 4. Connects an isolated `tlo-mcp` OpenTunnel profile with only the `tlo` route.
48
+ 5. Prints a public HTTPS endpoint and a ready-to-paste agent prompt for Executor Cloud.
38
49
 
39
- ### Credentials and logout
50
+ OpenTunnel runs inside this process using its Effect SDK. No separate tunnel CLI or daemon is installed.
51
+ Keep this process running and the computer awake. Ctrl-C closes the HTTP server and tunnel; identity and key persist
52
+ so subsequent starts reuse the URL. Use `--port 4791` if the default port is occupied.
53
+ The first tunnel certificate can take several minutes. Failed startup closes acquired resources.
40
54
 
41
- macOS stores credentials in Keychain under `@2digits/tlo-mcp/orbit`; Keychain errors never fall back to plaintext.
42
- Other platforms use `~/.config/2digits/tlo-mcp/session.json` with file mode `0600` and directory mode `0700`.
43
- Windows ACL hardening is not provided; restrict access to your user profile.
44
- Tokens never appear in process arguments or normal stdout. MCP diagnostics go to stderr.
55
+ ## Connect Executor Cloud
56
+
57
+ Paste the printed prompt into an agent with Executor tools. It registers a Streamable HTTP MCP integration with
58
+ API-key auth: `Authorization: Bearer <gateway-key>`. **Do not configure Orbit OAuth in Executor.**
59
+
60
+ The key is stored at `~/.config/2digits/tlo-mcp/gateway.key` (mode `0600`, directory `0700`).
61
+ On macOS, copy it without printing it:
45
62
 
46
63
  ```bash
47
- vp exec tlo-mcp logout
64
+ pbcopy < ~/.config/2digits/tlo-mcp/gateway.key
48
65
  ```
49
66
 
50
- Logout deletes local credentials; it does not revoke the upstream grant (Orbit advertises no revocation endpoint).
51
- Concurrent refresh/login is protected by a local lock. After a crashed process, confirm it is no longer running before
52
- removing `~/.config/2digits/tlo-mcp/session.lock`. Do not remove a live process's lock.
67
+ Enter it directly in Executor's secure credential form, never in agent chat. On other platforms, open the private
68
+ file locally and copy it into that form. The agent prompt contains no credentials. Executor must use its secure
69
+ handoff for the API key and verify one read-only tool after connecting.
70
+
71
+ ## Authentication and security
53
72
 
54
- ## Migrate from the cookie adapter
73
+ - Orbit credentials remain in macOS Keychain (`@2digits/tlo-mcp/orbit`). Other platforms use a private local session file.
74
+ - OAuth access tokens refresh before expiry; rotated refresh tokens are persisted before use.
75
+ - The gateway key is separate from Orbit credentials. All HTTP routes/methods require it before MCP parsing.
76
+ - Browser requests with an `Origin` header are rejected; non-browser MCP clients are supported.
77
+ - Request bodies are limited to 1 MiB; responses are not cached; request logging is disabled.
78
+ - OpenTunnel terminates TLS on this computer; its relay forwards encrypted traffic, not HTTP plaintext.
79
+ - The hostname is public/discoverable; route names are not authentication. Anyone possessing the gateway key
80
+ can use your Orbit tools with your permissions, including writes. Executor policies do not protect against a leaked key.
81
+ - Tools, schemas, annotations, instructions, structured content, errors and rate limits come from official Orbit.
82
+ Only tools are forwarded; resources/prompts/reverse requests are not exposed. No tool call is automatically replayed.
83
+ - To rotate the gateway key, stop the gateway, remove only `gateway.key`, restart, and update the Executor connection.
55
84
 
56
- The cookie adapter, `legacy` command and hand-written tools have been removed, including their exported services,
57
- schemas and layers. Only the official Orbit tools are available; discover their names and schemas through your MCP client.
85
+ ## Re-login and logout
58
86
 
59
- Remove `legacy` from your command arguments and remove `TLO_SESSION_TOKEN`, `TLO_COOKIES` and `TLO_BASE_URL` from your
60
- configuration. Run `tlo-mcp login`, then start `tlo-mcp` without arguments. Update workflows to use official Orbit tool
61
- names and input schemas instead of the old snake_case tool names.
87
+ ```bash
88
+ vp exec tlo-mcp login
89
+ vp exec tlo-mcp logout
90
+ ```
62
91
 
63
- ## License
92
+ Disable and stop the Pitchfork daemon (or stop the foreground gateway) before either command. After login,
93
+ enable and start it again. Leave it disabled after logout to avoid automatic re-login on repository entry.
94
+ Logout removes local OAuth credentials, not the upstream grant,
95
+ gateway key or tunnel identity. An expired/rejected existing grant fails visibly; re-login explicitly rather than
96
+ silently registering replacement clients. OAuth credential locks fail closed; never remove a live process's lock.
64
97
 
65
- MIT
98
+ ## SDK compatibility patches
99
+
100
+ The workspace pins `@opentunnel/client` and `@opentunnel/protocol` 0.2.2 to Effect 4.0.1 with scoped overrides.
101
+ Versioned patches under `patches/` update old `effect/unstable/*` imports, use Node's standard WebSocket constructor,
102
+ handle WebSocket errors and replace TypeScript parameter properties with erasable fields.
103
+ The SDK/protocol are bundled into the CLI so published output does not depend on Node loading TypeScript dependencies.
104
+ Re-test these patches when upgrading OpenTunnel. The standalone SDK's original Bun requirement does not apply
105
+ to this patched Node CLI.
106
+
107
+ ```bash
108
+ vp test
109
+ vp run types
110
+ vp run build
111
+ ```
package/dist/bin.mjs CHANGED
@@ -1,2 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,r as t,s as n,t as r}from"./proxy-BaBpBFTW.mjs";import*as i from"effect/Effect";import*as a from"effect/Layer";import*as o from"@effect/platform-node/NodeRuntime";import*as s from"@effect/platform-node/NodeServices";import*as c from"effect/cli/Command";const l=c.make(`login`,{},i.fn(`TloMcp.login`)(function*(){yield*(yield*e).login},i.provide(n),i.scoped)),u=c.make(`logout`,{},i.fn(`TloMcp.logout`)(function*(){yield*(yield*e).logout},i.provide(n),i.scoped)),d=c.make(`tlo-mcp`,{},()=>r.pipe(a.provide(t),a.provide(n),a.launch)).pipe(c.withSubcommands([l,u]));i.fn(`TloMcp.run`)(function*(e){return yield*c.runWith(d,{version:`0.1.38`})(e)})(process.argv.slice(2)).pipe(i.provide(s.layer),o.runMain);export{};
2
+ import*as e from"@effect/platform-node/NodeRuntime";import*as t from"@effect/platform-node/NodeServices";import*as n from"effect/Effect";import*as r from"effect/Schema";import*as i from"effect/cli/Command";import*as a from"effect/cli/Flag";import{OAuth as o,Pkce as s}from"@yielded/oauth";import*as c from"effect/Clock";import*as l from"effect/Context";import*as u from"effect/Crypto";import*as d from"effect/Equal";import*as f from"effect/Exit";import*as p from"effect/Layer";import*as m from"effect/Match";import*as h from"effect/Redacted";import*as ee from"effect/Semaphore";import{FetchHttpClient as g,HttpClient as _,HttpClientRequest as v,HttpClientResponse as te,HttpIncomingMessage as ne,HttpRouter as y,HttpServer as re,HttpServerRequest as b,HttpServerResponse as x}from"effect/http";import{ChildProcess as S,ChildProcessSpawner as C}from"effect/process";import{createHash as ie,randomBytes as ae,timingSafeEqual as oe}from"node:crypto";import{createServer as se}from"node:http";import*as ce from"@effect/platform-node/NodeHttpServer";import*as w from"effect/Deferred";import*as le from"effect/Predicate";import*as ue from"node:os";import{homedir as de}from"node:os";import*as fe from"effect/FileSystem";import*as pe from"effect/Path";import*as T from"effect/Stream";import{Context as me,Effect as E,Layer as D,Option as he,Queue as O,Schema as k,Stream as ge}from"effect";import{HttpApi as _e,HttpApiClient as ve,HttpApiEndpoint as A,HttpApiGroup as ye,HttpApiMiddleware as be,HttpApiSchema as xe,HttpApiSecurity as Se,OpenApi as Ce}from"effect/http-api";import*as j from"node:fs/promises";import*as M from"node:path";import*as we from"node:net";import{Duplex as Te}from"node:stream";import*as Ee from"node:tls";import*as De from"effect/ByteSize";import*as N from"effect/ai/McpProtocol";import*as Oe from"effect/ai/McpServer";import*as ke from"effect/Option";import*as Ae from"effect/Struct";import*as P from"effect/ai/McpSchema";import*as je from"effect/encoding/Sse";const Me=`https://api.orbit.teamleader.eu`,F=`${Me}/mcp`,Ne=r.RedactedFromValue(r.String.check(r.isMinLength(1))),Pe=r.Struct({clientId:r.String.check(r.isMinLength(1)),clientSecret:r.optionalKey(Ne),accessToken:Ne,refreshToken:r.optionalKey(Ne),expiresAt:r.optionalKey(r.Finite)});var I=class extends r.TaggedError()(`OrbitAuthError`,{message:r.String}){};function Fe(e,t,n){let r=t.refreshToken??e.refreshToken;return{clientId:e.clientId,...e.clientSecret===void 0?{}:{clientSecret:e.clientSecret},accessToken:t.accessToken,...r===void 0?{}:{refreshToken:r},...t.expiresIn===void 0?{}:{expiresAt:n+t.expiresIn*1e3}}}function Ie(e,t){return e.expiresAt===void 0||e.expiresAt<=t+6e4}function Le(e){return e.clientSecret===void 0?{method:`none`,publicClient:!0}:{method:`client_secret_post`,secret:e.clientSecret.pipe(h.value,h.make)}}function Re(e,t){let n=e.searchParams.get(`state`);if(n===null||e.pathname!==`/callback`||e.searchParams.getAll(`state`).length!==1)return;let r=Buffer.from(t),i=Buffer.from(n);if(r.length!==i.length||!oe(r,i))return;if(e.searchParams.has(`error`))throw Error(`Orbit authorization was denied.`);let a=e.searchParams.get(`code`);if(a!==null&&a.length!==0&&e.searchParams.getAll(`code`).length===1)return a}const ze=n.fn(`OrbitAuth.listenForLogin`)(function*(e){let t=yield*w.make(),r=yield*n.sync(se),i=yield*ce.make(()=>r,{port:0,host:`127.0.0.1`}).pipe(n.mapError(()=>I.make({message:`Could not bind OAuth callback.`})));if(!le.isTagged(i.address,`InetAddressV4`))return yield*I.make({message:`Could not bind OAuth callback.`});let a=`127.0.0.1:${i.address.port}`,o=`http://${a}/callback`,s={"content-type":`text/plain; charset=utf-8`,"cache-control":`no-store`,"referrer-policy":`no-referrer`,"content-security-policy":`default-src 'none'`},c=x.text(`Invalid callback.`,{status:400,headers:s}),l=x.text(`Authorization received. Return to your terminal to complete login.`,{headers:s}),u=x.text(`Authorization denied. Return to your terminal.`,{status:400,headers:s}),d=n.fn(`OrbitAuth.handleCallback`)(function*(){let r=yield*b.HttpServerRequest;return r.method!==`GET`||r.headers.host!==a||(yield*w.isDone(t))?c:yield*n.try({try:()=>Re(new URL(r.url,o),e),catch:()=>I.make({message:`Orbit authorization was denied.`})}).pipe(n.matchEffect({onFailure:e=>w.fail(t,e).pipe(n.map(e=>e?u:c)),onSuccess:e=>e===void 0?n.succeed(c):w.succeed(t,e).pipe(n.map(e=>e?l:c))}))}),f=yield*y.toHttpEffect(y.add(`*`,`*`,d()));return yield*re.serveEffect(f).pipe(n.provideService(re.HttpServer,i)),yield*n.addFinalizer(()=>n.sync(()=>r.closeAllConnections())),{redirectUri:o,code:w.await(t).pipe(n.timeout(`3 minutes`),n.mapError(()=>I.make({message:`Login denied or timed out. Run tlo-mcp login again.`})))}}),Be=r.fromJsonString(Pe),Ve=`@2digits/tlo-mcp/orbit`;var L=class extends l.Service()(`@2digits/tlo-mcp/oauth/store/SessionStore`){};function He(e){return`"${e.replaceAll(`\\`,String.raw`\\`).replaceAll(`"`,String.raw`\"`)}"`}function Ue(e={}){return p.effect(L,n.gen(function*(){let t=yield*fe.FileSystem,i=yield*pe.Path,a=yield*C.ChildProcessSpawner,o=e.platform??process.platform,s=e.directory??i.join(de(),`.config`,`2digits`,`tlo-mcp`),c=i.join(s,`session.json`),l=i.join(s,`session.lock`),u=t.makeDirectory(s,{recursive:!0,mode:448}).pipe(n.andThen(t.chmod(s,448))),d=n.fn(`SessionStore.security`)(function*(e,t){let r=yield*a.spawn(S.make(`/usr/bin/security`,e,{stdin:t===void 0?`ignore`:T.succeed(new TextEncoder().encode(t)),stderr:`ignore`}));return yield*n.all({output:r.stdout.pipe(T.decodeText(),T.mkString),exitCode:r.exitCode},{concurrency:`unbounded`})},n.scoped),f=n.fn(`SessionStore.load`)(function*(){let e;if(o===`darwin`){let t=yield*d([`find-generic-password`,`-a`,`orbit`,`-s`,Ve,`-w`]);if(t.exitCode===44)return;if(t.exitCode!==0)return yield*I.make({message:`Could not read macOS Keychain.`});e=t.output.trim()}else{let r=yield*t.readFileString(c).pipe(n.catchReason(`PlatformError`,`NotFound`,()=>n.void));if(r===void 0)return;e=r}return yield*r.decodeEffect(Be)(e)},n.mapError(()=>I.make({message:`Could not read credentials. Run tlo-mcp login again.`}))),p=n.fn(`SessionStore.save`)(function*(e){let n=yield*r.encodeEffect(Be)(e);if(o===`darwin`){if((yield*d([`-q`,`-i`],`add-generic-password -U -a orbit -s ${He(Ve)} -w ${He(n)}\n`)).exitCode!==0)return yield*I.make({message:`Could not save macOS Keychain credentials.`})}else{yield*u;let e=yield*t.makeTempDirectoryScoped({directory:s,prefix:`session-`}),r=i.join(e,`session.json`);yield*t.writeFileString(r,n,{mode:384}),yield*t.rename(r,c)}},n.scoped,n.mapError(()=>I.make({message:`Could not persist OAuth credentials. Run tlo-mcp login again.`}))),m=n.fn(`SessionStore.clear`)(function*(){if(o===`darwin`){let e=yield*d([`delete-generic-password`,`-a`,`orbit`,`-s`,Ve]);if(e.exitCode!==0&&e.exitCode!==44)return yield*I.make({message:`Could not remove Keychain credentials.`})}else yield*t.remove(c,{force:!0})},n.mapError(()=>I.make({message:`Could not remove OAuth credentials.`}))),h=n.fn(`SessionStore.lock`)(function*(){yield*u,yield*n.acquireRelease(t.writeFileString(l,String(process.pid),{flag:`wx`,mode:384}).pipe(n.catchReason(`PlatformError`,`AlreadyExists`,()=>I.make({message:`OAuth credentials locked. Close other login/refresh processes. After a crash only, remove ${l}.`}))),()=>t.remove(l).pipe(n.orDie))},n.mapError(e=>r.is(I)(e)?e:I.make({message:`Could not acquire OAuth credential lock. Check directory permissions.`})));return L.of({load:f(),save:p,clear:m(),lock:h()})}))}const We=Ue(),Ge=r.Struct({client_id:r.String.check(r.isMinLength(1)),client_secret:r.String.check(r.isMinLength(1)).pipe(r.RedactedFromValue,r.optionalKey)});var R=class extends l.Service()(`@2digits/tlo-mcp/oauth/OrbitAuth`){};const Ke=p.effect(R,n.gen(function*(){let e=yield*L,t=yield*u.Crypto,i=yield*_.HttpClient,a=yield*C.ChildProcessSpawner,l=yield*ee.make(1),p=yield*n.fn(`OrbitAuth.discover`)(function*(){let e=yield*_.withScope(i).get(`${Me}/.well-known/oauth-authorization-server`);if(e.status!==200)return yield*I.make({message:`Could not discover Orbit OAuth endpoints.`});let t=yield*e.json.pipe(n.flatMap(r.decodeUnknownEffect(o.Metadata)));return t.issuer!==`https://api.orbit.teamleader.eu`||t.authorization_endpoint!==`https://api.orbit.teamleader.eu/oauth2/Authorize`||t.token_endpoint!==`https://api.orbit.teamleader.eu/oauth2/access_token`?yield*I.make({message:`Unexpected Orbit endpoints; refusing to send credentials.`}):t},n.scoped,n.timeout(`15 seconds`),n.mapError(()=>I.make({message:`Invalid or unavailable Orbit OAuth metadata.`})))().pipe(n.cachedWithTTL(e=>f.isSuccess(e)?1/0:0)),te=n.fn(`OrbitAuth.accessToken`)(function*(){let t=yield*e.load;if(t===void 0)return yield*I.make({message:`Not authenticated. Run tlo-mcp login first.`});if(!Ie(t,yield*c.currentTimeMillis))return t.accessToken;if(t.refreshToken===void 0)return t.expiresAt===void 0?t.accessToken:yield*I.make({message:`Orbit session expired without a refresh token. Run tlo-mcp login.`});let r=yield*p,i=Fe(t,yield*(yield*o.make({metadata:r,clientId:t.clientId,authentication:Le(t),timeoutMs:15e3})).refreshGrant({refreshToken:t.refreshToken,resources:[F]}).pipe(n.flatMap(o.tokens)),yield*c.currentTimeMillis);return yield*e.save(i),i.accessToken}),ne=n.fn(`OrbitAuth.register`)(function*(e){let t=yield*v.post(`${Me}/oauth2/register`).pipe(v.bodyJson({client_name:`@2digits/tlo-mcp`,redirect_uris:[e],response_types:[`code`],grant_types:[`authorization_code`,`refresh_token`],token_endpoint_auth_method:`none`}),n.flatMap(_.withScope(i).execute));return t.status!==200&&t.status!==201?yield*I.make({message:`Orbit client registration failed (HTTP ${t.status}).`}):yield*t.json.pipe(n.flatMap(r.decodeUnknownEffect(Ge)))},n.scoped,n.timeout(`15 seconds`)),y=n.fn(`OrbitAuth.openBrowser`)(function*(e){return yield*(yield*m.value(process.platform).pipe(m.when(`darwin`,()=>S.make({stdout:`ignore`,stderr:`ignore`,forceKillAfter:`1 second`})`open ${h.value(e)}`),m.when(`win32`,()=>S.make({stdout:`ignore`,stderr:`ignore`,forceKillAfter:`1 second`})`rundll32 url.dll,FileProtocolHandler ${h.value(e)}`),m.orElse(()=>S.make({stdout:`ignore`,stderr:`ignore`,forceKillAfter:`1 second`})`xdg-open ${h.value(e)}`))).exitCode},n.scoped,n.timeout(`5 seconds`),n.filterOrFail(d.equals(C.ExitCode(0)),()=>I.make({message:`Could not open browser.`})),(e,t)=>e.pipe(n.catch(()=>n.sync(()=>{process.stderr.write(`Open this URL in your browser:\n${h.value(t)}\n`)})))),re=n.fn(`OrbitAuth.login`)(function*(){let t=yield*p,r=yield*s.random(),i=yield*s.make(),a=yield*r.pipe(h.value,ze),l=yield*ne(a.redirectUri),u={clientId:l.client_id,...l.client_secret===void 0?{}:{clientSecret:l.client_secret}},d=yield*o.make({metadata:t,...u,authentication:Le(u),timeoutMs:15e3}),f=yield*d.authorizationUrl({redirectUri:a.redirectUri,scopes:[],state:r,codeChallenge:i.challenge,resources:[F]});yield*y(f);let m=yield*a.code,ee=yield*d.codeGrant({code:h.make(m),redirectUri:a.redirectUri,pkceVerifier:i.verifier,resources:[F]}).pipe(n.flatMap(o.tokens));yield*e.save(Fe(u,ee,yield*c.currentTimeMillis)),yield*n.sync(()=>{process.stderr.write(`Orbit login complete.
3
+ `)})}),b=n.fn(`OrbitAuth.locked`)(function*(t){return yield*e.lock,yield*t},n.scoped,l.withPermit,n.provideService(_.HttpClient,i),n.provideService(u.Crypto,t),n.provideService(C.ChildProcessSpawner,a),n.provideService(g.RequestInit,{redirect:`error`}),n.mapError(e=>r.is(I)(e)?e:I.make({message:`Orbit authentication unavailable. Check connectivity or run tlo-mcp login again.`})));return R.of({accessToken:b(te()),login:b(re()),logout:b(e.clear)})})).pipe(p.provide(We),p.provide(g.layer)),qe=k.String.pipe(k.brand(`CSR.Raw`)),Je=k.String.pipe(k.brand(`Hostname`));function Ye(){return(e,t)=>{class n{static makeUnsafe(e){return e}}return Object.setPrototypeOf(n,t),n}}var Xe=class extends Ye()(`CertificateID`,k.String){};k.String.pipe(k.brand(`ChallengeToken`));var Ze=class extends k.Class(`Certificate/StateChallenge`)({type:k.Literal(`challenge`),token:k.String,key:k.String}){},Qe=class extends k.Class(`Certificate/StateIssuing`)({type:k.Literal(`issuing`)}){},$e=class extends k.Class(`Certificate/StateReady`)({type:k.Literal(`ready`),certificate:k.String,chain:k.String,expiry:k.String}){},et=class extends k.Class(`Certificate/StateFailed`)({type:k.Literal(`failed`),reason:k.String}){};const tt=k.Union([Ze,Qe,$e,et]);var nt=class extends k.Class(`Certificate`)({id:Xe,state:tt}){},z=class extends Ye()(`TunnelID`,k.String){};const B=k.String.pipe(k.brand(`TunnelToken`));k.TaggedError()(`NotFound`,{tunnelID:z}),k.TaggedError()(`NoCertificate`,{tunnelID:z}),k.TaggedError()(`CertificateNotReady`,{tunnelID:z,currentState:k.String}),k.TaggedError()(`InvalidHostname`,{provided:Je,expected:Je});const rt=k.Literals([`offline`,`online`]);var it=class extends k.Class(`Tunnel/Info`)({id:z,hostname:Je,state:rt,certificateID:Xe.pipe(k.optional)}){},at=`0.2.2`,ot=class extends k.TaggedError()(`InvalidRequestError`,{message:k.String},{httpApiStatus:400}){},st=class extends k.TaggedError()(`UnauthorizedError`,{message:k.String},{httpApiStatus:401}){},V=class extends k.TaggedError()(`TunnelNotFoundError`,{tunnelID:k.String,message:k.String},{httpApiStatus:404}){},ct=class extends k.TaggedError()(`CertificateNotFoundError`,{tunnelID:k.String,message:k.String},{httpApiStatus:404}){},lt=class extends k.TaggedError()(`InvalidHostnameError`,{provided:k.String,expected:k.String,message:k.String},{httpApiStatus:400}){},ut=class extends k.TaggedError()(`HostnameUnavailableError`,{name:k.String,message:k.String},{httpApiStatus:409}){},dt=class extends k.TaggedError()(`CertificateInProgressError`,{tunnelID:k.String,message:k.String},{httpApiStatus:409}){},H=class extends k.TaggedError()(`ServiceUnavailableError`,{message:k.String},{httpApiStatus:503}){};me.Service()(`@opentunnel/protocol/OpenTunnelAuthorizationToken`);var U=class extends be.Service()(`@opentunnel/protocol/OpenTunnelAuthorization`,{security:{bearer:Se.bearer},error:st}){};const ft=k.Struct({name:k.String.pipe(k.optional)}),pt=k.Struct({tunnel:it,token:B}).pipe(xe.status(201)),mt=ye.make(`tunnel`).add(A.post(`tunnel.create`,`/api/tunnel`,{payload:ft,success:pt,error:[ot,ut,H]})).add(A.get(`tunnel.get`,`/api/tunnel/:id`,{params:k.Struct({id:z}),success:it,error:[V,H]}).middleware(U)).add(A.post(`tunnel.bindCertificate`,`/api/tunnel/:id/certificate`,{params:k.Struct({id:z}),payload:k.Struct({csr:qe}),success:nt.pipe(xe.status(202)),error:[ot,lt,V,dt,H]}).middleware(U)).add(A.get(`tunnel.getCertificate`,`/api/tunnel/:id/certificate`,{params:k.Struct({id:z}),success:nt,error:[V,ct,H]}).middleware(U)).add(A.delete(`tunnel.remove`,`/api/tunnel/:id`,{params:k.Struct({id:z}),success:xe.NoContent,error:[V,H]}).middleware(U)).add(A.get(`tunnel.connect`,`/api/tunnel/:id/connect`,{params:k.Struct({id:z}),success:k.Boolean,error:[V]}).annotateMerge(Ce.annotations({summary:`Connect a bridge WebSocket`,description:`This endpoint upgrades to the opentunnel WebSocket protocol.`}))).annotateMerge(Ce.annotations({title:`tunnel`})),ht=_e.make(`opentunnel`).add(mt).annotateMerge(Ce.annotations({title:`OpenTunnel API`,version:`0.1.0`,description:`Create and manage blind TLS tunnels.`})),gt=`opentunnel-sdk/${at}`;var _t=class e extends me.Service()(`@opentunnel/client/OpenTunnelApiClient`){static layer(t){return D.effect(e,E.gen(function*(){let e=(yield*_.HttpClient).pipe(_.mapRequest(v.setHeader(`user-agent`,gt)));return{client:yield*ve.makeWith(ht,{baseUrl:t.api,httpClient:e}),authorized:n=>ve.makeWith(ht,{baseUrl:t.api,httpClient:e.pipe(_.mapRequest(v.bearerToken(n)))})}})).pipe(D.provide(g.layer))}};const W=(e,...t)=>{let n=vt(t),r=n.length<128?[n.length]:(()=>{let e=[];for(let t=n.length;t>0;t>>=8)e.unshift(t&255);return[128|e.length,...e]})();return vt([new Uint8Array([e,...r]),n])},vt=e=>{let t=new Uint8Array(e.reduce((e,t)=>e+t.length,0)),n=0;for(let r of e)t.set(r,n),n+=r.length;return t},G=(...e)=>W(48,...e),yt=(...e)=>W(49,...e),bt=e=>W(12,new TextEncoder().encode(e)),xt=e=>W(130,new TextEncoder().encode(e)),K=e=>{let[t,n,...r]=e.split(`.`).map(Number),i=[t*40+n];for(let e of r){let t=[e&127];for(let n=e>>7;n>0;n>>=7)t.unshift(n&127|128);i.push(...t)}return W(6,new Uint8Array(i))},St=e=>{let t=0;for(;t<e.length-1&&e[t]===0;)t++;let n=e.subarray(t);return W(2,n[0]&128?vt([new Uint8Array([0]),n]):n)};async function Ct(e,t){let n=new Uint8Array(await crypto.subtle.exportKey(`spki`,t.publicKey)),r=G(W(2,new Uint8Array([0])),G(yt(G(K(`2.5.4.3`),bt(e)))),n,W(160,G(K(`1.2.840.113549.1.9.14`),yt(G(G(K(`2.5.29.17`),W(4,G(xt(e),xt(`*.${e}`))))))))),i=new Uint8Array(await crypto.subtle.sign({name:`ECDSA`,hash:`SHA-256`},t.privateKey,r)),a=G(St(i.subarray(0,32)),St(i.subarray(32))),o=G(r,G(K(`1.2.840.10045.4.3.2`)),W(3,new Uint8Array([0]),a));return`-----BEGIN CERTIFICATE REQUEST-----\n${Buffer.from(o).toString(`base64`).replace(/.{1,64}/g,`$&
4
+ `)}-----END CERTIFICATE REQUEST-----`}var wt=class extends k.TaggedError()(`OpenTunnelClientError`,{message:k.String,cause:k.optional(k.Defect())}){},q=class extends k.TaggedError()(`OpenTunnelStorageError`,{message:k.String,cause:k.optional(k.Defect())}){};function Tt(e={}){let t=e.env??process.env,n=e.home??ue.homedir(),r=t.XDG_DATA_HOME??M.join(n,`.local`,`share`),i=M.join(r,`opentunnel`),a=e=>M.join(i,e),o=e=>E.tryPromise({try:()=>j.readFile(e,`utf8`),catch:e=>e}).pipe(E.catch(t=>E.gen(function*(){if(!(typeof t==`object`&&t&&`code`in t&&t.code===`ENOENT`))return yield*new q({message:`Failed to read ${e}`,cause:t})}))),s=E.fn(`OpenTunnelStorage.profiles`)(function*(){return(yield*E.tryPromise({try:()=>j.readdir(i,{withFileTypes:!0}),catch:e=>e}).pipe(E.catch(e=>E.gen(function*(){return typeof e==`object`&&e&&`code`in e&&e.code===`ENOENT`?[]:yield*new q({message:`Failed to list ${i}`,cause:e})})))).filter(e=>e.isDirectory()).map(e=>e.name).sort()}),c=E.fn(`OpenTunnelStorage.load`)(function*(e){let t=a(e),n=yield*o(M.join(t,`tunnel.json`));if(!n)return;let[r,i,s,c]=yield*E.all([o(M.join(t,`token`)),o(M.join(t,`private-key.pem`)),o(M.join(t,`certificate.pem`)),o(M.join(t,`chain.pem`))]);if(!r||!i||!s||c===void 0)return yield*new q({message:`Incomplete tunnel identity for ${e}`});let l=JSON.parse(n);return{id:l.id,hostname:l.hostname,token:r.trim(),privateKey:i,certificate:s,chain:c,certificateExpiry:new Date(l.certificateExpiry)}}),l=E.fn(`OpenTunnelStorage.loadPending`)(function*(e){let t=a(e),n=yield*o(M.join(t,`pending.json`));if(!n)return;let[r,i]=yield*E.all([o(M.join(t,`token`)),o(M.join(t,`private-key.pem`))]);if(!r||!i)return yield*new q({message:`Incomplete pending tunnel identity for ${e}`});let s=JSON.parse(n);return{id:s.id,hostname:s.hostname,token:r.trim(),privateKey:i,csr:s.csr}});return{profiles:()=>s(),load:e=>c(e),loadPending:e=>l(e),savePending:(e,t)=>E.tryPromise({try:async()=>{let n=a(e);await j.mkdir(n,{recursive:!0,mode:448}),await Promise.all([j.writeFile(M.join(n,`pending.json`),JSON.stringify({id:t.id,hostname:t.hostname,csr:t.csr},null,2),{mode:384}),j.writeFile(M.join(n,`token`),`${t.token}\n`,{mode:384}),j.writeFile(M.join(n,`private-key.pem`),t.privateKey,{mode:384})])},catch:t=>new q({message:`Failed to save pending tunnel identity for ${e}`,cause:t})}),save:(e,t)=>E.tryPromise({try:async()=>{let n=a(e);await j.mkdir(n,{recursive:!0,mode:448}),await Promise.all([j.writeFile(M.join(n,`tunnel.json`),JSON.stringify({id:t.id,hostname:t.hostname,certificateExpiry:t.certificateExpiry.toISOString()},null,2),{mode:384}),j.writeFile(M.join(n,`token`),`${t.token}\n`,{mode:384}),j.writeFile(M.join(n,`private-key.pem`),t.privateKey,{mode:384}),j.writeFile(M.join(n,`certificate.pem`),t.certificate,{mode:384}),j.writeFile(M.join(n,`chain.pem`),t.chain,{mode:384})]),await j.rm(M.join(n,`pending.json`),{force:!0})},catch:t=>new q({message:`Failed to save tunnel identity for ${e}`,cause:t})}),remove:e=>E.tryPromise({try:()=>j.rm(a(e),{recursive:!0,force:!0}),catch:t=>new q({message:`Failed to remove tunnel identity for ${e}`,cause:t})}),list:()=>s().pipe(E.flatMap(e=>E.forEach(e,e=>c(e).pipe(E.map(t=>t?{profile:e,tunnel:t}:void 0)))),E.map(e=>e.filter(e=>e!==void 0)))}}const Et=k.Int.check(k.isBetween({minimum:0,maximum:4294967295}));var Dt=class extends k.Class(`BridgeProtocol/ClientInfo`)({version:k.String,max_conns:k.Number}){},Ot=class extends k.Class(`BridgeProtocol/AttachMessage`)({type:k.Literal(`attach`),token:k.String,transport:k.Literal(`ws`),client:Dt,routes:k.Array(k.String).pipe(k.optional)}){},kt=class extends k.Class(`BridgeProtocol/AttachedMessage`)({type:k.Literal(`attached`),session:k.String,routes:k.Array(k.String),heartbeat_ms:k.Number,idle_timeout_ms:k.Number}){},At=class extends k.Class(`BridgeProtocol/AttachErrorMessage`)({type:k.Literal(`attach_error`),code:k.String}){},jt=class extends k.Class(`BridgeProtocol/PingMessage`)({type:k.Literal(`ping`),time_sent:k.Number}){},Mt=class extends k.Class(`BridgeProtocol/PongMessage`)({type:k.Literal(`pong`),time_sent:k.Number}){},Nt=class extends k.Class(`BridgeProtocol/DrainMessage`)({type:k.Literal(`drain`),reason:k.String}){},Pt=class extends k.Class(`BridgeProtocol/OpenMessage`)({type:k.Literal(`open`),conn:Et,peer:k.String,sni:k.String,alpn:k.String}){},Ft=class extends k.Class(`BridgeProtocol/EndMessage`)({type:k.Literal(`end`),conn:Et}){},It=class extends k.Class(`BridgeProtocol/ResetMessage`)({type:k.Literal(`reset`),conn:Et,code:k.String}){};k.Union([Ot,jt,Mt,Nt,Ft,It]);const Lt=k.Union([kt,At,jt,Mt,Nt,Pt,Ft,It]),Rt=k.decodeUnknownOption(Lt);function zt(e){try{return Rt(JSON.parse(e))}catch{return he.none()}}k.Union([Ot,kt,At,jt,Mt,Nt,Pt,Ft,It]);const J={BAD_ATTACH:`bad_attach`,BAD_TOKEN:`bad_token`,TUNNEL_NOT_BOUND:`tunnel_not_bound`,CERT_NOT_READY:`cert_not_ready`,REPLACED:`replaced`,UPSTREAM_CONNECT_FAILED:`upstream_connect_failed`,UPSTREAM_IO_ERROR:`upstream_io_error`,CLIENT_IO_ERROR:`client_io_error`,IDLE_TIMEOUT:`idle_timeout`,ROUTE_CONFLICT:`route_conflict`,INVALID_ROUTE:`invalid_route`,UNKNOWN_ROUTE:`unknown_route`},Bt={HEARTBEAT_MS:15e3,IDLE_TIMEOUT_MS:45e3,CONNECT_TIMEOUT_MS:1e4,RECONNECT_BACKOFF_MIN_MS:250,RECONNECT_BACKOFF_MAX_MS:3e4},Y={CONN_ID_SIZE:4,MAX_PAYLOAD_SIZE:32768};function Vt(e){return e.length<Y.CONN_ID_SIZE?null:{conn:new DataView(e.buffer,e.byteOffset,e.byteLength).getUint32(0,!1),payload:e.slice(Y.CONN_ID_SIZE)}}function Ht(e,t){let n=new Uint8Array(Y.CONN_ID_SIZE+t.length);return new DataView(n.buffer).setUint32(0,e,!1),n.set(t,Y.CONN_ID_SIZE),n}const Ut=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,Wt=e=>e===`@`||Ut.test(e),Gt=(e,t)=>{let n=e.toLowerCase(),r=t.toLowerCase();if(n===r)return`@`;let i=`.${r}`;if(!n.endsWith(i))return;let a=n.slice(0,-i.length);return Ut.test(a)?a:void 0},Kt=e=>{let t=e.lastIndexOf(`:`);if(t===-1)return;let n=e.slice(t+1);if(!/^\d{1,5}$/.test(n))return;let r=Number(n);if(r<1||r>65535)return;let i=e.slice(0,t);if(i.startsWith(`[`)){if(!i.endsWith(`]`))return;i=i.slice(1,-1)}if(i&&/^[A-Za-z0-9.\-:_]+$/.test(i))return{host:i,port:r}};var qt=class extends Error{code;constructor(e){super(`Bridge attach failed: ${e}`),this.code=e}};const Jt=/* @__PURE__ */ new Set([J.BAD_TOKEN,J.CERT_NOT_READY]),Yt=e=>{for(let[t,n]of Object.entries(e)){if(!Wt(t))throw Error(`Invalid route name '${t}'`);if(!Kt(n))throw Error(`Invalid target '${n}' for route '${t}': use host:port`)}},Xt=e=>Ee.createSecureContext({key:e.privateKey,cert:`${e.certificate}\n${e.chain}`}),Zt=(e,t)=>{let n=Object.keys(e).sort(),r=Object.keys(t).sort();return n.length===r.length&&n.every((e,t)=>e===r[t])},Qt=e=>{let t=Math.min(Bt.RECONNECT_BACKOFF_MIN_MS*2**Math.min(e,16),Bt.RECONNECT_BACKOFF_MAX_MS);return Math.round(t/2+t/2*Math.random())};var $t=class{routes;identity;secureContext;refreshTimer;session;shutdown=!1;wake;status;ready;closed;options;constructor(e){this.options=e,Yt(e.routes),this.routes={...e.routes},this.identity=e.identity,this.secureContext=Xt(e.identity),this.status={state:`connecting`,hostname:e.identity.hostname,routes:this.routes,connections:0};let t,n;this.ready=new Promise((e,r)=>{t=e,n=r}),this.closed=this.run(t).then(()=>t(),e=>{throw n(e),e}),this.ready.catch(()=>void 0),this.refreshCertificate()}async refreshCertificate(){if(this.shutdown)return;try{let e=new URL(`/api/tunnel/${encodeURIComponent(this.identity.id)}/certificate`,this.options.api),t=await fetch(e,{headers:{authorization:`Bearer ${this.identity.token}`}}),n=(t.ok?await t.json():void 0)?.state;if(n?.type===`ready`&&n.certificate&&n.expiry&&n.certificate!==this.identity.certificate){let e={...this.identity,certificate:n.certificate,chain:n.chain??``,certificateExpiry:new Date(n.expiry)};this.secureContext=Xt(e),this.identity=e,this.options.onRenewed?.(e),this.emit({type:`certificate-renewed`,expiry:n.expiry})}}catch{}if(this.shutdown)return;let e=this.identity.certificateExpiry.getTime()-Date.now()<2592e6;this.refreshTimer=setTimeout(()=>void this.refreshCertificate(),e?6e4:432e5),this.refreshTimer.unref?.()}getStatus(){return{...this.status,routes:{...this.status.routes}}}setRoutes(e){Yt(e);let t=this.routes;this.routes={...e},this.status.routes=this.routes,Zt(t,this.routes)||this.session?.end({type:`routes-changed`}),this.wake?.()}close(){return this.shutdown=!0,clearTimeout(this.refreshTimer),this.session?.end({type:`shutdown`}),this.wake?.(),this.closed.catch(()=>void 0)}emit(e){this.options.onEvent(e)}sleep(e){return new Promise(t=>{let n=setTimeout(r,e);function r(){clearTimeout(n),t()}this.wake=r}).finally(()=>{this.wake=void 0})}async run(e){let t=0;try{for(;!this.shutdown;){if(Object.keys(this.routes).length===0){this.status.state=`waiting-routes`,await this.sleep(2**31-1);continue}this.status.state=`connecting`,this.emit({type:`connecting`,attempt:t});let n=new en(this.options,this.routes,()=>this.secureContext,{emit:e=>this.emit(e),target:e=>this.routes[e],attached:t=>{this.status.state=`connected`,this.status.session=t,this.status.lastError=void 0,this.status.connectedAt=Date.now(),e()},connections:e=>{this.status.connections=e}});this.session=n;let r=await n.run();if(this.session=void 0,this.status.session=void 0,this.status.connections=0,r.type===`shutdown`)break;if(r.type===`fatal`)throw r.error;if(r.type===`routes-changed`){this.emit({type:`disconnected`,reason:`routes changed`}),t=0;continue}r.attached&&(t=0),this.emit({type:`disconnected`,reason:r.error});let i=Qt(t);t++,this.status.state=`reconnecting`,this.status.lastError=r.error,this.emit({type:`reconnecting`,attempt:t,delayMs:i}),await this.sleep(i)}this.status.state=`stopped`,this.emit({type:`stopped`})}catch(e){throw this.status.state=`stopped`,this.status.lastError=e instanceof Error?e.message:String(e),this.emit({type:`stopped`,error:this.status.lastError}),e}}},en=class{socket;finish;channels=/* @__PURE__ */ new Map;drainWaiters=[];drainTimer;heartbeat;attachTimeout;lastReceived=Date.now();attached=!1;options;routes;secureContext;hooks;constructor(e,t,n,r){this.options=e,this.routes=t,this.secureContext=n,this.hooks=r}run(){return new Promise(e=>{this.finish=e;let t=new URL(`/api/tunnel/${encodeURIComponent(this.options.identity.id)}/connect`,this.options.api);t.protocol=t.protocol===`https:`?`wss:`:`ws:`;let n=new WebSocket(t,[`opentunnel`]);n.binaryType=`arraybuffer`,this.socket=n,this.attachTimeout=setTimeout(()=>this.end({type:`retry`,error:`bridge attach timed out`,attached:!1}),Bt.CONNECT_TIMEOUT_MS),n.addEventListener(`open`,()=>{this.sendControl({type:`attach`,token:this.options.identity.token,transport:`ws`,routes:Object.keys(this.routes).sort(),client:{version:`0.1.0`,max_conns:256}})}),n.addEventListener(`message`,e=>{this.lastReceived=Date.now(),typeof e.data==`string`?this.onControl(e.data):this.onData(new Uint8Array(e.data))}),n.addEventListener(`close`,e=>{let t=`${e.code} ${e.reason}`.trim();this.end({type:`retry`,error:`bridge closed: ${t}`,attached:this.attached})}),n.addEventListener(`error`,()=>{this.end({type:`retry`,error:`bridge WebSocket failed`,attached:this.attached})})})}end(e){let t=this.finish;if(t){this.finish=void 0,clearTimeout(this.attachTimeout),clearInterval(this.heartbeat),clearInterval(this.drainTimer);for(let e of this.drainWaiters.splice(0))e();for(let e of[...this.channels.values()])e.destroy(`bridge disconnected`);this.socket&&this.socket.readyState<=WebSocket.OPEN&&this.socket.close(1e3),t(e)}}onControl(e){let t=zt(e);if(he.isNone(t))return;let n=t.value;if(!this.attached){if(n.type===`attached`){this.attached=!0,clearTimeout(this.attachTimeout);let e=Math.max(n.heartbeat_ms,1e3),t=Math.max(n.idle_timeout_ms,5e3);this.heartbeat=setInterval(()=>{if(Date.now()-this.lastReceived>t){this.end({type:`retry`,error:`bridge idle timeout`,attached:!0});return}this.sendControl({type:`ping`,time_sent:Date.now()})},e),this.hooks.attached(n.session),this.hooks.emit({type:`connected`,session:n.session,routes:[...n.routes]})}else n.type===`attach_error`&&this.end(Jt.has(n.code)?{type:`fatal`,error:new qt(n.code)}:{type:`retry`,error:`attach failed: ${n.code}`,attached:!1});return}switch(n.type){case`open`:return this.open(n.conn,n.sni,n.peer);case`end`:return this.channels.get(n.conn)?.endInbound();case`reset`:return this.channels.get(n.conn)?.destroy(n.code,!1);case`ping`:return this.sendControl({type:`pong`,time_sent:n.time_sent});case`drain`:return this.end({type:`retry`,error:`server draining: ${n.reason}`,attached:!0})}}onData(e){let t=Vt(e);t&&this.channels.get(t.conn)?.push(t.payload)}open(e,t,n){let r=Gt(t,this.options.identity.hostname),i=r===void 0?void 0:this.hooks.target(r),a=i===void 0?void 0:Kt(i);if(r===void 0||!a){this.sendControl({type:`reset`,conn:e,code:J.UNKNOWN_ROUTE});return}let o=new tn(e,r,a,this.secureContext(),this);this.channels.set(e,o),this.hooks.connections(this.channels.size),this.hooks.emit({type:`connection-opened`,conn:e,route:r,peer:n})}closed(e,t){this.channels.get(e.conn)===e&&(this.channels.delete(e.conn),this.hooks.connections(this.channels.size),this.hooks.emit({type:`connection-closed`,conn:e.conn,route:e.route,...t===void 0?{}:{error:t}}))}sendControl(e){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify(e))}sendData(e,t,n){let r=this.socket;if(!this.finish||r?.readyState!==WebSocket.OPEN){n(/* @__PURE__ */ Error(`bridge closed`));return}for(let n=0;n<t.byteLength;n+=Y.MAX_PAYLOAD_SIZE){let i=t.subarray(n,n+Y.MAX_PAYLOAD_SIZE);r.send(Ht(e,i))}if(r.bufferedAmount<1048576){n();return}this.drainWaiters.push(()=>n()),this.drainTimer??=setInterval(()=>{if(!(this.socket&&this.socket.bufferedAmount>262144&&this.finish)){clearInterval(this.drainTimer),this.drainTimer=void 0;for(let e of this.drainWaiters.splice(0))e()}},5)}},tn=class{conn;route;session;raw;tls;upstream;done=!1;constructor(e,t,n,r,i){this.conn=e,this.route=t,this.session=i,this.raw=new Te({allowHalfOpen:!0,read(){},write:(t,n,r)=>i.sendData(e,t,r),final:t=>{i.sendControl({type:`end`,conn:e}),t()}}),this.tls=new Ee.TLSSocket(this.raw,{isServer:!0,secureContext:r}),this.tls.on(`secure`,()=>{let e=we.connect({host:n.host,port:n.port,allowHalfOpen:!0});this.upstream=e,e.setNoDelay(!0),e.on(`error`,t=>this.destroy(e.connecting?J.UPSTREAM_CONNECT_FAILED:J.UPSTREAM_IO_ERROR,!0,t.message)),this.tls.pipe(e),e.pipe(this.tls)}),this.tls.on(`error`,e=>this.destroy(J.UPSTREAM_IO_ERROR,!0,e.message)),this.tls.on(`close`,()=>this.finish(void 0))}push(e){if(!this.done){if(this.raw.readableLength>8388608){this.destroy(J.UPSTREAM_IO_ERROR,!0,`local connection stalled`);return}this.raw.push(e)}}endInbound(){this.done||this.raw.push(null)}destroy(e,t=!0,n=e){this.done||(t&&this.session.sendControl({type:`reset`,conn:this.conn,code:e}),this.finish(n))}finish(e){this.done||(this.done=!0,this.upstream?.destroy(),this.tls.destroy(),this.raw.destroy(),this.session.closed(this,e))}};const X=e=>e?.profile??`default`,Z=(e,t)=>new wt({message:e,cause:t}),nn=e=>{let t=new Uint8Array(e),n=``;for(let e of t)n+=String.fromCharCode(e);let r=btoa(n);return`-----BEGIN PRIVATE KEY-----\n${r.match(/.{1,64}/g)?.join(`
5
+ `)??r}\n-----END PRIVATE KEY-----\n`};var rn=class e extends me.Service()(`@opentunnel/client/OpenTunnelClient`){static layer(t={}){let n=t.storage??Tt();return D.effect(e,E.gen(function*(){let e=yield*_t,r=new URL(t.api??`https://opentunnel.xyz`),i=E.fn(`OpenTunnelClient.tunnel.get`)(function*(e){return yield*n.load(X(e))}),a=E.fn(`OpenTunnelClient.tunnel.completePending`)(function*(t){let r=yield*e.authorized(B.make(t.pending.token));yield*E.sync(()=>t.onProgress?.(`requesting-certificate`)),yield*r.tunnel[`tunnel.bindCertificate`]({params:{id:z.make(t.pending.id)},payload:{csr:t.pending.csr}}).pipe(E.catchTag(`CertificateInProgressError`,()=>E.void),E.mapError(e=>Z(`Failed to start certificate issuance`,e)));let i=yield*E.gen(function*(){for(;;){yield*E.sync(()=>t.onProgress?.(`waiting-certificate`));let e=yield*r.tunnel[`tunnel.getCertificate`]({params:{id:z.make(t.pending.id)}}).pipe(E.mapError(e=>Z(`Failed to read certificate`,e)));if(e.state.type===`ready`)return e.state;if(e.state.type===`failed`)return yield*new wt({message:`Certificate issuance failed: ${e.state.reason}`});yield*E.sleep(`2 seconds`)}}),a={id:t.pending.id,hostname:t.pending.hostname,token:t.pending.token,privateKey:t.pending.privateKey,certificate:i.certificate,chain:i.chain,certificateExpiry:new Date(i.expiry)};return yield*E.sync(()=>t.onProgress?.(`saving-identity`)),yield*n.save(t.profile,a),yield*E.sync(()=>t.onProgress?.(`ready`)),a}),o=E.fn(`OpenTunnelClient.tunnel.provision`)(function*(e){yield*E.sync(()=>e.onProgress?.(`generating-key`));let t=yield*E.tryPromise({try:()=>crypto.subtle.generateKey({name:`ECDSA`,namedCurve:`P-256`},!0,[`sign`,`verify`]),catch:e=>Z(`Failed to generate certificate key`,e)});yield*E.sync(()=>e.onProgress?.(`generating-csr`));let r=yield*E.tryPromise({try:()=>Ct(e.hostname,t),catch:e=>Z(`Failed to generate certificate request`,e)}),i=yield*E.tryPromise({try:()=>crypto.subtle.exportKey(`pkcs8`,t.privateKey),catch:e=>Z(`Failed to export certificate key`,e)}),o={id:String(e.id),hostname:e.hostname,token:e.token,privateKey:nn(i),csr:r};return yield*n.savePending(e.profile,o),yield*a({profile:e.profile,pending:o,onProgress:e.onProgress})}),s=E.fn(`OpenTunnelClient.tunnel.create`)(function*(t){let r=X(t);if(yield*n.load(r))return yield*new wt({message:`Profile '${r}' already has a tunnel`});let i=yield*n.loadPending(r);if(i)return yield*E.sync(()=>t?.onProgress?.(`resuming-certificate`)),yield*a({profile:r,pending:i,onProgress:t?.onProgress});yield*E.sync(()=>t?.onProgress?.(`creating-tunnel`));let s=yield*e.client.tunnel[`tunnel.create`]({payload:{}}).pipe(E.mapError(e=>Z(`Failed to create tunnel`,e)));return yield*o({profile:r,id:s.tunnel.id,hostname:String(s.tunnel.hostname),token:s.token,onProgress:t?.onProgress})}),c=E.fn(`OpenTunnelClient.tunnel.ensure`)(function*(t){let r=yield*i(t);if(!r)return yield*s(t);let a=(yield*(yield*e.authorized(B.make(r.token))).tunnel[`tunnel.getCertificate`]({params:{id:z.make(r.id)}}).pipe(E.mapError(e=>Z(`Failed to read certificate`,e)))).state;if(a.type===`ready`&&a.certificate!==r.certificate){let e={...r,certificate:a.certificate,chain:a.chain,certificateExpiry:new Date(a.expiry)};return yield*n.save(X(t),e),e}return a.type===`failed`?yield*o({profile:X(t),id:z.make(r.id),hostname:r.hostname,token:B.make(r.token)}):r}),l=E.fn(`OpenTunnelClient.tunnel.pending`)(function*(e){let t=yield*n.loadPending(X(e));return t?{id:t.id,hostname:t.hostname}:void 0}),u=E.fn(`OpenTunnelClient.tunnel.resume`)(function*(e){let t=X(e),r=yield*n.loadPending(t);if(r)return yield*E.sync(()=>e?.onProgress?.(`resuming-certificate`)),yield*a({profile:t,pending:r,onProgress:e?.onProgress})});return{profile:{list:n.profiles},tunnel:{list:n.list,get:i,pending:l,resume:u,create:s,ensure:c,remove:E.fn(`OpenTunnelClient.tunnel.remove`)(function*(t){let r=X(t),i=yield*n.load(r);i&&(yield*(yield*e.authorized(B.make(i.token))).tunnel[`tunnel.remove`]({params:{id:z.make(i.id)}}).pipe(E.mapError(e=>Z(`Failed to remove tunnel`,e))),yield*n.remove(r))}),connect:E.fn(`OpenTunnelClient.tunnel.connect`)(function*(e){yield*E.try({try:()=>Yt(e.routes),catch:e=>Z(e instanceof Error?e.message:`Invalid routes`,e)});let t=yield*c(e),i=yield*O.unbounded(),a=yield*E.acquireRelease(E.sync(()=>new $t({api:r,identity:t,routes:e.routes,onEvent:e=>{O.offerUnsafe(i,e),e.type===`stopped`&&O.endUnsafe(i)},onRenewed:t=>{E.runFork(n.save(X(e),t).pipe(E.ignore))}})),e=>E.promise(()=>e.close()).pipe(E.andThen(O.end(i))));return yield*E.tryPromise({try:()=>a.ready,catch:e=>Z(`Failed to connect tunnel`,e)}),{tunnel:t,events:ge.fromQueue(i),status:E.sync(()=>a.getStatus()),setRoutes:e=>E.try({try:()=>a.setRoutes(e),catch:e=>Z(e instanceof Error?e.message:`Invalid routes`,e)}),closed:E.tryPromise({try:()=>a.closed,catch:e=>Z(`Tunnel stopped`,e)}),close:E.promise(()=>a.close())}})}}})).pipe(D.provide(_t.layer({api:t.api??`https://opentunnel.xyz`})))}};const an=r.Union([r.Struct({jsonrpc:r.Literal(`2.0`),id:P.RequestId,result:r.Unknown}),r.Struct({jsonrpc:r.Literal(`2.0`),id:r.NullOr(P.RequestId),error:P.McpErrorBase}),r.Struct({jsonrpc:r.Literal(`2.0`),method:r.String,params:r.optionalKey(r.Unknown)})]),on=r.fromJsonString(an),sn=N.v2025_06_18.protocolVersion;var Q=class extends r.TaggedError()(`OrbitMcpError`,{message:r.String}){},cn=class extends l.Service()(`@2digits/tlo-mcp/mcp/OrbitClient`){};const ln=p.effect(cn,n.gen(function*(){let e=yield*R,t=(yield*_.HttpClient).pipe(_.withScope,_.transformResponse(n.flatMap(te.matchStatus({"2xx":e=>n.succeed(e),401:e=>Q.make({message:`Orbit HTTP ${e.status}: authorization rejected. Run tlo-mcp login; a 403 can also mean insufficient rights.`}),403:e=>Q.make({message:`Orbit HTTP ${e.status}: authorization rejected. Run tlo-mcp login; a 403 can also mean insufficient rights.`}),429:e=>Q.make({message:`Orbit rate limited. Retry after ${e.headers[`retry-after`]??`the server cooldown`}; request not replayed.`}),orElse:e=>Q.make({message:`Orbit HTTP ${e.status}. Request not replayed.`})})))),i=0,a,o=n.fn(`OrbitClient.send`)(function*(o,s,c=!1){let l=++i,u=yield*e.accessToken,d=yield*v.post(F).pipe(v.bearerToken(u),v.setHeader(`accept`,`application/json, text/event-stream`),v.setHeader(`mcp-protocol-version`,sn),e=>a===void 0?e:v.setHeader(e,`mcp-session-id`,a),v.bodyJson({jsonrpc:`2.0`,...c?{}:{id:l},method:o,params:s})),f=yield*t.execute(d),p=f.headers[`mcp-session-id`];if(o===`initialize`&&(a=p),c)return;let m=f.headers[`content-type`]?.startsWith(`text/event-stream`)?yield*f.stream.pipe(T.decodeText(),T.pipeThroughChannel(je.decode({maxEventSize:10485760})),T.filter(e=>e.event===`message`||e.event===``),T.map(Ae.get(`data`)),T.mapEffect(e=>r.decodeEffect(on)(e)),T.filter(e=>`id`in e&&e.id===l),T.runHead,n.flatMap(e=>ke.isSome(e)?n.succeed(e.value):Q.make({message:`Orbit stream ended before replying.`}))):yield*te.schemaBodyJson(an)(f);return!(`id`in m)||m.id!==l?yield*Q.make({message:`Unexpected Orbit response ID.`}):`error`in m?yield*Q.make({message:m.error.message}):m.result},n.timeout(`120 seconds`),n.scoped,n.provideService(g.RequestInit,{redirect:`error`}),n.mapError(e=>r.is(Q)(e)?e:Q.make({message:`Orbit connection or response failed. Request not replayed.`}))),s=yield*o(`initialize`,{protocolVersion:sn,capabilities:{},clientInfo:{name:`@2digits/tlo-mcp`,version:`0.1.38`}}).pipe(n.flatMap(r.decodeUnknownEffect(P.InitializeResult)));if(s.protocolVersion!==sn)return yield*Q.make({message:`Unsupported upstream MCP protocol: ${s.protocolVersion}.`});yield*o(`notifications/initialized`,{},!0);let c=/* @__PURE__ */ new Set,l=yield*T.paginate(void 0,n.fn(`OrbitClient.listToolsPage`)(function*(e){let t=yield*o(`tools/list`,e===void 0?{}:{cursor:e}).pipe(n.flatMap(r.decodeUnknownEffect(P.ListToolsResult)));if(t.nextCursor!==void 0){if(c.has(t.nextCursor))return yield*Q.make({message:`Orbit returned a repeated tools cursor.`});c.add(t.nextCursor)}return[t.tools,ke.fromUndefinedOr(t.nextCursor)]})).pipe(T.runCollect);return cn.of({tools:l,instructions:s.instructions,callTool:n.fn(`OrbitClient.callTool`)(function*(e,t){return yield*o(`tools/call`,{name:e,arguments:t})},n.flatMap(r.decodeUnknownEffect(P.CallToolResult)),n.catchTag(`SchemaError`,()=>Q.make({message:`Invalid Orbit tool result. Request not replayed.`})))})})).pipe(p.provide(g.layer)),un=p.effectDiscard(n.gen(function*(){let e=yield*cn,t=yield*Oe.McpServer;yield*n.forEach(e.tools,n.fn(`OrbitProxyLive.tool`)(function*(i){yield*t.addTool({tool:i,annotations:l.empty(),handle:n.fn(`handle`)(function*(e){return yield*r.decodeUnknownEffect(r.JsonObject)(e)},n.mapError(e=>P.InvalidParams.make({message:`Tool arguments must be a JSON object.`,data:e})),n.flatMap(t=>e.callTool(i.name,t).pipe(n.mapError(e=>P.InternalError.make({message:e.message})))))})}),{discard:!0})})),dn=`${de()}/.config/2digits/tlo-mcp/gateway.key`,fn=r.String.check(r.isPattern(/^[a-f0-9]{64}$/)).pipe(r.RedactedFromValue);var $=class extends r.TaggedError()(`GatewayError`,{message:r.String}){};const pn=n.fn(`Gateway.loadKey`)(function*(e){let t=yield*fe.FileSystem,i=(yield*pe.Path).dirname(e);yield*t.makeDirectory(i,{recursive:!0,mode:448}),yield*t.chmod(i,448),yield*t.writeFileString(e,ae(32).toString(`hex`),{flag:`wx`,mode:384}).pipe(n.catchReason(`PlatformError`,`AlreadyExists`,()=>n.void));let a=yield*t.stat(e);return a.type!==`File`||a.mode&63?yield*$.make({message:`Gateway key must be a private file (chmod 600).`}):yield*t.readFileString(e).pipe(n.flatMap(r.decodeEffect(fn)))},n.mapError(()=>$.make({message:`Cannot load private gateway key. Check its permissions and format.`})));function mn(e){let t=ie(`sha256`).update(`Bearer ${h.value(e)}`).digest();return y.middleware(e=>n.gen(function*(){let r=yield*b.HttpServerRequest,i=ie(`sha256`).update(r.headers.authorization??``).digest();return oe(t,i)?r.headers.origin===void 0?yield*e.pipe(n.provideService(ne.MaxBodySize,De.bytes(1048576)),n.map(e=>x.setHeader(e,`cache-control`,`no-store`))):x.empty({status:403,headers:{"cache-control":`no-store`}}):x.empty({status:401,headers:{"www-authenticate":`Bearer`,"cache-control":`no-store`}})}),{global:!0})}function hn(e){let t=p.unwrap(n.gen(function*(){let e=yield*cn;return un.pipe(p.provide(Oe.layerHttp({name:`tlo-mcp`,version:`1.0.0`,instructions:e.instructions,path:`/mcp`,protocols:[N.v2025_11_25,N.v2025_06_18,N.v2025_03_26]})))}));return p.merge(t,mn(e))}const gn=r.String.check(r.isPattern(/^[a-z0-9]+\.opentunnel\.xyz$/)),_n=n.fn(`Gateway.openTunnel`)(function*(e){let t=yield*(yield*rn).tunnel.connect({profile:`tlo-mcp`,routes:{tlo:`127.0.0.1:${e}`}}),i=yield*r.decodeEffect(gn)(t.tunnel.hostname);return yield*t.events.pipe(T.runForEach(e=>e.type===`reconnecting`?n.sync(()=>process.stderr.write(`OpenTunnel reconnecting...
6
+ `)):n.void),n.forkScoped),{endpoint:`https://tlo.${i}/mcp`,closed:t.closed.pipe(n.mapError(()=>$.make({message:`OpenTunnel stopped unexpectedly. Restart tlo-mcp.`})))}},n.timeout(`180 seconds`),n.mapError(()=>$.make({message:`Could not connect OpenTunnel. Check network access and retry; existing tunnel identity is preserved.`})));function vn(e){return`Connect my personal Teamleader Orbit MCP to Executor Cloud.
7
+ Endpoint: ${e}
8
+ Transport: Streamable HTTP. Authentication: API key in Authorization: Bearer <key>.
9
+ Register an MCP integration named Teamleader Orbit (slug tlo_mcp) with a bearer-header API-key template, not OAuth.
10
+ Use a secure credential handoff so I enter the gateway key directly in Executor; never ask me to paste it into chat.
11
+ Create a personal connection, discover tools, and verify with one read-only tool. Do not change business data.
12
+ If this endpoint already exists, reuse its integration/connection instead of creating duplicates.
13
+ Orbit login and refresh are handled on my Mac; do not register another Orbit OAuth client.
14
+ `}const yn=n.fn(`TloMcp.connect`)(function*(e){let t=yield*pn(dn),r=yield*L,i=yield*R;(yield*r.load)===void 0&&(yield*i.login),yield*p.build(y.serve(hn(t),{disableLogger:!0,disableListenLog:!0}).pipe(p.provide(ln),p.provide(ce.layer(()=>se({maxHeaderSize:8192,requestTimeout:13e4}),{host:`127.0.0.1`,port:e})))),yield*n.sync(()=>process.stderr.write(`Connecting OpenTunnel (first certificate may take a few minutes)...
15
+ `));let{endpoint:a,closed:o}=yield*_n(e);return yield*n.sync(()=>{process.stderr.write(`TLO gateway ready: ${a}\nGateway key: ${dn} (private; not printed)\n`),process.platform===`darwin`&&process.stderr.write(`Copy the key for Executor's secure credential form: pbcopy < "${dn}"\n`),process.stderr.write(`Keep this process running and your Mac awake. Ctrl-C closes the gateway and tunnel.
16
+
17
+ Copy this prompt to your agent:
18
+
19
+ `),process.stdout.write(vn(a))}),yield*o,yield*$.make({message:`The OpenTunnel connection stopped. Restart tlo-mcp.`})},n.scoped,n.provide(p.mergeAll(Ke,We,rn.layer()))),bn=i.make(`login`,{},n.fn(`TloMcp.login`)(function*(){yield*(yield*R).login},n.provide(Ke),n.scoped)),xn=i.make(`logout`,{},n.fn(`TloMcp.logout`)(function*(){yield*(yield*R).logout},n.provide(Ke),n.scoped)),Sn=i.make(`tlo-mcp`,{port:a.Int(`port`).pipe(a.withSchema(r.Int.check(r.isBetween({minimum:1024,maximum:65535}))),a.withDefault(4790))},({port:e})=>yn(e)).pipe(i.withSubcommands([bn,xn]));n.fn(`TloMcp.run`)(function*(e){return yield*i.runWith(Sn,{version:`1.0.0`})(e)})(process.argv.slice(2)).pipe(n.provide(t.layer),e.runMain);export{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2digits/tlo-mcp",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/2digits-agency/configs.git",
@@ -14,35 +14,32 @@
14
14
  ],
15
15
  "type": "module",
16
16
  "sideEffects": false,
17
- "types": "./dist/index.d.mts",
18
- "exports": {
19
- ".": "./dist/index.mjs",
20
- "./bin": "./dist/bin.mjs",
21
- "./package.json": "./package.json"
22
- },
23
17
  "publishConfig": {
24
18
  "access": "public"
25
19
  },
26
20
  "dependencies": {
27
- "@effect/platform-node": "4.0.1",
28
- "@yielded/oauth": "0.1.0-beta.28",
29
- "effect": "4.0.1"
21
+ "@effect/platform-node": "4.0.2",
22
+ "@opentunnel/client": "0.2.2",
23
+ "@yielded/oauth": "0.1.0-beta.30",
24
+ "effect": "4.0.2"
30
25
  },
31
26
  "devDependencies": {
32
27
  "@2digits/tsconfig": "0.9.2",
33
28
  "@arethetypeswrong/core": "0.18.5",
34
- "@effect/vitest": "4.0.1",
35
- "@oxc-node/cli": "0.1.4",
29
+ "@effect/vitest": "4.0.2",
36
30
  "publint": "0.3.25",
37
31
  "typescript": "7.0.2",
38
32
  "vite-plus": "1.1.0",
39
33
  "vitest": "5.0.3"
40
34
  },
35
+ "engines": {
36
+ "node": ">=24"
37
+ },
41
38
  "public": true,
42
39
  "scripts": {
43
40
  "build": "vp pack --minify --config-loader unconfig",
44
41
  "dev": "vp pack --watch --config-loader unconfig",
45
- "run": "oxnode ./src/bin.ts",
42
+ "run": "vp run build && node ./dist/bin.mjs",
46
43
  "test": "vitest run",
47
44
  "test:watch": "vitest",
48
45
  "types": "tsc --noEmit"
package/dist/bin.d.mts DELETED
@@ -1 +0,0 @@
1
- export {}
package/dist/index.d.mts DELETED
@@ -1,80 +0,0 @@
1
- import { OAuth } from "@yielded/oauth";
2
- import * as Context from "effect/Context";
3
- import * as Crypto from "effect/Crypto";
4
- import * as Effect from "effect/Effect";
5
- import * as Layer from "effect/Layer";
6
- import * as Redacted from "effect/Redacted";
7
- import * as Schema from "effect/Schema";
8
- import { HttpClient } from "effect/http";
9
- import { ChildProcessSpawner } from "effect/process";
10
- import * as FileSystem from "effect/FileSystem";
11
- import * as Path from "effect/Path";
12
- import * as McpSchema from "effect/ai/McpSchema";
13
- import * as Scope from "effect/Scope";
14
- //#region src/oauth/session.d.ts
15
- export declare const Session: Schema.Struct<{
16
- readonly clientId: Schema.String;
17
- readonly clientSecret: Schema.optionalKey<Schema.RedactedFromValue<Schema.String>>;
18
- readonly accessToken: Schema.RedactedFromValue<Schema.String>;
19
- readonly refreshToken: Schema.optionalKey<Schema.RedactedFromValue<Schema.String>>;
20
- readonly expiresAt: Schema.optionalKey<Schema.Finite>;
21
- }>;
22
- export interface Session extends Schema.Schema.Type<typeof Session> {}
23
- declare const OrbitAuthError_base: Schema.Class<OrbitAuthError, Schema.TaggedStruct<"OrbitAuthError", {
24
- readonly message: Schema.String;
25
- }>, import("effect/Cause").YieldableError>;
26
- export declare class OrbitAuthError extends OrbitAuthError_base {}
27
- //#endregion
28
- //#region src/oauth/store.d.ts
29
- interface SessionStoreShape {
30
- readonly load: Effect.Effect<Session | undefined, OrbitAuthError>;
31
- readonly save: (session: Session) => Effect.Effect<void, OrbitAuthError>;
32
- readonly clear: Effect.Effect<void, OrbitAuthError>;
33
- readonly lock: Effect.Effect<void, OrbitAuthError, Scope.Scope>;
34
- }
35
- declare const SessionStore_base: Context.ServiceClass<SessionStore, "@2digits/tlo-mcp/oauth/store/SessionStore", SessionStoreShape>;
36
- export declare class SessionStore extends SessionStore_base {}
37
- /**
38
- * Keychain on macOS; private atomic files elsewhere. Credentials never enter argv.
39
- *
40
- * @param options
41
- * @param options.directory
42
- * @param options.platform
43
- */
44
- export declare function makeSessionStoreLayer(options?: {
45
- readonly directory?: string;
46
- readonly platform?: string;
47
- }): Layer.Layer<SessionStore, never, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
48
- export declare const SessionStoreLive: Layer.Layer<SessionStore, never, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
49
- //#endregion
50
- //#region src/oauth/OrbitAuth.d.ts
51
- interface OrbitAuthShape {
52
- readonly login: Effect.Effect<void, OrbitAuthError>;
53
- readonly logout: Effect.Effect<void, OrbitAuthError>;
54
- readonly accessToken: Effect.Effect<Redacted.Redacted, OrbitAuthError>;
55
- }
56
- declare const OrbitAuth_base: Context.ServiceClass<OrbitAuth, "@2digits/tlo-mcp/oauth/OrbitAuth", OrbitAuthShape>;
57
- export declare class OrbitAuth extends OrbitAuth_base {}
58
- export declare const OrbitAuthLayer: Layer.Layer<OrbitAuth, never, ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | HttpClient.HttpClient | SessionStore>;
59
- export declare const OrbitAuthLive: Layer.Layer<OrbitAuth, never, ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
60
- //#endregion
61
- //#region src/mcp/OrbitClient.d.ts
62
- declare const OrbitMcpError_base: Schema.Class<OrbitMcpError, Schema.TaggedStruct<"OrbitMcpError", {
63
- readonly message: Schema.String;
64
- }>, import("effect/Cause").YieldableError>;
65
- export declare class OrbitMcpError extends OrbitMcpError_base {}
66
- interface OrbitClientShape {
67
- readonly tools: ReadonlyArray<McpSchema.Tool>;
68
- readonly instructions: string | undefined;
69
- readonly callTool: (name: string, arguments_: Schema.JsonObject) => Effect.Effect<McpSchema.CallToolResult, OrbitMcpError>;
70
- }
71
- declare const OrbitClient_base: Context.ServiceClass<OrbitClient, "@2digits/tlo-mcp/mcp/OrbitClient", OrbitClientShape>;
72
- export declare class OrbitClient extends OrbitClient_base {}
73
- /**
74
- * Streamable HTTP request/response transport; no retry of possibly committed tool calls.
75
- */
76
- export declare const OrbitClientLive: Layer.Layer<OrbitClient, OrbitMcpError | Schema.SchemaError, OrbitAuth>;
77
- //#endregion
78
- //#region src/mcp/proxy.d.ts
79
- export declare const OrbitProxyLive: Layer.Layer<never, import("effect/Cause").IllegalArgumentError, OrbitClient | import("effect/Stdio").Stdio>;
80
- //#endregion
package/dist/index.mjs DELETED
@@ -1 +0,0 @@
1
- import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./proxy-BaBpBFTW.mjs";export{e as OrbitAuth,n as OrbitAuthError,s as OrbitAuthLayer,l as OrbitAuthLive,o as OrbitClient,c as OrbitClientLive,i as OrbitMcpError,u as OrbitProxyLive,r as Session,t as SessionStore,a as SessionStoreLive,d as makeSessionStoreLayer};
@@ -1,2 +0,0 @@
1
- import{OAuth as e,Pkce as t}from"@yielded/oauth";import*as n from"effect/Clock";import*as r from"effect/Context";import*as i from"effect/Crypto";import*as a from"effect/Effect";import*as o from"effect/Equal";import*as s from"effect/Exit";import*as c from"effect/Layer";import*as l from"effect/Match";import*as u from"effect/Redacted";import*as d from"effect/Schema";import*as f from"effect/Semaphore";import{FetchHttpClient as p,HttpClient as m,HttpClientRequest as h,HttpClientResponse as g,HttpRouter as _,HttpServer as v,HttpServerRequest as y,HttpServerResponse as b}from"effect/http";import{ChildProcess as x,ChildProcessSpawner as S}from"effect/process";import{timingSafeEqual as C}from"node:crypto";import{createServer as w}from"node:http";import*as T from"@effect/platform-node/NodeHttpServer";import*as E from"effect/Deferred";import*as ee from"effect/Predicate";import{homedir as te}from"node:os";import*as ne from"effect/FileSystem";import*as re from"effect/Path";import*as D from"effect/Stream";import*as O from"effect/Option";import*as k from"effect/Struct";import*as A from"effect/ai/McpProtocol";import*as j from"effect/ai/McpSchema";import*as ie from"effect/encoding/Sse";import*as M from"effect/ai/McpServer";import*as N from"effect/Logger";const P=`https://api.orbit.teamleader.eu`,F=`${P}/mcp`,I=d.RedactedFromValue(d.String.check(d.isMinLength(1))),L=d.Struct({clientId:d.String.check(d.isMinLength(1)),clientSecret:d.optionalKey(I),accessToken:I,refreshToken:d.optionalKey(I),expiresAt:d.optionalKey(d.Finite)});var R=class extends d.TaggedError()(`OrbitAuthError`,{message:d.String}){};function z(e,t,n){let r=t.refreshToken??e.refreshToken;return{clientId:e.clientId,...e.clientSecret===void 0?{}:{clientSecret:e.clientSecret},accessToken:t.accessToken,...r===void 0?{}:{refreshToken:r},...t.expiresIn===void 0?{}:{expiresAt:n+t.expiresIn*1e3}}}function B(e,t){return e.expiresAt===void 0||e.expiresAt<=t+6e4}function V(e){return e.clientSecret===void 0?{method:`none`,publicClient:!0}:{method:`client_secret_post`,secret:e.clientSecret.pipe(u.value,u.make)}}function ae(e,t){let n=e.searchParams.get(`state`);if(n===null||e.pathname!==`/callback`||e.searchParams.getAll(`state`).length!==1)return;let r=Buffer.from(t),i=Buffer.from(n);if(r.length!==i.length||!C(r,i))return;if(e.searchParams.has(`error`))throw Error(`Orbit authorization was denied.`);let a=e.searchParams.get(`code`);if(a!==null&&a.length!==0&&e.searchParams.getAll(`code`).length===1)return a}const oe=a.fn(`OrbitAuth.listenForLogin`)(function*(e){let t=yield*E.make(),n=yield*a.sync(w),r=yield*T.make(()=>n,{port:0,host:`127.0.0.1`}).pipe(a.mapError(()=>R.make({message:`Could not bind OAuth callback.`})));if(!ee.isTagged(r.address,`InetAddressV4`))return yield*R.make({message:`Could not bind OAuth callback.`});let i=`127.0.0.1:${r.address.port}`,o=`http://${i}/callback`,s={"content-type":`text/plain; charset=utf-8`,"cache-control":`no-store`,"referrer-policy":`no-referrer`,"content-security-policy":`default-src 'none'`},c=b.text(`Invalid callback.`,{status:400,headers:s}),l=b.text(`Authorization received. Return to your terminal to complete login.`,{headers:s}),u=b.text(`Authorization denied. Return to your terminal.`,{status:400,headers:s}),d=a.fn(`OrbitAuth.handleCallback`)(function*(){let n=yield*y.HttpServerRequest;return n.method!==`GET`||n.headers.host!==i||(yield*E.isDone(t))?c:yield*a.try({try:()=>ae(new URL(n.url,o),e),catch:()=>R.make({message:`Orbit authorization was denied.`})}).pipe(a.matchEffect({onFailure:e=>E.fail(t,e).pipe(a.map(e=>e?u:c)),onSuccess:e=>e===void 0?a.succeed(c):E.succeed(t,e).pipe(a.map(e=>e?l:c))}))}),f=yield*_.toHttpEffect(_.add(`*`,`*`,d()));return yield*v.serveEffect(f).pipe(a.provideService(v.HttpServer,r)),yield*a.addFinalizer(()=>a.sync(()=>n.closeAllConnections())),{redirectUri:o,code:E.await(t).pipe(a.timeout(`3 minutes`),a.mapError(()=>R.make({message:`Login denied or timed out. Run tlo-mcp login again.`})))}}),H=d.fromJsonString(L),U=`@2digits/tlo-mcp/orbit`;var W=class extends r.Service()(`@2digits/tlo-mcp/oauth/store/SessionStore`){};function G(e){return`"${e.replaceAll(`\\`,String.raw`\\`).replaceAll(`"`,String.raw`\"`)}"`}function K(e={}){return c.effect(W,a.gen(function*(){let t=yield*ne.FileSystem,n=yield*re.Path,r=yield*S.ChildProcessSpawner,i=e.platform??process.platform,o=e.directory??n.join(te(),`.config`,`2digits`,`tlo-mcp`),s=n.join(o,`session.json`),c=n.join(o,`session.lock`),l=t.makeDirectory(o,{recursive:!0,mode:448}).pipe(a.andThen(t.chmod(o,448))),u=a.fn(`SessionStore.security`)(function*(e,t){let n=yield*r.spawn(x.make(`/usr/bin/security`,e,{stdin:t===void 0?`ignore`:D.succeed(new TextEncoder().encode(t)),stderr:`ignore`}));return yield*a.all({output:n.stdout.pipe(D.decodeText(),D.mkString),exitCode:n.exitCode},{concurrency:`unbounded`})},a.scoped),f=a.fn(`SessionStore.load`)(function*(){let e;if(i===`darwin`){let t=yield*u([`find-generic-password`,`-a`,`orbit`,`-s`,U,`-w`]);if(t.exitCode===44)return;if(t.exitCode!==0)return yield*R.make({message:`Could not read macOS Keychain.`});e=t.output.trim()}else{let n=yield*t.readFileString(s).pipe(a.catchReason(`PlatformError`,`NotFound`,()=>a.void));if(n===void 0)return;e=n}return yield*d.decodeEffect(H)(e)},a.mapError(()=>R.make({message:`Could not read credentials. Run tlo-mcp login again.`}))),p=a.fn(`SessionStore.save`)(function*(e){let r=yield*d.encodeEffect(H)(e);if(i===`darwin`){if((yield*u([`-q`,`-i`],`add-generic-password -U -a orbit -s ${G(U)} -w ${G(r)}\n`)).exitCode!==0)return yield*R.make({message:`Could not save macOS Keychain credentials.`})}else{yield*l;let e=yield*t.makeTempDirectoryScoped({directory:o,prefix:`session-`}),i=n.join(e,`session.json`);yield*t.writeFileString(i,r,{mode:384}),yield*t.rename(i,s)}},a.scoped,a.mapError(()=>R.make({message:`Could not persist OAuth credentials. Run tlo-mcp login again.`}))),m=a.fn(`SessionStore.clear`)(function*(){if(i===`darwin`){let e=yield*u([`delete-generic-password`,`-a`,`orbit`,`-s`,U]);if(e.exitCode!==0&&e.exitCode!==44)return yield*R.make({message:`Could not remove Keychain credentials.`})}else yield*t.remove(s,{force:!0})},a.mapError(()=>R.make({message:`Could not remove OAuth credentials.`}))),h=a.fn(`SessionStore.lock`)(function*(){yield*l,yield*a.acquireRelease(t.writeFileString(c,String(process.pid),{flag:`wx`,mode:384}).pipe(a.catchReason(`PlatformError`,`AlreadyExists`,()=>R.make({message:`OAuth credentials locked. Close other login/refresh processes. After a crash only, remove ${c}.`}))),()=>t.remove(c).pipe(a.orDie))},a.mapError(e=>d.is(R)(e)?e:R.make({message:`Could not acquire OAuth credential lock. Check directory permissions.`})));return W.of({load:f(),save:p,clear:m(),lock:h()})}))}const q=K(),se=d.Struct({client_id:d.String.check(d.isMinLength(1)),client_secret:d.String.check(d.isMinLength(1)).pipe(d.RedactedFromValue,d.optionalKey)});var J=class extends r.Service()(`@2digits/tlo-mcp/oauth/OrbitAuth`){};const Y=c.effect(J,a.gen(function*(){let r=yield*W,c=yield*i.Crypto,g=yield*m.HttpClient,_=yield*S.ChildProcessSpawner,v=yield*f.make(1),y=yield*a.fn(`OrbitAuth.discover`)(function*(){let t=yield*m.withScope(g).get(`${P}/.well-known/oauth-authorization-server`);if(t.status!==200)return yield*R.make({message:`Could not discover Orbit OAuth endpoints.`});let n=yield*t.json.pipe(a.flatMap(d.decodeUnknownEffect(e.Metadata)));return n.issuer!==`https://api.orbit.teamleader.eu`||n.authorization_endpoint!==`https://api.orbit.teamleader.eu/oauth2/Authorize`||n.token_endpoint!==`https://api.orbit.teamleader.eu/oauth2/access_token`?yield*R.make({message:`Unexpected Orbit endpoints; refusing to send credentials.`}):n},a.scoped,a.timeout(`15 seconds`),a.mapError(()=>R.make({message:`Invalid or unavailable Orbit OAuth metadata.`})))().pipe(a.cachedWithTTL(e=>s.isSuccess(e)?1/0:0)),b=a.fn(`OrbitAuth.accessToken`)(function*(){let t=yield*r.load;if(t===void 0)return yield*R.make({message:`Not authenticated. Run tlo-mcp login first.`});if(!B(t,yield*n.currentTimeMillis))return t.accessToken;if(t.refreshToken===void 0)return t.expiresAt===void 0?t.accessToken:yield*R.make({message:`Orbit session expired without a refresh token. Run tlo-mcp login.`});let i=yield*y,o=z(t,yield*(yield*e.make({metadata:i,clientId:t.clientId,authentication:V(t),timeoutMs:15e3})).refreshGrant({refreshToken:t.refreshToken,resources:[F]}).pipe(a.flatMap(e.tokens)),yield*n.currentTimeMillis);return yield*r.save(o),o.accessToken}),C=a.fn(`OrbitAuth.register`)(function*(e){let t=yield*h.post(`${P}/oauth2/register`).pipe(h.bodyJson({client_name:`@2digits/tlo-mcp`,redirect_uris:[e],response_types:[`code`],grant_types:[`authorization_code`,`refresh_token`],token_endpoint_auth_method:`none`}),a.flatMap(m.withScope(g).execute));return t.status!==200&&t.status!==201?yield*R.make({message:`Orbit client registration failed (HTTP ${t.status}).`}):yield*t.json.pipe(a.flatMap(d.decodeUnknownEffect(se)))},a.scoped,a.timeout(`15 seconds`)),w=a.fn(`OrbitAuth.openBrowser`)(function*(e){return yield*(yield*l.value(process.platform).pipe(l.when(`darwin`,()=>x.make({stdout:`ignore`,stderr:`ignore`,forceKillAfter:`1 second`})`open ${u.value(e)}`),l.when(`win32`,()=>x.make({stdout:`ignore`,stderr:`ignore`,forceKillAfter:`1 second`})`rundll32 url.dll,FileProtocolHandler ${u.value(e)}`),l.orElse(()=>x.make({stdout:`ignore`,stderr:`ignore`,forceKillAfter:`1 second`})`xdg-open ${u.value(e)}`))).exitCode},a.scoped,a.timeout(`5 seconds`),a.filterOrFail(o.equals(S.ExitCode(0)),()=>R.make({message:`Could not open browser.`})),(e,t)=>e.pipe(a.catch(()=>a.sync(()=>{process.stderr.write(`Open this URL in your browser:\n${u.value(t)}\n`)})))),T=a.fn(`OrbitAuth.login`)(function*(){let i=yield*y,o=yield*t.random(),s=yield*t.make(),c=yield*o.pipe(u.value,oe),l=yield*C(c.redirectUri),d={clientId:l.client_id,...l.client_secret===void 0?{}:{clientSecret:l.client_secret}},f=yield*e.make({metadata:i,...d,authentication:V(d),timeoutMs:15e3}),p=yield*f.authorizationUrl({redirectUri:c.redirectUri,scopes:[],state:o,codeChallenge:s.challenge,resources:[F]});yield*w(p);let m=yield*c.code,h=yield*f.codeGrant({code:u.make(m),redirectUri:c.redirectUri,pkceVerifier:s.verifier,resources:[F]}).pipe(a.flatMap(e.tokens));yield*r.save(z(d,h,yield*n.currentTimeMillis)),yield*a.sync(()=>{process.stderr.write(`Orbit login complete.
2
- `)})}),E=a.fn(`OrbitAuth.locked`)(function*(e){return yield*r.lock,yield*e},a.scoped,v.withPermit,a.provideService(m.HttpClient,g),a.provideService(i.Crypto,c),a.provideService(S.ChildProcessSpawner,_),a.provideService(p.RequestInit,{redirect:`error`}),a.mapError(e=>d.is(R)(e)?e:R.make({message:`Orbit authentication unavailable. Check connectivity or run tlo-mcp login again.`})));return J.of({accessToken:E(b()),login:E(T()),logout:E(r.clear)})})),ce=Y.pipe(c.provide(q),c.provide(p.layer)),X=d.Union([d.Struct({jsonrpc:d.Literal(`2.0`),id:j.RequestId,result:d.Unknown}),d.Struct({jsonrpc:d.Literal(`2.0`),id:d.NullOr(j.RequestId),error:j.McpErrorBase}),d.Struct({jsonrpc:d.Literal(`2.0`),method:d.String,params:d.optionalKey(d.Unknown)})]),le=d.fromJsonString(X),Z=A.v2025_06_18.protocolVersion;var Q=class extends d.TaggedError()(`OrbitMcpError`,{message:d.String}){},$=class extends r.Service()(`@2digits/tlo-mcp/mcp/OrbitClient`){};const ue=c.effect($,a.gen(function*(){let e=yield*J,t=(yield*m.HttpClient).pipe(m.withScope,m.transformResponse(a.flatMap(g.matchStatus({"2xx":e=>a.succeed(e),401:e=>Q.make({message:`Orbit HTTP ${e.status}: authorization rejected. Run tlo-mcp login; a 403 can also mean insufficient rights.`}),403:e=>Q.make({message:`Orbit HTTP ${e.status}: authorization rejected. Run tlo-mcp login; a 403 can also mean insufficient rights.`}),429:e=>Q.make({message:`Orbit rate limited. Retry after ${e.headers[`retry-after`]??`the server cooldown`}; request not replayed.`}),orElse:e=>Q.make({message:`Orbit HTTP ${e.status}. Request not replayed.`})})))),n=0,r,i=a.fn(`OrbitClient.send`)(function*(i,o,s=!1){let c=++n,l=yield*e.accessToken,u=yield*h.post(F).pipe(h.bearerToken(l),h.setHeader(`accept`,`application/json, text/event-stream`),h.setHeader(`mcp-protocol-version`,Z),e=>r===void 0?e:h.setHeader(e,`mcp-session-id`,r),h.bodyJson({jsonrpc:`2.0`,...s?{}:{id:c},method:i,params:o})),f=yield*t.execute(u),p=f.headers[`mcp-session-id`];if(i===`initialize`&&(r=p),s)return;let m=f.headers[`content-type`]?.startsWith(`text/event-stream`)?yield*f.stream.pipe(D.decodeText(),D.pipeThroughChannel(ie.decode({maxEventSize:10485760})),D.filter(e=>e.event===`message`||e.event===``),D.map(k.get(`data`)),D.mapEffect(e=>d.decodeEffect(le)(e)),D.filter(e=>`id`in e&&e.id===c),D.runHead,a.flatMap(e=>O.isSome(e)?a.succeed(e.value):Q.make({message:`Orbit stream ended before replying.`}))):yield*g.schemaBodyJson(X)(f);return!(`id`in m)||m.id!==c?yield*Q.make({message:`Unexpected Orbit response ID.`}):`error`in m?yield*Q.make({message:m.error.message}):m.result},a.timeout(`120 seconds`),a.scoped,a.provideService(p.RequestInit,{redirect:`error`}),a.mapError(e=>d.is(Q)(e)?e:Q.make({message:`Orbit connection or response failed. Request not replayed.`}))),o=yield*i(`initialize`,{protocolVersion:Z,capabilities:{},clientInfo:{name:`@2digits/tlo-mcp`,version:`0.1.38`}}).pipe(a.flatMap(d.decodeUnknownEffect(j.InitializeResult)));if(o.protocolVersion!==Z)return yield*Q.make({message:`Unsupported upstream MCP protocol: ${o.protocolVersion}.`});yield*i(`notifications/initialized`,{},!0);let s=/* @__PURE__ */ new Set,c=yield*D.paginate(void 0,a.fn(`OrbitClient.listToolsPage`)(function*(e){let t=yield*i(`tools/list`,e===void 0?{}:{cursor:e}).pipe(a.flatMap(d.decodeUnknownEffect(j.ListToolsResult)));if(t.nextCursor!==void 0){if(s.has(t.nextCursor))return yield*Q.make({message:`Orbit returned a repeated tools cursor.`});s.add(t.nextCursor)}return[t.tools,O.fromUndefinedOr(t.nextCursor)]})).pipe(D.runCollect);return $.of({tools:c,instructions:o.instructions,callTool:a.fn(`OrbitClient.callTool`)(function*(e,t){return yield*i(`tools/call`,{name:e,arguments:t})},a.flatMap(d.decodeUnknownEffect(j.CallToolResult)),a.catchTag(`SchemaError`,()=>Q.make({message:`Invalid Orbit tool result. Request not replayed.`})))})})).pipe(c.provide(p.layer)),de=c.merge(N.layer([N.consolePretty()]),c.succeed(N.LogToStderr,!0)),fe=c.unwrap(a.gen(function*(){let e=yield*$;return c.effectDiscard(a.gen(function*(){let t=yield*M.McpServer;yield*a.forEach(e.tools,a.fn(`OrbitProxyLive.tool`)(function*(n){yield*t.addTool({tool:n,annotations:r.empty(),handle:a.fn(`handle`)(function*(e){return yield*d.decodeUnknownEffect(d.JsonObject)(e)},a.mapError(e=>j.InvalidParams.make({message:`Tool arguments must be a JSON object.`,data:e})),a.flatMap(t=>e.callTool(n.name,t).pipe(a.mapError(e=>j.InternalError.make({message:e.message})))))})}),{discard:!0})})).pipe(c.provide(M.layerStdio({name:`tlo-mcp`,version:`0.1.38`,instructions:e.instructions,protocols:[A.v2025_11_25,A.v2025_06_18,A.v2025_03_26]})),c.provide(de))}));export{J as a,W as c,R as d,L as f,Q as i,q as l,$ as n,Y as o,ue as r,ce as s,fe as t,K as u};