@usine/gateway 0.1.0 → 0.2.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/LICENSE +661 -21
- package/README.md +99 -11
- package/dist/admin/api.d.ts +6 -66
- package/dist/admin/api.d.ts.map +1 -1
- package/dist/admin/api.js +4 -4
- package/dist/admin/api.js.map +1 -1
- package/dist/admin/server.d.ts +1 -1
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +1 -1
- package/dist/admin/server.js.map +1 -1
- package/dist/attachment/attachment.d.ts +8 -1
- package/dist/attachment/attachment.d.ts.map +1 -1
- package/dist/attachment/attachment.js +14 -4
- package/dist/attachment/attachment.js.map +1 -1
- package/dist/attachment/store.d.ts +7 -0
- package/dist/attachment/store.d.ts.map +1 -1
- package/dist/attachment/store.js +46 -14
- package/dist/attachment/store.js.map +1 -1
- package/dist/caller/caller.d.ts +1 -1
- package/dist/caller/caller.d.ts.map +1 -1
- package/dist/caller/caller.js +8 -7
- package/dist/caller/caller.js.map +1 -1
- package/dist/chatgpt/auth-file.d.ts +47 -0
- package/dist/chatgpt/auth-file.d.ts.map +1 -0
- package/dist/chatgpt/auth-file.js +132 -0
- package/dist/chatgpt/auth-file.js.map +1 -0
- package/dist/chatgpt/logins.d.ts +50 -0
- package/dist/chatgpt/logins.d.ts.map +1 -0
- package/dist/chatgpt/logins.js +303 -0
- package/dist/chatgpt/logins.js.map +1 -0
- package/dist/gateway.d.ts +8 -4
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +8 -4
- package/dist/gateway.js.map +1 -1
- package/dist/github/github.d.ts +19 -8
- package/dist/github/github.d.ts.map +1 -1
- package/dist/github/github.js +45 -21
- package/dist/github/github.js.map +1 -1
- package/dist/github/rename.d.ts +30 -0
- package/dist/github/rename.d.ts.map +1 -0
- package/dist/github/rename.js +83 -0
- package/dist/github/rename.js.map +1 -0
- package/dist/llm/llm.d.ts +64 -15
- package/dist/llm/llm.d.ts.map +1 -1
- package/dist/llm/llm.js +89 -37
- package/dist/llm/llm.js.map +1 -1
- package/dist/node/egress-proxy.js +1 -1
- package/dist/node/egress-proxy.js.map +1 -1
- package/dist/node/environment.d.ts +15 -10
- package/dist/node/environment.d.ts.map +1 -1
- package/dist/node/environment.js +51 -24
- package/dist/node/environment.js.map +1 -1
- package/dist/node/node.d.ts +7 -8
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +22 -23
- package/dist/node/node.js.map +1 -1
- package/dist/node/reload.d.ts +2 -1
- package/dist/node/reload.d.ts.map +1 -1
- package/dist/node/reload.js +41 -13
- package/dist/node/reload.js.map +1 -1
- package/dist/proxy/forward.d.ts +2 -2
- package/dist/proxy/forward.d.ts.map +1 -1
- package/dist/proxy/forward.js +1 -1
- package/dist/proxy/forward.js.map +1 -1
- package/dist/proxy/proxy.d.ts +4 -3
- package/dist/proxy/proxy.d.ts.map +1 -1
- package/dist/proxy/proxy.js +132 -24
- package/dist/proxy/proxy.js.map +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @usine/gateway
|
|
2
2
|
|
|
3
|
-
The service on the gateway host in front of usine's worker VMs ([ADR 0027](../../docs/adr/0027-gateway-credential-proxy.md), [#223](https://github.com/basaltbytes/usine/issues/223)). Worker VMs have no public address, GitHub token, or
|
|
3
|
+
The service on the gateway host in front of usine's worker VMs ([ADR 0027](../../docs/adr/0027-gateway-credential-proxy.md), [#223](https://github.com/basaltbytes/usine/issues/223)). Worker VMs have no public address, GitHub token, API key, or subscription login. They reach GitHub through the gateway, which adds the credential. They reach an LLM provider the same way if the gateway has that provider's credential: an API key, a Claude subscription token, or a ChatGPT login. Otherwise they use the harness's own login through the gateway's forward proxy, which allows only allowlisted hosts: package registries and the harness login hosts. [Workers and the gateway](https://docs.usine.dev/architecture/workers-and-gateway/) describes the deployment.
|
|
4
4
|
|
|
5
5
|
The gateway serves:
|
|
6
6
|
|
|
7
|
-
- **A reverse proxy** for workers on the private network. `git.<domain>` serves git smart HTTP at `/OWNER/REPO.git
|
|
7
|
+
- **A reverse proxy** for workers on the private network. `git.<domain>` serves git smart HTTP at `/OWNER/REPO.git`, release downloads, and the GitHub REST and GraphQL API under `/api/`, as GitHub Enterprise Server does, so `gh` works with `GH_HOST=git.<domain>`. It adds the installation token that the executor attached for that worker and repo; see [GitHub paths](#github-paths). `llm.<domain>` serves the Anthropic API under `/anthropic`, and under `/openai` an OpenAI-compatible API or, with a [ChatGPT login](#chatgpt-login), the ChatGPT backend that Codex uses, with the gateway's credential. Other hosts are refused; the forward proxy serves the allowlisted ones.
|
|
8
8
|
- **A forward proxy** for `HTTPS_PROXY` and `HTTP_PROXY`: `CONNECT` on port 443 for HTTPS and absolute-form requests for plain HTTP mirrors, to allowlisted hosts only. A tunnel opens only if the client's TLS ClientHello names the CONNECT host. A ClientHello naming another host or none, an encrypted ClientHello, or non-TLS bytes end the tunnel (`server-name-mismatch`, `server-name-missing`, `not-tls`). It resolves each name itself and refuses private, loopback, and link-local addresses. Before a plain HTTP request leaves, it removes `X-Usine-Gateway-Token` and any `Authorization` or `x-api-key` value that is a caller token, and leaves other credentials alone.
|
|
9
9
|
- **An admin API** for the executor: attach, renew, detach, and list a worker's repo credentials, and list the [LLM providers](#llm-providers) it has credentials for. It requires the admin token as a bearer token. Attachments are kept in memory; the executor attaches again when it renews or restarts. A [reload](#reload) keeps them.
|
|
10
10
|
|
|
11
|
-
Refused requests log `Gateway refused a request` at warning level, with the reason, host, client address, and the caller token's worker. Forwarded requests log at info with the worker, repo, method, path, and status. No token, key,
|
|
11
|
+
Refused requests log `Gateway refused a request` at warning level, with the reason, host, client address, and the caller token's worker. Forwarded requests log at info with the worker, repo, method, path, and status, and with the host of the response's `Location` header (`target`), if any; release downloads log as `Served a GitHub release download`. A [rewritten](#github-paths) `Location` header also logs `Rewrote a GitHub Location header to the gateway`, with the original host (`from`) and the new path. A repo's [new name](#github-paths) that an attachment learns from GitHub logs `Learned the new name of an attached repository`, with `newName`; a new name that GitHub's API doesn't confirm logs `GitHub's API didn't confirm the new name of an attached repository` at warning level, with `newName` and the `reason`. A ChatGPT login logs each load, refresh, and failure; see [ChatGPT login](#chatgpt-login). No token, key, request body, or query string is logged.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -32,7 +32,7 @@ On Hetzner Cloud, `gateway.sh install` does either on the gateway VM and restart
|
|
|
32
32
|
|
|
33
33
|
| Import | Contents |
|
|
34
34
|
| --- | --- |
|
|
35
|
-
| `@usine/gateway` | The platform-free core: `GatewaySettings`, `SettingsInForce` (the allowlist and LLM providers that a reload replaces), `Attachments`, `CallerTokens`, `GatewayProxy` (the reverse proxy as an `HttpServerRequest` handler), `adminRoutes` (the admin API for an `HttpRouter`), `gatewayServices`, the egress allowlist parser, and the domain records. It needs an `HttpClient` that leaves `content-encoding` alone. |
|
|
35
|
+
| `@usine/gateway` | The platform-free core: `GatewaySettings`, `SettingsInForce` (the allowlist and LLM providers that a reload replaces), `Attachments`, `CallerTokens`, `ChatGptLogins` (the ChatGPT logins and their refresh), `GatewayProxy` (the reverse proxy as an `HttpServerRequest` handler), `adminRoutes` (the admin API for an `HttpRouter`), `gatewayServices`, the egress allowlist parser, and the domain records. It needs an `HttpClient` that leaves `content-encoding` alone, and a `FileSystem` for ChatGPT auth files. |
|
|
36
36
|
| `@usine/gateway/admin` | `GatewayAdminApi`, the admin API contract for `HttpApiClient.make` in the executor, with the records a client sends and reads (`AttachRequest`, `InstallationCredential`, `Attached`, `AttachmentId`, `CallerToken`, `Detached`, `AttachmentList`, `LlmProviderList`) and `GATEWAY_TOKEN_HEADER`. |
|
|
37
37
|
| `@usine/gateway/node` | `gatewayLayer` (the three servers on Node, with undici upstream), `gatewayFromEnvironment`, `settingsFromEnvironment`, `GatewayAddresses`, and `GatewayReload`. |
|
|
38
38
|
| `usine-gateway` | The gateway VM's command. It reads the settings below, reloads on SIGHUP, and logs one JSON object per line. |
|
|
@@ -60,6 +60,37 @@ A caller token works only while its attachment is in force. After a detach or to
|
|
|
60
60
|
|
|
61
61
|
A child sends its caller token wherever its tool sends a credential: the `X-Usine-Gateway-Token` header, `Authorization` as `Bearer`, `token`, or `Basic` (password), or `x-api-key`. The gateway removes all of them before forwarding.
|
|
62
62
|
|
|
63
|
+
## GitHub paths
|
|
64
|
+
|
|
65
|
+
`git.<domain>` serves these paths with the attachment's installation token. A path that names another repo than the caller token is refused with `repository-mismatch`, unless it's the repo's new name after a rename (see below); other paths, such as web pages, with `unsupported-path`. Paths that name no repo, such as `/api/graphql`, `/api/v3/user`, and `/api/v3/repositories/ID/...`, have no repo for the gateway to check: the installation token, issued for the attempt's repo only, limits them.
|
|
66
|
+
|
|
67
|
+
| Path on `git.<domain>` | Upstream | Token sent as |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `/OWNER/REPO.git/info/refs`, `/OWNER/REPO.git/git-upload-pack`, `/OWNER/REPO.git/git-receive-pack`, `/OWNER/REPO.git/info/lfs/...`, each also without `.git` | `https://github.com` | the `x-access-token` password |
|
|
70
|
+
| `/OWNER/REPO/releases/download/TAG/FILE`, `/OWNER/REPO/releases/latest/download/FILE` | `https://github.com` | the `x-access-token` password |
|
|
71
|
+
| `/api/v3/...`, `/api/graphql` | `https://api.github.com` | a bearer token |
|
|
72
|
+
| `/api/uploads/...` | `https://uploads.github.com` | a bearer token |
|
|
73
|
+
|
|
74
|
+
GitHub answers a release download with a redirect to an asset host, such as `release-assets.githubusercontent.com` or `objects.githubusercontent.com`, in a URL signed for that file for a few minutes. The gateway returns the redirect unchanged and doesn't follow it, so the token goes only to github.com. The worker fetches the asset through the forward proxy, which needs the asset hosts on the egress allowlist: the GitHub release assets rows of [`egress-allowlist`](../../infra/hetzner/gateway/egress-allowlist). On a worker, where `GH_TOKEN` holds the caller token:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
curl -fL -u ":$GH_TOKEN" -o tool.tar.gz http://git.<domain>/OWNER/REPO/releases/download/TAG/FILE
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
curl sends `-u` credentials only to the host of the first URL; a header set with `-H` would also reach the asset host.
|
|
81
|
+
|
|
82
|
+
gh can't download from these hosts. It sends every request to the gateway through its socket, followed redirects included, so `gh release download` reaches the reverse proxy for the asset host, which refuses it with `unknown-host`. For a host on the egress allowlist, the refusal says to use curl instead: the command above for a release asset, or this one for an archive, which GitHub redirects to `codeload.github.com`:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
curl -fL -u ":$GH_TOKEN" -o repo.tar.gz http://git.<domain>/api/v3/repos/OWNER/REPO/tarball/REF
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Actions logs and artifacts redirect to `results-receiver.actions.githubusercontent.com` and Azure Blob Storage, which the allowlist doesn't list, so `gh run view --log`, `gh run view --log-failed`, and `gh run download` don't work on a worker, with gh or curl ([#540](https://github.com/basaltbytes/usine/issues/540)). [ADR 0027](../../docs/adr/0027-gateway-credential-proxy.md) records why the reverse proxy doesn't fetch other hosts.
|
|
89
|
+
|
|
90
|
+
GitHub redirects some requests for a renamed or transferred repo: API calls to `https://api.github.com/repositories/ID/...`, and release downloads to the new name on github.com. The gateway rewrites a `Location` on github.com or api.github.com to `http://git.<domain>`, with API paths under `/api/v3`, and keeps the path and query, so the client follows it through the gateway. A `Location` on any other host passes unchanged.
|
|
91
|
+
|
|
92
|
+
The gateway checks the followed request like any other. An attachment learns the repo's new name only from a permanent redirect (301 or 308) of the repo itself to the same path with another owner or repo name: git's `/OWNER/REPO/info/refs`, which starts every clone and fetch, or the API's `/repos/OWNER/REPO`. Before the attachment learns the name, the gateway confirms it with GitHub's API and the attachment's credential: `/repos/OWNER/REPO` with the attached name must redirect to `/repositories/ID`, and the `full_name` of that repo must be the new name. The attachment then serves paths with either name until it's detached or expires. Other attachments, temporary redirects, redirects of anything else, such as a release download or `/repositories/ID/...`, and names that the API doesn't confirm teach no name, so a path with another name is still refused with `repository-mismatch`. A release download under the repo's former name works once a git clone or fetch under that name has taught the attachment the new name.
|
|
93
|
+
|
|
63
94
|
## LLM providers
|
|
64
95
|
|
|
65
96
|
`GET /v1/providers` lists the providers the gateway has a credential for, with each credential's kind, never the credential:
|
|
@@ -68,31 +99,83 @@ A child sends its caller token wherever its tool sends a credential: the `X-Usin
|
|
|
68
99
|
{"providers": [{"provider": "anthropic", "credential": "api-key"}]}
|
|
69
100
|
```
|
|
70
101
|
|
|
71
|
-
`credential` is `api-key` for `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`, and `subscription` for `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN`. Providers without a credential are absent. The list follows a [reload](#reload). The executor reads it when it checks a worker whose `llm_providers` lists
|
|
102
|
+
`credential` is `api-key` for `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`, and `subscription` for `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN` and `USINE_GATEWAY_CHATGPT_AUTH_FILE`. Providers without a credential are absent. The list follows a [reload](#reload). The executor reads it when it checks a worker whose `llm_providers` lists a provider, so usage counts Claude Code and Codex children on the gateway's API key in the API keys pool, and on a subscription in the Claude or ChatGPT plan pool ([ADR 0013](../../docs/adr/0013-cost-accounting-native-meters-guardrails.md)). Gateways released before this endpoint answer 404.
|
|
72
103
|
|
|
73
104
|
## Worker environment
|
|
74
105
|
|
|
75
|
-
`infra/hetzner/gateway/worker-cloud-init.yaml` sets up the machine. For each attempt, the executor adds the caller token and sends each variable that contains it over the SSH connection, not the command line; the worker's sshd accepts them with `AcceptEnv`. The Claude Code row applies if the gateway has the Anthropic credential
|
|
106
|
+
`infra/hetzner/gateway/worker-cloud-init.yaml` sets up the machine. For each attempt, the executor adds the caller token and sends each variable that contains it over the SSH connection, not the command line; the worker's sshd accepts them with `AcceptEnv`. The Claude Code row applies if the gateway has the Anthropic credential, and the Codex row if it has the OpenAI credential; otherwise the harness uses its login on the worker and `HTTPS_PROXY`.
|
|
76
107
|
|
|
77
108
|
| Tool | Setting |
|
|
78
109
|
| --- | --- |
|
|
79
110
|
| git | remote `http://git.<domain>/OWNER/REPO.git`; `GIT_CONFIG_KEY_0=http.http://git.<domain>/.extraHeader`, `GIT_CONFIG_VALUE_0="X-Usine-Gateway-Token: <caller token>"`, and `url.http://git.<domain>/.insteadOf` for `https://github.com/` and `https://git.<domain>/` |
|
|
80
111
|
| gh | `GH_HOST=git.<domain>`, `GH_ENTERPRISE_TOKEN=<caller token>`, `GH_TOKEN=<caller token>`; gh's config sets `http_unix_socket: /run/usine-gateway/http.sock`, which `systemd-socket-proxyd` forwards to the gateway, because gh speaks only HTTPS to a custom host but plain HTTP over a socket |
|
|
81
112
|
| Claude Code | `ANTHROPIC_BASE_URL=http://llm.<domain>/anthropic`, `ANTHROPIC_AUTH_TOKEN=<caller token>` |
|
|
82
|
-
| Codex |
|
|
113
|
+
| Codex | `CODEX_HOME=<root>/workers/<worker>/codex/<attempt>`, whose `config.toml` sets `model_provider = "usine-gateway"` with `base_url = "http://llm.<domain>/openai/v1"`, `env_key = "USINE_GATEWAY_TOKEN"`, `wire_api = "responses"`, and `requires_openai_auth = false`; `USINE_GATEWAY_TOKEN=<caller token>` |
|
|
83
114
|
| package managers, harness logins | `HTTPS_PROXY` and `HTTP_PROXY` set to `http://<gateway>:3128`, `NO_PROXY=<domain>` |
|
|
84
115
|
|
|
116
|
+
Codex with this `model_provider` sends its caller token as a bearer token, needs no login, and calls only `POST <base_url>/responses` over HTTP with server-sent events, since a custom provider uses neither the Responses WebSocket nor the remote model list. The same configuration works whether the gateway holds an API key or a ChatGPT login.
|
|
117
|
+
|
|
85
118
|
A client that sends every request through `HTTPS_PROXY`, as Claude Code does, still reaches `git.<domain>` and `llm.<domain>`: the forward proxy sends their port 80 requests to the reverse proxy.
|
|
86
119
|
|
|
87
120
|
## Configuration
|
|
88
121
|
|
|
89
122
|
The [CLI reference](https://docs.usine.dev/reference/configuration/cli/) lists every `usine-gateway` variable, with defaults and the environment file rules. Changing `USINE_GATEWAY_CALLER_KEY` invalidates every caller token.
|
|
90
123
|
|
|
91
|
-
|
|
124
|
+
Each provider holds one credential; setting both of a provider's variables is an error.
|
|
125
|
+
|
|
126
|
+
| Provider | Variable | Credential | Sent upstream as |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | An Anthropic API key | `x-api-key` |
|
|
129
|
+
| `anthropic` | `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN` | A Claude subscription token from `claude setup-token` | A bearer token, with the OAuth beta flag merged into `anthropic-beta` |
|
|
130
|
+
| `openai` | `OPENAI_API_KEY` | An OpenAI-compatible API key | A bearer token |
|
|
131
|
+
| `openai` | `USINE_GATEWAY_CHATGPT_AUTH_FILE` | The absolute path of a Codex `auth.json` with a ChatGPT login | A bearer token with the login's access token, and `ChatGPT-Account-ID`; see [ChatGPT login](#chatgpt-login) |
|
|
132
|
+
|
|
133
|
+
`USINE_GATEWAY_ANTHROPIC_UPSTREAM` (default `https://api.anthropic.com`) is the Anthropic upstream. `USINE_GATEWAY_OPENAI_UPSTREAM` (default `https://api.openai.com`) is the upstream for an OpenAI API key and can be any OpenAI-compatible provider, and `USINE_GATEWAY_CHATGPT_UPSTREAM` (default `https://chatgpt.com/backend-api/codex`) the one for a ChatGPT login. A provider without a credential is refused with `provider-not-configured`.
|
|
134
|
+
|
|
135
|
+
### LLM routes
|
|
136
|
+
|
|
137
|
+
Each provider serves only the routes its harness calls, whatever the credential, so a worker can't use the gateway's credential for the rest of the provider's API, such as files, batches, or organization administration. A route matches whole path segments, so a percent-encoded separator (`%2F`, `%5C`), a doubled or trailing `/`, or a dot segment, which URL parsing resolves first, never reaches the upstream. Other paths are refused with `unsupported-path` before any credential is used or refreshed.
|
|
138
|
+
|
|
139
|
+
| Prefix | Routes | Harness |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `/anthropic` | `/v1/messages`, `/v1/messages/count_tokens`, `/v1/models`, `/v1/models/MODEL` | Claude Code |
|
|
142
|
+
| `/openai` | `/v1/responses`, `/v1/responses/compact`, `/v1/models` | Codex |
|
|
143
|
+
|
|
144
|
+
The query string passes unchanged, such as Claude Code's `?beta=true`.
|
|
145
|
+
|
|
146
|
+
## Claude subscription token
|
|
147
|
+
|
|
148
|
+
A `claude setup-token` token is valid for one year and has no refresh token, so the gateway doesn't refresh it ([authentication](https://code.claude.com/docs/en/authentication)). Before it expires, run `claude setup-token` again, replace `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN`, and reload. The token is opaque, so the gateway can't tell when it expires; once it has, Anthropic refuses each request and Claude Code reports the error. Run the placeholder swap test in [Tests](#tests) before you rely on the subscription token.
|
|
149
|
+
|
|
150
|
+
## ChatGPT login
|
|
151
|
+
|
|
152
|
+
With `USINE_GATEWAY_CHATGPT_AUTH_FILE`, the gateway serves Codex's [LLM routes](#llm-routes) under `/openai` on `llm.<domain>` from the ChatGPT backend, which has them without `/v1`: Codex's `/openai/v1/responses` goes to `https://chatgpt.com/backend-api/codex/responses`. The gateway replaces the caller token with `Authorization: Bearer <access token>`, sets `ChatGPT-Account-ID` to the login's account, and passes Codex's other headers, such as `originator` and `session-id`, and the streamed body unchanged.
|
|
153
|
+
|
|
154
|
+
The file is the `auth.json` that `codex login` writes in `CODEX_HOME`. The gateway reads its `tokens.access_token`, `tokens.refresh_token`, and `tokens.account_id` (or the account in `tokens.id_token`) on first use and on each [reload](#reload), and keeps them in memory. It refreshes the access token as Codex does:
|
|
155
|
+
|
|
156
|
+
- When the access token expires within five minutes, or the backend answered a request with 401, the next request first sends the refresh token to `https://auth.openai.com/oauth/token` with Codex's client ID. A 401 for a token refreshed less than a minute earlier doesn't start another refresh.
|
|
157
|
+
- Refreshes run one at a time, and requests that arrive during a refresh wait for its result. A refresh runs apart from the request that started it, so a request that ends early doesn't stop it between the rotation and the write-back.
|
|
158
|
+
- The response's new access token, rotated refresh token, and ID token go to the file, through a temporary file with mode 0600 in the same directory that replaces it, so the service needs write access to the directory. The file's other fields stay. If the file holds a login the gateway didn't refresh, such as one you just copied in, it's left for the next reload.
|
|
159
|
+
- If a refresh fails, the waiting requests are refused with `provider-login-unavailable`, and nothing is sent upstream with the old token. After a network error, a server error, a 408, or a 429 from the token endpoint, the refusal is 503, which Codex retries: requests fail without calling the token endpoint for ten seconds, then the next one tries again. Any other client error, such as `invalid_grant`, or the codes `refresh_token_expired`, `refresh_token_reused`, and `refresh_token_invalidated`, rejects the refresh token: the refusal is 401, which Codex doesn't retry, and the gateway doesn't try again until a reload reads another login. An auth file that can't be read is refused with 503. Refusals give a fixed message and never the token endpoint's text; the log has its error code.
|
|
160
|
+
|
|
161
|
+
At startup and each reload, the gateway reads the file and checks that it can write next to it, and fails or keeps the previous providers if it can't. A reload keeps the tokens in memory if the file has their refresh token or one they replaced, so it never goes back to a used refresh token, and otherwise uses the file's login.
|
|
162
|
+
|
|
163
|
+
| Log line | Level | When |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `Loaded the ChatGPT login` | info | The file was read, with `authFile`, the access token's `expiresAt`, and `replaced` on a reload |
|
|
166
|
+
| `Refreshing the ChatGPT login`, `Refreshed the ChatGPT login` | info | A refresh started (`reason` is `expiring` or `refused-by-upstream`) and ended, with `rotated` |
|
|
167
|
+
| `Wrote the refreshed ChatGPT login to its auth file` | info | The rotated tokens are in the file |
|
|
168
|
+
| `Left the auth file as it is: it holds another ChatGPT login` | warning | The file has a login the gateway didn't refresh; reload to use it |
|
|
169
|
+
| `Could not write the refreshed ChatGPT login to its auth file` | error | The login works from memory, but after a restart the file's refresh token may no longer work |
|
|
170
|
+
| `Could not refresh the ChatGPT login; a later request tries again` | error | A network error or server error, with `error` and the `backoff` |
|
|
171
|
+
| `The token endpoint rejected the ChatGPT login's refresh token` | error | The login needs a new `auth.json` and a reload |
|
|
172
|
+
| `The ChatGPT backend refused the gateway's access token` | warning | The backend answered 401; the next request refreshes first, unless the token is less than a minute old |
|
|
173
|
+
|
|
174
|
+
Make the gateway's login separate from the one on your own machine. A refresh token works once, so if two copies of one `auth.json` refresh, the second is rejected with `refresh_token_reused`. [`infra/hetzner/gateway/README.md`](../../infra/hetzner/gateway/README.md) gives the steps.
|
|
92
175
|
|
|
93
176
|
## Reload
|
|
94
177
|
|
|
95
|
-
On SIGHUP, `usine-gateway` rereads its environment files and allowlist, and replaces the egress allowlist and the LLM provider settings: `ANTHROPIC_API_KEY`, `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN`, `OPENAI_API_KEY`,
|
|
178
|
+
On SIGHUP, `usine-gateway` rereads its environment files and allowlist, and replaces the egress allowlist and the LLM provider settings: `ANTHROPIC_API_KEY`, `USINE_GATEWAY_ANTHROPIC_SUBSCRIPTION_TOKEN`, `OPENAI_API_KEY`, `USINE_GATEWAY_CHATGPT_AUTH_FILE`, the three upstreams, and `USINE_GATEWAY_EGRESS_ALLOWLIST`. It also rereads the ChatGPT auth file, so copying a new `auth.json` over the old one and reloading replaces the login. Under systemd, `ExecReload=kill -HUP $MAINPID` makes `systemctl reload usine-gateway` send it.
|
|
96
179
|
|
|
97
180
|
Attachments, the admin API, listening sockets, and open tunnels stay, so running attempts keep their GitHub and LLM requests. An open tunnel stays until a side closes it; each new `CONNECT` or plain HTTP request uses the new allowlist, and each later LLM request the new key. The domain, listeners, admin token, and caller key change only with a restart. A reload that finds one of them changed logs `Settings changed that take effect only after a restart` at warning level with the variable names, and applies the rest.
|
|
98
181
|
|
|
@@ -101,7 +184,7 @@ Each reload ends with `Reloaded the gateway settings`, which gives the environme
|
|
|
101
184
|
| Problem | The reload keeps | Log line, at error level |
|
|
102
185
|
| --- | --- | --- |
|
|
103
186
|
| The allowlist can't be read or doesn't parse | The previous allowlist | `Kept the previous egress allowlist`, with the path, the line, and the problem |
|
|
104
|
-
| A provider setting is invalid,
|
|
187
|
+
| A provider setting is invalid, both of a provider's credentials are set, or the ChatGPT auth file can't be read or isn't a ChatGPT login | The previous providers | `Kept the previous LLM providers` |
|
|
105
188
|
| An environment file can't be read | Everything | `Kept the gateway settings: an environment file could not be read`, with the path |
|
|
106
189
|
|
|
107
190
|
`systemctl reload` returns before the reload ends; the journal has the result. Variables from the process environment at startup don't change, so a gateway without environment files reloads only its allowlist.
|
|
@@ -110,4 +193,9 @@ Each reload ends with `Reloaded the gateway settings`, which gives the environme
|
|
|
110
193
|
|
|
111
194
|
`pnpm --filter @usine/gateway test` runs the unit tests and an end-to-end test that starts the gateway on loopback ports, clones `octocat/Hello-World` through it, calls `gh api` through a Unix socket as a worker does, and opens a tunnel to `registry.npmjs.org`. It needs network access but no secret.
|
|
112
195
|
|
|
113
|
-
|
|
196
|
+
The unit tests for the ChatGPT login (`test/chatgpt.test.ts`) use stand-ins for the token endpoint and the ChatGPT backend: refresh timing, write-back of rotated tokens, one refresh for concurrent requests, failed and rejected refreshes, header rewriting, refused paths, and streaming through the Node gateway.
|
|
197
|
+
|
|
198
|
+
`pnpm --filter @usine/gateway test:placeholder-swap` runs the harnesses themselves against the gateway and local stand-ins, so CI, which has neither binary, doesn't run it. Run it on a machine with the harnesses on `PATH` before you rely on a subscription credential at the gateway:
|
|
199
|
+
|
|
200
|
+
- Claude Code (needs `claude`): the worker has only a caller token as `CLAUDE_CODE_OAUTH_TOKEN`, and the gateway sends a subscription token instead. The first case runs Claude Code against a local stand-in for the Anthropic API and checks what reached it. The second runs against `api.anthropic.com` with the real token in `CLAUDE_CODE_OAUTH_TOKEN`, and is skipped without it.
|
|
201
|
+
- Codex (needs `codex`): `codex exec` runs with the `model_provider` the executor writes and only a caller token, against a stand-in for the ChatGPT backend and its token endpoint. The login's access token expires within a minute, so the case checks that the gateway refreshes it, sends the new token and the account to the backend, and writes the rotated refresh token to the auth file. Nothing reaches OpenAI.
|
package/dist/admin/api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/
|
|
2
|
+
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
|
|
3
3
|
import { type Attachment, AttachmentAlreadyExpired, AttachmentConflict } from "../attachment/attachment.ts";
|
|
4
4
|
import { type LlmProviders } from "../llm/llm.ts";
|
|
5
5
|
export { AttachmentId, InstallationCredential } from "../attachment/attachment.ts";
|
|
@@ -84,39 +84,7 @@ export declare class AdminAuthorization extends AdminAuthorization_base {
|
|
|
84
84
|
* Attachments are in memory; after a Gateway restart, the executor's next renewal re-creates
|
|
85
85
|
* each one under the same ID, so caller tokens stay valid.
|
|
86
86
|
*/
|
|
87
|
-
export declare const GatewayAdminApi: HttpApi.HttpApi<"GatewayAdminApi", HttpApiGroup.HttpApiGroup<"attachments", HttpApiEndpoint.HttpApiEndpoint<"
|
|
88
|
-
id: Schema.brand<Schema.String, "AttachmentId">;
|
|
89
|
-
}>>, never, never, never, Schema.toCodecJson<typeof Detached>, never, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"list", "GET", "/v1/attachments", never, never, never, never, Schema.toCodecJson<typeof AttachmentList>, never, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"attach", "POST", "/v1/attachments", never, never, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachRequest, {
|
|
90
|
-
readonly worker: string;
|
|
91
|
-
readonly repository: string;
|
|
92
|
-
readonly credential: {
|
|
93
|
-
readonly _tag: "InstallationCredential";
|
|
94
|
-
readonly token: import("effect/Redacted").Redacted<string>;
|
|
95
|
-
} | {
|
|
96
|
-
readonly _tag: "AnonymousCredential";
|
|
97
|
-
};
|
|
98
|
-
readonly expiresAt: import("effect/DateTime").Utc;
|
|
99
|
-
}, readonly [Schema.Struct<{
|
|
100
|
-
readonly worker: Schema.brand<Schema.String, "WorkerName">;
|
|
101
|
-
readonly repository: Schema.brand<Schema.String, "RepositoryName">;
|
|
102
|
-
readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
|
|
103
|
-
readonly expiresAt: Schema.DateTimeUtc;
|
|
104
|
-
}>], {
|
|
105
|
-
readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
|
|
106
|
-
readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
|
|
107
|
-
readonly credential: {
|
|
108
|
-
readonly _tag: "InstallationCredential";
|
|
109
|
-
readonly token: import("effect/Redacted").Redacted<string>;
|
|
110
|
-
} | {
|
|
111
|
-
readonly _tag: "AnonymousCredential";
|
|
112
|
-
};
|
|
113
|
-
readonly expiresAt: import("effect/DateTime").Utc;
|
|
114
|
-
}>, Schema.Struct<{
|
|
115
|
-
readonly worker: Schema.brand<Schema.String, "WorkerName">;
|
|
116
|
-
readonly repository: Schema.brand<Schema.String, "RepositoryName">;
|
|
117
|
-
readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
|
|
118
|
-
readonly expiresAt: Schema.DateTimeUtc;
|
|
119
|
-
}>, never, never>>, never, Schema.toCodecJson<typeof Attached>, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachmentAlreadyExpired, {
|
|
87
|
+
export declare const GatewayAdminApi: HttpApi.HttpApi<"GatewayAdminApi", HttpApiGroup.HttpApiGroup<"attachments", HttpApiEndpoint.HttpApiEndpoint<"attach", "POST", "/v1/attachments", never, never, Schema.toCodecJson<typeof AttachRequest>, never, Schema.toCodecJson<typeof Attached>, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachmentAlreadyExpired, {
|
|
120
88
|
readonly _tag: "AttachmentAlreadyExpired";
|
|
121
89
|
readonly worker: string;
|
|
122
90
|
readonly repository: string;
|
|
@@ -134,39 +102,11 @@ export declare const GatewayAdminApi: HttpApi.HttpApi<"GatewayAdminApi", HttpApi
|
|
|
134
102
|
readonly worker: Schema.brand<Schema.String, "WorkerName">;
|
|
135
103
|
readonly repository: Schema.brand<Schema.String, "RepositoryName">;
|
|
136
104
|
readonly expiresAt: Schema.DateTimeUtc;
|
|
137
|
-
}>, never, never>>, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"
|
|
105
|
+
}>, never, never>>, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"detach", "DELETE", "/v1/attachments/:id", Schema.toCodecStringTree<Schema.Struct<{
|
|
138
106
|
id: Schema.brand<Schema.String, "AttachmentId">;
|
|
139
|
-
}>>, never, Schema.toCodecJson<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
readonly credential: {
|
|
143
|
-
readonly _tag: "InstallationCredential";
|
|
144
|
-
readonly token: import("effect/Redacted").Redacted<string>;
|
|
145
|
-
} | {
|
|
146
|
-
readonly _tag: "AnonymousCredential";
|
|
147
|
-
};
|
|
148
|
-
readonly expiresAt: import("effect/DateTime").Utc;
|
|
149
|
-
}, readonly [Schema.Struct<{
|
|
150
|
-
readonly worker: Schema.brand<Schema.String, "WorkerName">;
|
|
151
|
-
readonly repository: Schema.brand<Schema.String, "RepositoryName">;
|
|
152
|
-
readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
|
|
153
|
-
readonly expiresAt: Schema.DateTimeUtc;
|
|
154
|
-
}>], {
|
|
155
|
-
readonly worker: string & import("effect/Brand").Brand<"WorkerName">;
|
|
156
|
-
readonly repository: string & import("effect/Brand").Brand<"RepositoryName">;
|
|
157
|
-
readonly credential: {
|
|
158
|
-
readonly _tag: "InstallationCredential";
|
|
159
|
-
readonly token: import("effect/Redacted").Redacted<string>;
|
|
160
|
-
} | {
|
|
161
|
-
readonly _tag: "AnonymousCredential";
|
|
162
|
-
};
|
|
163
|
-
readonly expiresAt: import("effect/DateTime").Utc;
|
|
164
|
-
}>, Schema.Struct<{
|
|
165
|
-
readonly worker: Schema.brand<Schema.String, "WorkerName">;
|
|
166
|
-
readonly repository: Schema.brand<Schema.String, "RepositoryName">;
|
|
167
|
-
readonly credential: Schema.Union<readonly [typeof import("../attachment/attachment.ts").InstallationCredential, typeof import("../attachment/attachment.ts").AnonymousCredential]>;
|
|
168
|
-
readonly expiresAt: Schema.DateTimeUtc;
|
|
169
|
-
}>, never, never>>, never, Schema.toCodecJson<typeof Attached>, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachmentAlreadyExpired, {
|
|
107
|
+
}>>, never, never, never, Schema.toCodecJson<typeof Detached>, never, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"list", "GET", "/v1/attachments", never, never, never, never, Schema.toCodecJson<typeof AttachmentList>, never, AdminAuthorization, never> | HttpApiEndpoint.HttpApiEndpoint<"renew", "PUT", "/v1/attachments/:id", Schema.toCodecStringTree<Schema.Struct<{
|
|
108
|
+
id: Schema.brand<Schema.String, "AttachmentId">;
|
|
109
|
+
}>>, never, Schema.toCodecJson<typeof AttachRequest>, never, Schema.toCodecJson<typeof Attached>, Schema.toCodecJson<Schema.decodeTo<Schema.declareConstructor<AttachmentAlreadyExpired, {
|
|
170
110
|
readonly _tag: "AttachmentAlreadyExpired";
|
|
171
111
|
readonly worker: string;
|
|
172
112
|
readonly repository: string;
|
package/dist/admin/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,eAAe,EAChB,MAAM,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,eAAe,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,UAAU,EACf,wBAAwB,EACxB,kBAAkB,EAGnB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;;;;;;;AAExE,mGAAmG;AACnG,qBAAa,aAAc,SAAQ,kBAKjC;CAAG;;;;;;;;AAEL,uCAAuC;AACvC,qBAAa,iBAAkB,SAAQ,sBAQrC;IACA,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,iBAAiB,CAQ/D;CACF;;;;;AAED;;;GAGG;AACH,qBAAa,QAAS,SAAQ,aAG5B;CAAG;;;;AAEL,8EAA8E;AAC9E,qBAAa,QAAS,SAAQ,aAE5B;CAAG;;;;AAEL,qBAAa,cAAe,SAAQ,mBAElC;CAAG;;;;;AAEL,yGAAyG;AACzG,qBAAa,kBAAmB,SAAQ,uBAKtC;CAAG;;;;AAEL,0HAA0H;AAC1H,qBAAa,eAAgB,SAAQ,oBAInC;IACA,MAAM,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,YAAY,GAAG,eAAe,CAWzE;CACF;;;;;;;;;;;;;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,uBAGvC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iRA4B3B,CAAC"}
|
package/dist/admin/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RepositoryName } from "@usine/core/repository";
|
|
2
2
|
import { GatewayCredentialKind, WorkerName } from "@usine/core/worker";
|
|
3
3
|
import { Option, Schema } from "effect";
|
|
4
|
-
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity, } from "effect/
|
|
4
|
+
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity, } from "effect/http-api";
|
|
5
5
|
import { AttachmentAlreadyExpired, AttachmentConflict, AttachmentCredential, AttachmentId, } from "../attachment/attachment.js";
|
|
6
6
|
import { CallerToken } from "../caller/caller.js";
|
|
7
7
|
import { credentialKind, LlmProviderName, } from "../llm/llm.js";
|
|
@@ -75,7 +75,6 @@ export class LlmProviderList extends Schema.Class("@usine/gateway/LlmProviderLis
|
|
|
75
75
|
*/
|
|
76
76
|
export class AdminAuthorization extends HttpApiMiddleware.Service()("@usine/gateway/AdminAuthorization", { error: HttpApiError.Unauthorized, security: { bearer: HttpApiSecurity.bearer } }) {
|
|
77
77
|
}
|
|
78
|
-
const strict = { parseOptions: { onExcessProperty: "error" } };
|
|
79
78
|
/**
|
|
80
79
|
* The admin API the executor uses to attach, renew, and detach a Worker VM's GitHub credential
|
|
81
80
|
* for an Attempt, and to read the Gateway's LLM provider credential kinds (ADR 0013).
|
|
@@ -84,12 +83,12 @@ const strict = { parseOptions: { onExcessProperty: "error" } };
|
|
|
84
83
|
*/
|
|
85
84
|
export const GatewayAdminApi = HttpApi.make("GatewayAdminApi").add(HttpApiGroup.make("attachments")
|
|
86
85
|
.add(HttpApiEndpoint.post("attach", "/v1/attachments", {
|
|
87
|
-
payload: AttachRequest
|
|
86
|
+
payload: AttachRequest,
|
|
88
87
|
success: Attached,
|
|
89
88
|
error: AttachmentAlreadyExpired.annotate({ httpApiStatus: 422 }),
|
|
90
89
|
}), HttpApiEndpoint.put("renew", "/v1/attachments/:id", {
|
|
91
90
|
params: { id: AttachmentId },
|
|
92
|
-
payload: AttachRequest
|
|
91
|
+
payload: AttachRequest,
|
|
93
92
|
success: Attached,
|
|
94
93
|
error: [
|
|
95
94
|
AttachmentAlreadyExpired.annotate({ httpApiStatus: 422 }),
|
|
@@ -99,6 +98,7 @@ export const GatewayAdminApi = HttpApi.make("GatewayAdminApi").add(HttpApiGroup.
|
|
|
99
98
|
params: { id: AttachmentId },
|
|
100
99
|
success: Detached,
|
|
101
100
|
}), HttpApiEndpoint.get("list", "/v1/attachments", { success: AttachmentList }))
|
|
101
|
+
.annotate(HttpApi.PayloadParseOptions, { onExcessProperty: "error" })
|
|
102
102
|
.middleware(AdminAuthorization), HttpApiGroup.make("llm")
|
|
103
103
|
.add(HttpApiEndpoint.get("providers", "/v1/providers", { success: LlmProviderList }))
|
|
104
104
|
.middleware(AdminAuthorization));
|
package/dist/admin/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,eAAe,GAChB,MAAM,
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"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"}
|
package/dist/admin/server.d.ts
CHANGED
|
@@ -6,5 +6,5 @@ import { GatewaySettings, SettingsInForce } from "../settings.ts";
|
|
|
6
6
|
* The admin API routes for an `HttpRouter`. Bind them to a loopback or private address; the
|
|
7
7
|
* executor connects through SSH.
|
|
8
8
|
*/
|
|
9
|
-
export declare const adminRoutes: Layer.Layer<never, never, import("effect/FileSystem").FileSystem | GatewaySettings | import("effect/
|
|
9
|
+
export declare const adminRoutes: Layer.Layer<never, never, import("effect/FileSystem").FileSystem | GatewaySettings | import("effect/http/Etag").Generator | import("effect/http/HttpPlatform").HttpPlatform | import("effect/http/HttpRouter").HttpRouter | import("effect/Path").Path | import("effect/http/HttpRouter").Request<"Requires", Attachments> | import("effect/http/HttpRouter").Request<"Requires", CallerTokens> | import("effect/http/HttpRouter").Request<"Requires", SettingsInForce>>;
|
|
10
10
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAY,MAAM,QAAQ,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAgB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA8GlE;;;GAGG;AACH,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAY,MAAM,QAAQ,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAgB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA8GlE;;;GAGG;AACH,eAAO,MAAM,WAAW,0cAGvB,CAAC"}
|
package/dist/admin/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Layer, Redacted } from "effect";
|
|
2
|
-
import { HttpApiBuilder, HttpApiError } from "effect/
|
|
2
|
+
import { HttpApiBuilder, HttpApiError } from "effect/http-api";
|
|
3
3
|
import { Attachment, newAttachmentId } from "../attachment/attachment.js";
|
|
4
4
|
import { Attachments } from "../attachment/store.js";
|
|
5
5
|
import { CallerClaims, CallerTokens } from "../caller/caller.js";
|
package/dist/admin/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RepositoryName } from "@usine/core/repository";
|
|
1
2
|
import { DateTime, Effect, Schema } from "effect";
|
|
2
3
|
/**
|
|
3
4
|
* The ID of an attachment, unchanged by renewals. A new attach for the same Worker and
|
|
@@ -42,9 +43,15 @@ export declare class Attachment extends Attachment_base {
|
|
|
42
43
|
}
|
|
43
44
|
declare const LiveAttachment_base: Schema.Class<LiveAttachment, Schema.TaggedStruct<"LiveAttachment", {
|
|
44
45
|
readonly attachment: typeof Attachment;
|
|
46
|
+
readonly newNames: Schema.$Array<Schema.brand<Schema.String, "RepositoryName">>;
|
|
45
47
|
}>, {}>;
|
|
46
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* A lookup result for an active attachment. `newNames` are the names GitHub's permanent
|
|
50
|
+
* redirects gave the attached repository after a rename or transfer.
|
|
51
|
+
*/
|
|
47
52
|
export declare class LiveAttachment extends LiveAttachment_base {
|
|
53
|
+
/** Returns whether `repository` is the attached repository, by its attached name or a new name. */
|
|
54
|
+
serves(repository: RepositoryName): boolean;
|
|
48
55
|
}
|
|
49
56
|
declare const ExpiredAttachment_base: Schema.Class<ExpiredAttachment, Schema.TaggedStruct<"ExpiredAttachment", {
|
|
50
57
|
readonly expiredAt: Schema.DateTimeUtc;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../../src/attachment/attachment.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../../src/attachment/attachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAEjF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGlD;;;GAGG;AACH,eAAO,MAAM,YAAY,6CAEW,CAAC;AACrC,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAC;AAEpD,kGAAkG;AAClG,eAAO,MAAM,eAAe,oFAE3B,CAAC;;;;AAEF,yFAAyF;AACzF,qBAAa,sBAAuB,SAAQ,2BAG3C;CAAG;;AAEJ;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,wBAGxC;CAAG;AAEJ,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,oFAA8D,CAAC;AAChG,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAC;;;;;;;;AAEpE;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,eAM9B;IACA,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAG,OAAO,CAEnC;CACF;;;;;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,mBAGlC;IACA,mGAAmG;IACnG,MAAM,CAAC,UAAU,EAAE,cAAc,GAAG,OAAO,CAK1C;CACF;;;;AAED,6EAA6E;AAC7E,qBAAa,iBAAkB,SAAQ,sBAGtC;CAAG;;AAEJ,mHAAmH;AACnH,qBAAa,iBAAkB,SAAQ,sBAGtC;CAAG;AAEJ,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;;;;;;AAEtF,6DAA6D;AAC7D,qBAAa,wBAAyB,SAAQ,6BAG7C;CAAG;;;;;;AAEJ,mGAAmG;AACnG,qBAAa,kBAAmB,SAAQ,uBAGvC;CAAG"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { RepositoryName } from "@usine/core/repository";
|
|
1
|
+
import { canonicalRepositoryName, RepositoryName } from "@usine/core/repository";
|
|
2
2
|
import { WorkerName } from "@usine/core/worker";
|
|
3
|
-
import { DateTime, Effect,
|
|
3
|
+
import { DateTime, Effect, Schema } from "effect";
|
|
4
|
+
import { Base64Url } from "effect/encoding";
|
|
4
5
|
/**
|
|
5
6
|
* The ID of an attachment, unchanged by renewals. A new attach for the same Worker and
|
|
6
7
|
* repository gets a new ID, so an earlier caller token is never accepted again.
|
|
7
8
|
*/
|
|
8
9
|
export const AttachmentId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/u, { message: "Expected a 22-character attachment ID" })).pipe(Schema.brand("AttachmentId"));
|
|
9
10
|
/** Returns a new attachment ID of 128 random bits from the platform's cryptographic generator. */
|
|
10
|
-
export const newAttachmentId = Effect.sync(() => AttachmentId.make(
|
|
11
|
+
export const newAttachmentId = Effect.sync(() => AttachmentId.make(Base64Url.encode(crypto.getRandomValues(new Uint8Array(16)))));
|
|
11
12
|
/** A GitHub App installation token added to the Worker's requests for the repository. */
|
|
12
13
|
export class InstallationCredential extends Schema.TaggedClass()("InstallationCredential", { token: Schema.Redacted(Schema.NonEmptyString) }) {
|
|
13
14
|
}
|
|
@@ -37,10 +38,19 @@ export class Attachment extends Schema.Class("@usine/gateway/Attachment")({
|
|
|
37
38
|
return DateTime.isLessThan(now, this.expiresAt);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A lookup result for an active attachment. `newNames` are the names GitHub's permanent
|
|
43
|
+
* redirects gave the attached repository after a rename or transfer.
|
|
44
|
+
*/
|
|
41
45
|
export class LiveAttachment extends Schema.TaggedClass()("LiveAttachment", {
|
|
42
46
|
attachment: Attachment,
|
|
47
|
+
newNames: Schema.Array(RepositoryName),
|
|
43
48
|
}) {
|
|
49
|
+
/** Returns whether `repository` is the attached repository, by its attached name or a new name. */
|
|
50
|
+
serves(repository) {
|
|
51
|
+
const wanted = canonicalRepositoryName(repository);
|
|
52
|
+
return [this.attachment.repository, ...this.newNames].some((name) => canonicalRepositoryName(name) === wanted);
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
/** A lookup result for an attachment whose token expired without renewal. */
|
|
46
56
|
export class ExpiredAttachment extends Schema.TaggedClass()("ExpiredAttachment", { expiredAt: Schema.DateTimeUtc }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../src/attachment/attachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../src/attachment/attachment.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RepositoryName } from "@usine/core/repository";
|
|
1
2
|
import { Context, Effect, Layer } from "effect";
|
|
2
3
|
import { type Attachment, AttachmentAlreadyExpired, AttachmentConflict, type AttachmentId, type AttachmentLookup } from "./attachment.ts";
|
|
3
4
|
declare const Attachments_base: Context.ServiceClass<Attachments, "@usine/gateway/Attachments", {
|
|
@@ -12,6 +13,12 @@ declare const Attachments_base: Context.ServiceClass<Attachments, "@usine/gatewa
|
|
|
12
13
|
readonly list: () => Effect.Effect<ReadonlyArray<Attachment>>;
|
|
13
14
|
/** Returns whether the attachment is active, expired, or missing. */
|
|
14
15
|
readonly find: (id: AttachmentId) => Effect.Effect<AttachmentLookup>;
|
|
16
|
+
/**
|
|
17
|
+
* Records `name` as the attached repository's new name, from GitHub's permanent redirect of a
|
|
18
|
+
* request for it, so the attachment serves paths that name it until detach or expiry. Does
|
|
19
|
+
* nothing if the attachment isn't live or already serves `name`.
|
|
20
|
+
*/
|
|
21
|
+
readonly learnNewName: (id: AttachmentId, name: RepositoryName) => Effect.Effect<void>;
|
|
15
22
|
/** Removes expired attachments and returns the number removed. */
|
|
16
23
|
readonly sweep: () => Effect.Effect<number>;
|
|
17
24
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/attachment/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/attachment/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACtF,OAAO,EAAE,OAAO,EAAkB,MAAM,EAAE,KAAK,EAAc,MAAM,QAAQ,CAAC;AAC5E,OAAO,EACL,KAAK,UAAU,EACf,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAItB,MAAM,iBAAiB,CAAC;;IA4BrB;;;OAGG;qBACc,CACf,UAAU,EAAE,UAAU,KACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,wBAAwB,GAAG,kBAAkB,CAAC;IACvE,kEAAkE;qBACjD,CAAC,EAAE,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;IAC7D,6DAA6D;mBAC9C,MAAM,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7D,qEAAqE;mBACtD,CAAC,EAAE,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC;IACpE;;;;OAIG;2BACoB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;IACtF,kEAAkE;oBAClD,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;;AA3B/C;;;GAGG;AACH,qBAAa,WAAY,SAAQ,gBAyBA;IAC/B,gGAAgG;IAChG,MAAM,CAAC,QAAQ,CAAC,KAAK,yCA4InB;CACH"}
|