@hixumo/hive 0.0.0-stage → 0.0.1
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 +24 -0
- package/README.md +320 -2
- package/dist/api-address.js +24 -0
- package/dist/api.js +164 -0
- package/dist/browser.js +47 -0
- package/dist/build-info.js +3 -0
- package/dist/callers.js +89 -0
- package/dist/choose.js +137 -0
- package/dist/cli.js +10 -0
- package/dist/commands/link.js +79 -0
- package/dist/commands/login.js +69 -0
- package/dist/commands/logout.js +54 -0
- package/dist/commands/pull.js +81 -0
- package/dist/commands/run.js +82 -0
- package/dist/commands/sessions.js +94 -0
- package/dist/commands/whoami.js +39 -0
- package/dist/config.js +60 -0
- package/dist/context.js +56 -0
- package/dist/credentials.js +116 -0
- package/dist/device-flow.js +93 -0
- package/dist/dotenv.js +51 -0
- package/dist/environments.js +40 -0
- package/dist/files.js +28 -0
- package/dist/github-actions.js +142 -0
- package/dist/gitignore.js +169 -0
- package/dist/link-file.js +67 -0
- package/dist/main.js +243 -0
- package/dist/output.js +53 -0
- package/dist/time.js +42 -0
- package/dist/values.js +111 -0
- package/dist/version.js +6 -0
- package/package.json +29 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Hixumo Hive CLI
|
|
2
|
+
Copyright (c) 2026 Hixumo. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This software (the Hixumo Hive command-line tool, published on npm as @hixumo/hive) is made
|
|
5
|
+
available by Hixumo for one purpose: to work with the Hixumo Hive service.
|
|
6
|
+
|
|
7
|
+
You may install and run this software, unchanged, on any computer, server or build system
|
|
8
|
+
(including continuous integration), to use the Hixumo Hive service.
|
|
9
|
+
|
|
10
|
+
You may not, without Hixumo's written permission:
|
|
11
|
+
|
|
12
|
+
1. copy, modify, adapt or translate this software, or make works based on it;
|
|
13
|
+
2. sell, rent, sublicense, redistribute or publish this software, in whole or in part,
|
|
14
|
+
other than by pointing to its official package on npm;
|
|
15
|
+
3. remove or change this notice or any notice of ownership in the software.
|
|
16
|
+
|
|
17
|
+
Hixumo keeps all rights in the software that this licence does not expressly grant. Your use
|
|
18
|
+
of the Hixumo Hive service itself is governed by its own terms.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
21
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
22
|
+
NONINFRINGEMENT. IN NO EVENT SHALL HIXUMO BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
23
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
24
|
+
WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,321 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hixumo/hive
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Hixumo Hive CLI: run a project with its environment's values from Hixumo Hive, without a
|
|
4
|
+
`.env` file. Sign in once per computer, link a folder to a project, and `hive run` the rest. In
|
|
5
|
+
GitHub Actions, GitHub vouches for the run and no token is kept anywhere; on other servers and in
|
|
6
|
+
other CI, an access token in `HIVE_TOKEN` does the same with no sign-in.
|
|
7
|
+
|
|
8
|
+
Needs Node 22 or newer.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Nothing to install:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx @hixumo/hive login
|
|
16
|
+
npx @hixumo/hive run -- npm run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or keep it in a project (`npm install --save-dev @hixumo/hive`, then `npx hive …`), or for every
|
|
20
|
+
folder (`npm install --global @hixumo/hive`, then `hive …`). The examples below say `hive`.
|
|
21
|
+
|
|
22
|
+
## On a laptop
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
hive login # once per computer: a code, the browser, Approve
|
|
26
|
+
cd client-a-app
|
|
27
|
+
hive link # pick space → project → environment; writes .hive.json
|
|
28
|
+
hive run -- npm run dev # this folder's values, no .env file
|
|
29
|
+
hive run --env staging -- npm test
|
|
30
|
+
hive pull # a .env file, when a tool needs one (asks first)
|
|
31
|
+
hive whoami
|
|
32
|
+
hive logout
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
### `hive login [--no-browser]`
|
|
38
|
+
|
|
39
|
+
Signs this computer in, as `gh auth login` does: it shows a one-time code (`WDJB-MJHT`) and opens
|
|
40
|
+
the Hive website, where you check the code matches and approve ("Confirm it is you" first).
|
|
41
|
+
`--no-browser` only prints the address, for a computer without one.
|
|
42
|
+
|
|
43
|
+
One sign-in per computer for each Hive (each Hive API address). Signed in to this Hive already,
|
|
44
|
+
it says as whom and asks before replacing it (the replaced one is ended at Hive); a sign-in to
|
|
45
|
+
another Hive is left as it is. It lasts until `hive logout`, until it is ended from another
|
|
46
|
+
computer or the website, or until it is 90 days unused. It needs a terminal: on servers, use
|
|
47
|
+
`HIVE_TOKEN`.
|
|
48
|
+
|
|
49
|
+
### `hive logout`
|
|
50
|
+
|
|
51
|
+
Ends this computer's sign-in to this Hive at Hive, then forgets it here. When Hive cannot be
|
|
52
|
+
reached it is still forgotten here, and the CLI says so: end it later with `hive sessions revoke`
|
|
53
|
+
from another computer, or on the website's "Your sign-ins" page. When the system keychain will
|
|
54
|
+
not let it be removed, the CLI says that instead, with the entry to remove by hand, and exits 1.
|
|
55
|
+
|
|
56
|
+
### `hive whoami [--env <name>]`
|
|
57
|
+
|
|
58
|
+
The account signed in on this computer, and the Hive it talks to. With `HIVE_TOKEN` set: the
|
|
59
|
+
token's space, project and environment, and when it expires. In GitHub Actions: the repository and
|
|
60
|
+
what its run may read, as in "GitHub Actions (hixumo/hixumo-backend) reads production of
|
|
61
|
+
hixumo-backend, in Hixumo." (the access it takes for this is ended at once). There `--env` names
|
|
62
|
+
the environment to check, else `HIVE_ENV`, else `.hive.json`; elsewhere it is not used.
|
|
63
|
+
|
|
64
|
+
### `hive link [--project <id>] [--env <name>] [--yes]`
|
|
65
|
+
|
|
66
|
+
Pick the space, the project and the environment with the arrow keys (a single choice is taken
|
|
67
|
+
without asking). Your personal space comes first, its name followed by "(personal)", then every
|
|
68
|
+
organization you are in by name, with your role there (owner, admin or member) dimmed after it; a
|
|
69
|
+
paused space is not offered. Then `.hive.json` is written in this folder:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"project": "7f3a9c2e-1b2c-4d5e-8f90-123456789abc",
|
|
74
|
+
"environment": "development"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It holds no secret: a project id opens nothing by itself. Commit it, so everyone on the project
|
|
79
|
+
runs with the same project. `environment` is the default environment's name. A folder already
|
|
80
|
+
linked is replaced only after you say yes; `--yes` replaces it without asking, and is needed for
|
|
81
|
+
that without a terminal. With both `--project` and `--env` it asks nothing.
|
|
82
|
+
|
|
83
|
+
### `hive run [--env <name>] -- <command…>`
|
|
84
|
+
|
|
85
|
+
Reads the values and starts the command with them added to its environment. Hive's values win
|
|
86
|
+
over the shell's. `HIVE_TOKEN` is taken out of the command's environment: the command gets the
|
|
87
|
+
values, not the key to them. The command gets the terminal as it is (its output is never touched;
|
|
88
|
+
the CLI's own messages go to stderr), Ctrl+C and other signals reach it once (at a terminal,
|
|
89
|
+
Ctrl+C comes from the terminal itself, so the CLI does not send it again), and `hive run` exits
|
|
90
|
+
with its exit code (130 when it stopped on Ctrl+C). No file is written.
|
|
91
|
+
|
|
92
|
+
Everything after `--` is the command, its own options included: `hive run -- npm test -- --watch`.
|
|
93
|
+
|
|
94
|
+
### `hive pull [--env <name>] [--out <file>] [--yes]`
|
|
95
|
+
|
|
96
|
+
Writes the values as a `.env` file (`--out`, default `.env` in this folder), readable only by you
|
|
97
|
+
(mode 0600), in the same form as the website's "Copy as .env". It asks before replacing a file,
|
|
98
|
+
and when no `.gitignore` line covers the file it warns and offers to add one (it reads and writes
|
|
99
|
+
`.gitignore` files only; it never runs git). The line goes beside `.hive.json`, or in the
|
|
100
|
+
repository root's `.gitignore` when `.hive.json` is above the repository, where git would not
|
|
101
|
+
read it. Without a terminal it needs `--yes`, which answers yes to both.
|
|
102
|
+
|
|
103
|
+
Prefer `hive run`: a `.env` file is one more copy of the values.
|
|
104
|
+
|
|
105
|
+
### `hive sessions [--json]`
|
|
106
|
+
|
|
107
|
+
Every sign-in of yours, numbered: the CLI on each computer and the website in each browser.
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
CLI sign-ins (2)
|
|
111
|
+
1 MacBook-Pro (this computer) made 2 Nov last used just now from 39.45.10.21
|
|
112
|
+
2 office-pc made 28 Oct last used 3 days ago from 182.180.4.9
|
|
113
|
+
|
|
114
|
+
Website sign-ins (1)
|
|
115
|
+
3 Chrome on macOS started 2 hours ago ends in 29 days from 39.45.10.21
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`--json` prints the same with each sign-in's id, for scripts.
|
|
119
|
+
|
|
120
|
+
### `hive sessions revoke <number or id> [--yes]`
|
|
121
|
+
|
|
122
|
+
Ends one of them, after showing which and asking (`--yes` skips the question, and is needed
|
|
123
|
+
without a terminal). The number is the one `hive sessions` showed; the id from `--json` works as
|
|
124
|
+
well. A CLI sign-in is refused from its next request; a website one is signed out, and its
|
|
125
|
+
browser is sent to sign in. This computer's own sign-in is ended with `hive logout`.
|
|
126
|
+
|
|
127
|
+
Ending a sign-in needs "Confirm it is you" within the last 10 minutes, so a stolen CLI sign-in
|
|
128
|
+
cannot keep its owner out. When it is older, the CLI opens the browser to confirm, then ends the
|
|
129
|
+
sign-in. Without a terminal it stops and says so: run it in a terminal, or end the sign-in on the
|
|
130
|
+
website's "Your sign-ins" page.
|
|
131
|
+
|
|
132
|
+
### `hive help [command]`, `hive --version`
|
|
133
|
+
|
|
134
|
+
`hive <command> --help` works too.
|
|
135
|
+
|
|
136
|
+
## Choosing what to read
|
|
137
|
+
|
|
138
|
+
- **`HIVE_TOKEN` set:** the token decides the project and the environment; any sign-in on the
|
|
139
|
+
computer is ignored, and no `.hive.json` is needed. A committed `.hive.json`'s environment is
|
|
140
|
+
ignored (it is the developers' default; a deploy's token is production's). The run stops with a
|
|
141
|
+
sentence when `.hive.json` names another project (the wrong token in this repository), or when
|
|
142
|
+
`--env` or `HIVE_ENV` names another environment than the token's. A `.hive.json` that cannot be
|
|
143
|
+
read, or is not a link file, is left out with one warning.
|
|
144
|
+
- **Else, in GitHub Actions with `permissions: id-token: write`:** GitHub vouches for the run (see
|
|
145
|
+
below). The project comes from `HIVE_PROJECT`, else the nearest `.hive.json`; the environment
|
|
146
|
+
from `--env`, else `HIVE_ENV`, else `.hive.json`.
|
|
147
|
+
- **Otherwise:** this computer's sign-in, the nearest `.hive.json` (this folder, then each one
|
|
148
|
+
above it), and the environment from `--env`, else `HIVE_ENV`, else `.hive.json`. Names match
|
|
149
|
+
ignoring case (`--env Production` finds `production`).
|
|
150
|
+
|
|
151
|
+
Not signed in and no token: "Run `hive login` first, or set HIVE_TOKEN." In GitHub Actions without
|
|
152
|
+
the permission: "Add `permissions: id-token: write` to this workflow (or job) so Hive can check who
|
|
153
|
+
is running it."
|
|
154
|
+
|
|
155
|
+
### Protected environments
|
|
156
|
+
|
|
157
|
+
Reading a protected environment (production, by default) with your own sign-in needs "Confirm it
|
|
158
|
+
is you" within the last 10 minutes, as on the website. The CLI opens the browser for it and
|
|
159
|
+
carries on by itself. Without a terminal it stops: "production is protected. Use an access token
|
|
160
|
+
on servers, or run this in a terminal." An access token never asks.
|
|
161
|
+
|
|
162
|
+
## In GitHub Actions: no token at all
|
|
163
|
+
|
|
164
|
+
GitHub signs a short token for each run that says which repository, branch or tag, GitHub
|
|
165
|
+
environment and workflow it is (OpenID Connect, as GitHub does for AWS). Hive checks it against
|
|
166
|
+
the project's GitHub Actions rules, and a run one of them allows gets a workflow access: one
|
|
167
|
+
environment, read only, 5 minutes at most. The CLI ends it as soon as the values are read, before
|
|
168
|
+
your command starts. Nothing is stored in GitHub, and neither GitHub's token nor the access is
|
|
169
|
+
ever printed or given to the command.
|
|
170
|
+
|
|
171
|
+
1. Once per GitHub account: the space's **Connections** tab on the Hive website →
|
|
172
|
+
**Connect GitHub account**.
|
|
173
|
+
2. Per environment: the project's **Machine access** tab → GitHub Actions → **Add rule** (the
|
|
174
|
+
repository, its GitHub environment, a branch or tag, and optionally the workflow file).
|
|
175
|
+
3. The workflow:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
name: Deploy
|
|
179
|
+
on:
|
|
180
|
+
release:
|
|
181
|
+
types: [published]
|
|
182
|
+
permissions:
|
|
183
|
+
id-token: write
|
|
184
|
+
contents: read
|
|
185
|
+
jobs:
|
|
186
|
+
deploy:
|
|
187
|
+
runs-on: ubuntu-latest
|
|
188
|
+
environment: production
|
|
189
|
+
steps:
|
|
190
|
+
- uses: actions/checkout@v4
|
|
191
|
+
- run: npx @hixumo/hive run --env production -- npm run build
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The project comes from the committed `.hive.json` (or `HIVE_PROJECT`, the project's id, set in
|
|
195
|
+
the workflow). `npx @hixumo/hive whoami --env production` in a step says what the run may read.
|
|
196
|
+
|
|
197
|
+
Only `push`, `release`, `workflow_dispatch`, `schedule` and `deployment` runs are let in; pull
|
|
198
|
+
requests never are. A run no rule allows is refused with what GitHub said about it:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
Error: No GitHub Actions rule of production allows this. Add one in the project's Machine access tab.
|
|
202
|
+
This run, as GitHub describes it:
|
|
203
|
+
repository hixumo/hixumo-backend
|
|
204
|
+
ref refs/heads/main
|
|
205
|
+
environment production
|
|
206
|
+
event push
|
|
207
|
+
workflow .github/workflows/deploy.yml
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`HIVE_TOKEN`, when it is set, is used instead.
|
|
211
|
+
|
|
212
|
+
## On other servers and in other CI: `HIVE_TOKEN`
|
|
213
|
+
|
|
214
|
+
Make an access token in the project's Machine access tab on the Hive website (one project, one
|
|
215
|
+
environment, read only), and keep it as a secret. Protected environments (production) have
|
|
216
|
+
access tokens off until an Owner or Admin turns them on there.
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
- run: npx @hixumo/hive run -- npm run build
|
|
220
|
+
env:
|
|
221
|
+
HIVE_TOKEN: ${{ secrets.HIVE_TOKEN }}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Any server: `HIVE_TOKEN=hive_st_… npx @hixumo/hive run -- node server.js`.
|
|
225
|
+
`hive whoami` says what a token reads. A token that is revoked or has expired is refused with a
|
|
226
|
+
sentence that says which; the CLI never prints the token.
|
|
227
|
+
|
|
228
|
+
## Where things are kept
|
|
229
|
+
|
|
230
|
+
| What | Where |
|
|
231
|
+
|---|---|
|
|
232
|
+
| The sign-in's token | The system keychain (macOS Keychain, Windows Credential Manager, the Linux Secret Service), service `hixumo-hive`, account the Hive API's address (like `https://hive-api.example.com`): one entry per Hive. Where there is none, `credentials.json` in the config folder, `{ "apiUrl", "token" }`, readable only by you, with a warning when it is saved; it keeps one sign-in, so signing in to another Hive there replaces it (and ends it at its Hive). |
|
|
233
|
+
| The account's name and email, and which Hive it is for | `config.json` in the config folder. Never the token. |
|
|
234
|
+
| The config folder | `$XDG_CONFIG_HOME/hixumo-hive`, else `~/.config/hixumo-hive`; `%APPDATA%\hixumo-hive` on Windows. |
|
|
235
|
+
| A folder's project | `.hive.json`, committed. |
|
|
236
|
+
|
|
237
|
+
Each token is saved with the Hive API address it was made with, and only ever sent there: a
|
|
238
|
+
`HIVE_API_URL` pointing elsewhere later finds no sign-in until you `hive login` there, and signing
|
|
239
|
+
in to one Hive (from any config folder) never replaces another Hive's sign-in in the keychain.
|
|
240
|
+
|
|
241
|
+
## Settings
|
|
242
|
+
|
|
243
|
+
| Variable | Does |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `HIVE_TOKEN` | An access token (`hive_st_…`): use it instead of the sign-in. |
|
|
246
|
+
| `HIVE_ENV` | The environment, for one shell (`--env` wins over it). |
|
|
247
|
+
| `HIVE_PROJECT` | GitHub Actions only: the project's id, instead of the one in `.hive.json`. |
|
|
248
|
+
| `HIVE_API_URL` | The Hive API's address (an origin, https), instead of the one built in. |
|
|
249
|
+
| `HIVE_CONFIG_DIR` | Another config folder. |
|
|
250
|
+
| `HIVE_KEYRING=off` | Do not use the keychain: `credentials.json` only. |
|
|
251
|
+
| `NO_COLOR` | No colour. (Colour is only ever used on a terminal.) |
|
|
252
|
+
|
|
253
|
+
Exit codes: 0 done, 1 refused or failed (or answered "no"), 2 wrong usage, 130 stopped with
|
|
254
|
+
Ctrl+C. `hive run` exits with its command's own.
|
|
255
|
+
|
|
256
|
+
Messages go to stderr; what you asked for (`whoami`, `sessions`) goes to stdout. Nothing the CLI
|
|
257
|
+
prints ever holds a value or a token.
|
|
258
|
+
|
|
259
|
+
## Developing
|
|
260
|
+
|
|
261
|
+
Building and testing need Node 22.18 or newer (the tests run the TypeScript sources directly).
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
npm ci
|
|
265
|
+
HIVE_API_URL=http://localhost:8200 npm run build # dist/, for the local Hive API
|
|
266
|
+
npm run typecheck
|
|
267
|
+
npm test
|
|
268
|
+
node dist/cli.js help
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
- The address built in comes from `HIVE_API_URL` in the environment, else from this repository's
|
|
272
|
+
`.env` (see `.env.example`). `scripts/write-build-info.mjs` writes it to `src/build-info.ts`
|
|
273
|
+
(not committed) and refuses an address that is not https, except localhost. No address is
|
|
274
|
+
written in the source.
|
|
275
|
+
- The tests play the Hive API with a small server of their own (`tests/fake-hive.ts`), run the
|
|
276
|
+
commands in their process and the real CLI as a child, and keep every file they write in a
|
|
277
|
+
temporary folder: `HIVE_TEST_TMP` when it is set, else the system's. They set
|
|
278
|
+
`HIVE_CONFIG_DIR` and `HIVE_KEYRING=off`, so nothing reaches the home folder or the keychain.
|
|
279
|
+
For a local run against a real Hive, do the same:
|
|
280
|
+
`HIVE_CONFIG_DIR=<a scratch folder> HIVE_KEYRING=off HIVE_API_URL=http://localhost:8200 node dist/cli.js login`.
|
|
281
|
+
|
|
282
|
+
## Releasing
|
|
283
|
+
|
|
284
|
+
The CLI repository's GitHub environment `production` holds the variable `HIVE_API_URL`, the Hive
|
|
285
|
+
API's public address; `publish.yml` runs in that environment and builds with it. Pull requests'
|
|
286
|
+
CI builds with a placeholder (`https://hive-api.example.com`): it never gets the environment. `prepublishOnly` refuses to publish a build made for anything but a
|
|
287
|
+
public https address.
|
|
288
|
+
|
|
289
|
+
A push to `main` that changes the CLI (`src/`, `scripts/`, `package.json`, the lock file,
|
|
290
|
+
`tsconfig.build.json` or the workflow) publishes the next version by itself, then tags it
|
|
291
|
+
`v<version>` and makes its GitHub Release with the changes since the last one. Nobody edits a
|
|
292
|
+
version number: each digit goes from 0 to 9, then the one before it goes up
|
|
293
|
+
(`0.2.0 → 0.2.1 → … → 0.2.9 → 0.3.0 → … → 0.9.9 → 1.0.0`, `scripts/next-version.mjs`). The
|
|
294
|
+
first publish is `package.json`'s version as it is; for a jump (like `1.0.0`), raise
|
|
295
|
+
`package.json` past the newest tag by hand. The version is set on the runner only, so
|
|
296
|
+
`package.json` in the repository keeps its first version and nothing is pushed back to `main`.
|
|
297
|
+
A push that only changes docs publishes nothing.
|
|
298
|
+
|
|
299
|
+
1. Create the `hixumo` organization on npmjs.com.
|
|
300
|
+
2. The first version (npm sets up trusted publishing only on a package that already exists):
|
|
301
|
+
on npmjs.com make a granular access token (Bypass two-factor authentication on, since a
|
|
302
|
+
workflow can't type a 2FA code; packages and scopes: read and write; organizations: no
|
|
303
|
+
access; expiring in a few days), and put it in this repository's `production`
|
|
304
|
+
environment as the secret `NPM_TOKEN`. Push to `main` (or run the workflow from the Actions
|
|
305
|
+
tab): it publishes `package.json`'s version with it.
|
|
306
|
+
3. On the package's settings page on npmjs.com, add this repository's `publish.yml` as its
|
|
307
|
+
trusted publisher, with the environment `production`, and allow direct publishing
|
|
308
|
+
(`npm publish`): the workflow publishes directly. npm drops a configuration that hasn't
|
|
309
|
+
published within 2 days, so push (or re-run the workflow) soon after. Then delete the `NPM_TOKEN` secret,
|
|
310
|
+
and the token on npmjs.com.
|
|
311
|
+
4. From then on: push to `main`. Each push that changes the CLI publishes the next version,
|
|
312
|
+
tags it and makes its Release. No npm token is stored anywhere.
|
|
313
|
+
|
|
314
|
+
The package is public on npm while this repository stays private. npm makes a provenance
|
|
315
|
+
statement only for a public repository, so `publish.yml` publishes without one; if the
|
|
316
|
+
repository is ever made public, add `--provenance` there.
|
|
317
|
+
|
|
318
|
+
## Licence
|
|
319
|
+
|
|
320
|
+
Not open source. The CLI may be installed and run, unchanged, to use Hixumo Hive; copying,
|
|
321
|
+
changing or redistributing it needs Hixumo's permission. See `LICENSE`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Hive API's address, as an origin (scheme, host and port); the CLI adds `/api/v1` itself.
|
|
3
|
+
* One rule for the address built in (scripts/write-build-info.mjs) and for HIVE_API_URL when the
|
|
4
|
+
* CLI runs: every request carries a token, so it only ever goes over https, except to this
|
|
5
|
+
* computer.
|
|
6
|
+
*/
|
|
7
|
+
const THIS_COMPUTER = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
8
|
+
export function apiOriginOf(address) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(address.trim());
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error("HIVE_API_URL must be the Hive API's address, like https://hive-api.example.com.");
|
|
15
|
+
}
|
|
16
|
+
const local = url.protocol === 'http:' && THIS_COMPUTER.has(url.hostname);
|
|
17
|
+
if (url.protocol !== 'https:' && !local) {
|
|
18
|
+
throw new Error('HIVE_API_URL must start with https:// (plain http only for localhost): every request carries a token.');
|
|
19
|
+
}
|
|
20
|
+
if (url.pathname !== '/' || url.search || url.hash || url.username || url.password) {
|
|
21
|
+
throw new Error(`HIVE_API_URL must be an origin, like ${url.origin}, without a path: the CLI adds /api/v1.`);
|
|
22
|
+
}
|
|
23
|
+
return url.origin;
|
|
24
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { plain } from "./output.js";
|
|
3
|
+
import { USER_AGENT } from "./version.js";
|
|
4
|
+
/** A call the API refused, or that never reached it (`status` 0). */
|
|
5
|
+
export class ApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
/** The word to act on, when the API gave one: "reauth_required", "token_invalid". */
|
|
8
|
+
code;
|
|
9
|
+
/** `github_token_invalid`: which check GitHub's token failed ("audience", "expired", …). */
|
|
10
|
+
reason;
|
|
11
|
+
/** `github_not_allowed`: the run as GitHub's token describes it. */
|
|
12
|
+
claims;
|
|
13
|
+
constructor(status, message, code, details = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ApiError';
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.reason = details.reason;
|
|
19
|
+
this.claims = details.claims;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const API_PREFIX = '/api/v1';
|
|
23
|
+
const TIMEOUT_MS = 20_000;
|
|
24
|
+
const RETRY_DELAY_MS = 500;
|
|
25
|
+
const WORKFLOW_ACCESS_PREFIX = 'hive_gh_';
|
|
26
|
+
export class HiveApi {
|
|
27
|
+
origin;
|
|
28
|
+
token;
|
|
29
|
+
send;
|
|
30
|
+
timeoutMs;
|
|
31
|
+
retryDelayMs;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.origin = options.origin;
|
|
34
|
+
this.token = options.token;
|
|
35
|
+
this.send = options.fetch ?? ((...args) => fetch(...args));
|
|
36
|
+
this.timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
|
|
37
|
+
this.retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
|
|
38
|
+
}
|
|
39
|
+
me() {
|
|
40
|
+
return this.request('GET', '/me');
|
|
41
|
+
}
|
|
42
|
+
projects(spaceId) {
|
|
43
|
+
return this.request('GET', `/spaces/${segment(spaceId)}/projects`);
|
|
44
|
+
}
|
|
45
|
+
project(projectId) {
|
|
46
|
+
return this.request('GET', `/projects/${segment(projectId)}`);
|
|
47
|
+
}
|
|
48
|
+
reveal(environmentId) {
|
|
49
|
+
return this.request('POST', `/environments/${segment(environmentId)}/reveal`);
|
|
50
|
+
}
|
|
51
|
+
tokenInfo() {
|
|
52
|
+
return this.request('GET', '/token');
|
|
53
|
+
}
|
|
54
|
+
tokenValues() {
|
|
55
|
+
return this.request('GET', '/token/values');
|
|
56
|
+
}
|
|
57
|
+
/** GitHub's OIDC token for this run, traded for a workflow access (no Hive token is sent). */
|
|
58
|
+
githubAccess(body) {
|
|
59
|
+
return this.request('POST', '/github/access', body);
|
|
60
|
+
}
|
|
61
|
+
/** Ends this workflow access at Hive (204). Only ever sent with one: never with an access token. */
|
|
62
|
+
async endToken() {
|
|
63
|
+
if (!this.token?.startsWith(WORKFLOW_ACCESS_PREFIX))
|
|
64
|
+
throw new Error('Only a workflow access is ended.');
|
|
65
|
+
await this.request('POST', '/token/end');
|
|
66
|
+
}
|
|
67
|
+
startLogin(deviceName) {
|
|
68
|
+
return this.request('POST', '/cli/login', { deviceName });
|
|
69
|
+
}
|
|
70
|
+
pollLogin(deviceCode) {
|
|
71
|
+
return this.request('POST', '/cli/login/poll', { deviceCode });
|
|
72
|
+
}
|
|
73
|
+
startConfirm() {
|
|
74
|
+
return this.request('POST', '/cli/confirm');
|
|
75
|
+
}
|
|
76
|
+
pollConfirm(deviceCode) {
|
|
77
|
+
return this.request('POST', '/cli/confirm/poll', { deviceCode });
|
|
78
|
+
}
|
|
79
|
+
logout() {
|
|
80
|
+
return this.request('POST', '/cli/logout');
|
|
81
|
+
}
|
|
82
|
+
signIns() {
|
|
83
|
+
return this.request('GET', '/me/sign-ins');
|
|
84
|
+
}
|
|
85
|
+
revokeCliSignIn(id) {
|
|
86
|
+
return this.request('POST', `/me/sign-ins/cli/${segment(id)}/revoke`);
|
|
87
|
+
}
|
|
88
|
+
endWebsiteSignIn(id) {
|
|
89
|
+
return this.request('POST', `/me/sign-ins/website/${segment(id)}/end`);
|
|
90
|
+
}
|
|
91
|
+
/** The `data` of a successful answer; an ApiError for anything else. */
|
|
92
|
+
async request(method, path, body) {
|
|
93
|
+
const text = body === undefined ? undefined : JSON.stringify(body);
|
|
94
|
+
const headers = { Accept: 'application/json', 'User-Agent': USER_AGENT };
|
|
95
|
+
if (this.token)
|
|
96
|
+
headers['X-Hive-Token'] = this.token;
|
|
97
|
+
if (text !== undefined)
|
|
98
|
+
headers['Content-Type'] = 'application/json';
|
|
99
|
+
if (method !== 'GET')
|
|
100
|
+
headers['x-amz-content-sha256'] = createHash('sha256').update(text ?? '').digest('hex');
|
|
101
|
+
const url = `${this.origin}${API_PREFIX}${path}`;
|
|
102
|
+
let response;
|
|
103
|
+
let answer;
|
|
104
|
+
for (let attempt = 1;; attempt += 1) {
|
|
105
|
+
try {
|
|
106
|
+
response = await this.send(url, {
|
|
107
|
+
method,
|
|
108
|
+
headers,
|
|
109
|
+
body: text,
|
|
110
|
+
redirect: 'manual',
|
|
111
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
112
|
+
});
|
|
113
|
+
answer = await response.text();
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (method === 'GET' && attempt === 1 && error instanceof TypeError) {
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
throw this.unreached(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) {
|
|
125
|
+
throw new ApiError(response.status, `Hive answered with a redirect, which hive never follows. Check HIVE_API_URL (${this.origin}).`);
|
|
126
|
+
}
|
|
127
|
+
// Done, with nothing to say (`POST /token/end`).
|
|
128
|
+
if (response.status === 204)
|
|
129
|
+
return undefined;
|
|
130
|
+
let envelope;
|
|
131
|
+
try {
|
|
132
|
+
envelope = JSON.parse(answer);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Not JSON: an error page from something in front of the API.
|
|
136
|
+
}
|
|
137
|
+
if (response.ok && envelope?.success === true) {
|
|
138
|
+
return envelope.data;
|
|
139
|
+
}
|
|
140
|
+
const claims = envelope?.claims;
|
|
141
|
+
throw new ApiError(response.status, messageOf(envelope?.message) ?? `Hive could not answer (${response.status}).`, typeof envelope?.code === 'string' ? envelope.code : undefined, {
|
|
142
|
+
reason: typeof envelope?.reason === 'string' ? envelope.reason : undefined,
|
|
143
|
+
claims: typeof claims === 'object' && claims !== null && !Array.isArray(claims) ? claims : undefined,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
unreached(error) {
|
|
147
|
+
if (error?.name === 'TimeoutError') {
|
|
148
|
+
return new ApiError(0, `Hive did not answer within ${this.timeoutMs / 1000} seconds.`);
|
|
149
|
+
}
|
|
150
|
+
const code = error?.cause?.code;
|
|
151
|
+
return new ApiError(0, `Could not reach Hive at ${this.origin}${typeof code === 'string' ? ` (${code})` : ''}.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** An id as one path segment: `../me` stays inside the route it was meant for. */
|
|
155
|
+
function segment(id) {
|
|
156
|
+
return encodeURIComponent(id);
|
|
157
|
+
}
|
|
158
|
+
function messageOf(message) {
|
|
159
|
+
if (typeof message === 'string' && message)
|
|
160
|
+
return plain(message);
|
|
161
|
+
if (Array.isArray(message) && message.length)
|
|
162
|
+
return plain(message.join('; '));
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export function browserCommand(url, platform) {
|
|
3
|
+
let parsed;
|
|
4
|
+
try {
|
|
5
|
+
parsed = new URL(url);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
11
|
+
return undefined;
|
|
12
|
+
// The address as the parser read it, never the text it came as: the parser drops spaces around
|
|
13
|
+
// it and line breaks and tabs inside it, which cmd would otherwise read as its own.
|
|
14
|
+
const address = parsed.href;
|
|
15
|
+
switch (platform) {
|
|
16
|
+
case 'darwin':
|
|
17
|
+
return { command: 'open', args: [address] };
|
|
18
|
+
case 'win32':
|
|
19
|
+
// `start` is part of cmd. In double quotes cmd takes & and | as text; a quote or a % (its
|
|
20
|
+
// variables) still could break out, and Hive's addresses have neither.
|
|
21
|
+
if (/["%]/.test(address))
|
|
22
|
+
return undefined;
|
|
23
|
+
return { command: 'cmd', args: ['/c', 'start', '""', `"${address}"`] };
|
|
24
|
+
default:
|
|
25
|
+
return { command: 'xdg-open', args: [address] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function openBrowser(url) {
|
|
29
|
+
const how = browserCommand(url, process.platform);
|
|
30
|
+
if (!how)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
const child = spawn(how.command, how.args, {
|
|
34
|
+
stdio: 'ignore',
|
|
35
|
+
detached: true,
|
|
36
|
+
// The quotes above are cmd's; Node must not add its own around them.
|
|
37
|
+
windowsVerbatimArguments: process.platform === 'win32',
|
|
38
|
+
});
|
|
39
|
+
child.on('error', () => {
|
|
40
|
+
// No opener here: the address is printed.
|
|
41
|
+
});
|
|
42
|
+
child.unref();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// The same.
|
|
46
|
+
}
|
|
47
|
+
}
|