Share
A public URL for a service on your own machine, and a list of what you have open.
Also for this capability: API · CLI · MCP · SDKs
A public URL for a service on your own machine, and a list of what you have open.
| Base URL | https://api.hanzo.ai |
| Operations | 2 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1152 · Share — A Public URL for a Local Service — Draft · read the specification →
/v1/share publishes a service on your own machine to a public
https://<token>.share.hanzo.ai URL, and lists what you have open. It is
implemented in hanzoai/cloud at apps/share as the thin, org-scoped control
surface over a zrok controller: it provisions a per-org tunnel account from the
caller's validated identity and hands the CLI the credential it needs, so
hanzo share 3000 needs zero manual setup. The heavy data plane — the ziti
fabric and the public frontend proxy — stays a separate runtime by design.
Motivation
A tunnel needs an account, and an account is exactly the kind of manual setup that makes a developer tool go unused. Folding the control surface into the one cloud binary means the account is derived from the identity the caller already holds; the alternative — each developer registering against the controller by hand — puts a second credential system beside IAM for no property in return.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
It owns no store
Provisioning is stateless: the per-org account is keyed deterministically off the
org slug — email share+<org>@hanzo.ai, password
HMAC(SHARE_ACCOUNT_SECRET, org) — so ensure-account plus login reconstruct the
same credential every time from the controller. The controller IS the store
(apps/share/client.go). Creating an existing account is ignored; login always
returns the current account token, so enable is idempotent and a repeat call
hands back the same account rather than creating a second one.
The address
Two typed operations: POST /v1/share/enable (provision the caller org's tunnel
account and return the credential, controller endpoint, namespace and URL
template the CLI needs) and GET /v1/share (the org's active shares, for the CLI
and console). The list answers empty rather than absent when there are none.
Tenancy
The org is resolved in one place before any handler touches the controller
(gate, apps/share/share.go), from principal.Acting — the typed-op reader of
the validated org — never from an input field, which is caller-supplied and would
make a tenant key the caller's to assert. A caller can only ever provision or
list its OWN org's account.
Fail-closed
Absent the controller admin credential (ZROK_ADMIN_TOKEN, KMS-injected into the
environment), every operation MUST answer an honest 503; the surface never
fabricates a share or a token. An unreachable controller is 502.
Money, events, observability, stage
It is metered (plugin/share/main.go:28, Price: cloud.Metered;
spend.go:313), and the billed act is the provision: enable creates one
tunnel account on the fabric using the platform's credential, authorized
before the fabric is asked and debited once at SHARE_FEE_CENTS
(apps/share/meter.go). The unit is the account, once per org — enable is
idempotent, so a repeat call hands back the same credential unbilled, and
reading shares back is free whatever the balance. The bytes themselves never
cross this process, so there is no traffic here to meter. It publishes
nothing on the bus and emits nothing beyond the request span every route
gets. The stage is beta: the manifest row declares it
(manifest/apps.go:335, Stage: Beta); it is developer tooling in the core
loop, the server half of the hanzo share command, reached by flag while
the fabric deployment settles.
Upstream
It fronts the zrok controller (github.com/openziti/zrok, Apache-2.0), run as its
own deployment together with the ziti fabric it rides on. None of zrok's code is
linked into cloud — the control client here is hand-written against the
controller's REST API, and the API base is env-selectable so the fork's move of
that API under /v1 is a config edit, not a rebuild
(apps/share/client.go:53-62).
Rationale
The alternative to a thin control surface is folding the whole tunnel stack into the cloud binary. The data plane is long-lived connections and a public proxy — a different failure domain and a different scaling shape from a request-scoped API — so the split keeps the binary's blast radius out of every open tunnel. The deterministic account derivation is the price of owning no store: it trades a table of per-org credentials for one HMAC secret, which concentrates custody in KMS where it already is.
Security Considerations
Three secrets, three consequences. The admin token is control of every org's
tunnels, which is why it arrives from KMS and its absence turns the surface off
rather than open. The HMAC secret derives every org's controller password, so its
compromise is impersonation of any org at the controller — same custody, same
rule. The account token returned by enable is the caller's own tunnel
credential and is treated as a secret in transit. The wrong implementation of
tenancy — reading the org from the request — would let one tenant enumerate and
enable another's tunnels; the org is therefore never an input.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | share at its own prefix | 2 operations |
| CLI | hanzo share … | 2 of 2 |
| SDK | ShareApi in every published client | 2 methods |
| MCP | tool share on https://api.hanzo.ai/v1/mcp | 2 operations, 1 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/share, operation get_share:
hanzo share getimport { Configuration, ShareApi } from 'hanzoai';
const api = new ShareApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getShare();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ShareApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ShareApi(client).get_share()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.ShareAPI.GetShare(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, share_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = share_api::get_share(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ShareApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new ShareApi(client).getShare();curl https://api.hanzo.ai/v1/share \
-H "Authorization: Bearer $HANZO_API_KEY"Tool share, op get_share — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "share",
"arguments": {
"op": "get_share",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/share/enable | Enable provisions the caller org's tunnel account and returns the credential the hanzo share CLI needs to run a tunnel. |
GET /v1/share | Returns the tunnel shares the caller's org currently has open, across every environment that org has enabled. |
How is this guide?