@hypequery/cli 1.16.1 → 1.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -272
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -0
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +20 -34
- package/dist/commands/live-source.d.ts +22 -0
- package/dist/commands/live-source.d.ts.map +1 -0
- package/dist/commands/live-source.js +119 -0
- package/dist/commands/login.js +2 -2
- package/dist/utils/cloud-credential-store.d.ts +1 -0
- package/dist/utils/cloud-credential-store.d.ts.map +1 -1
- package/dist/utils/cloud-credential-store.js +4 -2
- package/dist/utils/cloud-deployment-access.d.ts +12 -0
- package/dist/utils/cloud-deployment-access.d.ts.map +1 -0
- package/dist/utils/cloud-deployment-access.js +37 -0
- package/dist/utils/deployment-upload.d.ts +2 -0
- package/dist/utils/deployment-upload.d.ts.map +1 -1
- package/dist/utils/deployment-upload.js +8 -0
- package/dist/utils/live-deployment.d.ts +36 -0
- package/dist/utils/live-deployment.d.ts.map +1 -0
- package/dist/utils/live-deployment.js +205 -0
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -1,302 +1,60 @@
|
|
|
1
1
|
# @hypequery/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The fastest way to start a type-safe ClickHouse analytics backend in TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@hypequery/cli` connects to ClickHouse or embedded chDB, generates schema types, scaffolds queries or semantic datasets, and runs a local API with interactive documentation. The same CLI can generate React route manifests and deploy a verified analytics bundle when you are ready.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- scaffold `analytics/` files
|
|
9
|
-
- run the local dev server with docs
|
|
10
|
-
- build and validate portable deployment contracts
|
|
11
|
-
|
|
12
|
-
## Quick Start
|
|
13
|
-
|
|
14
|
-
Run it directly:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npx @hypequery/cli init
|
|
18
|
-
npx @hypequery/cli dev
|
|
19
|
-
npx @hypequery/cli generate
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Or install it once:
|
|
7
|
+
## Start here
|
|
23
8
|
|
|
24
9
|
```bash
|
|
25
10
|
npm install -D @hypequery/cli
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Commands
|
|
29
|
-
|
|
30
|
-
### `hypequery init`
|
|
31
|
-
|
|
32
|
-
Scaffolds the standard hypequery setup.
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
11
|
npx hypequery init
|
|
12
|
+
npx hypequery dev --open
|
|
36
13
|
```
|
|
37
14
|
|
|
38
|
-
|
|
39
|
-
request-context authentication scaffolding, where to write the generated
|
|
40
|
-
files, and which API style to create. Selecting chDB replaces the remote
|
|
41
|
-
credential questions with an embedded-storage choice.
|
|
15
|
+
`init` checks the database, writes an `analytics/` project, generates types, and installs the packages used by the scaffold.
|
|
42
16
|
|
|
43
|
-
|
|
44
|
-
package manifest is found, interactive setup asks for confirmation before
|
|
45
|
-
writing files and dependency installation must be completed manually.
|
|
46
|
-
|
|
47
|
-
It will:
|
|
48
|
-
|
|
49
|
-
- connect to ClickHouse or start an embedded chDB session
|
|
50
|
-
- generate schema types when the database is available
|
|
51
|
-
- create client and query files
|
|
52
|
-
- write `.env` values for ClickHouse connections
|
|
53
|
-
- update `.gitignore`, including a project-local persistent chDB directory
|
|
54
|
-
- install scaffold dependencies, including `zod` and the selected database adapter
|
|
55
|
-
|
|
56
|
-
Options:
|
|
57
|
-
|
|
58
|
-
- `--path <path>`: output directory, default `analytics/`
|
|
59
|
-
- `--style <style>`: `queries` (default) or `datasets`
|
|
60
|
-
- `--database <type>`: `clickhouse` (default) or `chdb`
|
|
61
|
-
- `--chdb-path <path>`: persistent chDB data directory; omit for an in-memory session
|
|
62
|
-
- `--auth <mode>`: `none` (default) or `context`
|
|
63
|
-
- `--all-tables`: with `--style datasets`, scaffold every table
|
|
64
|
-
- `--tables <names>`: with `--style datasets`, scaffold these comma-separated tables
|
|
65
|
-
- `--exclude-tables <names>`: with `--style datasets`, exclude these comma-separated tables
|
|
66
|
-
- `--no-example`: skip the example query
|
|
67
|
-
- `--no-interactive`: skip prompts; ClickHouse connection details come from env vars
|
|
68
|
-
- `--force`: overwrite existing scaffold files
|
|
69
|
-
- `--skip-connection`: skip testing the selected database before scaffolding
|
|
70
|
-
|
|
71
|
-
Set `HYPEQUERY_SKIP_INSTALL=1` to skip the automatic dependency install.
|
|
72
|
-
|
|
73
|
-
To scaffold against persistent embedded chDB without server credentials:
|
|
17
|
+
For a semantic layer:
|
|
74
18
|
|
|
75
19
|
```bash
|
|
76
|
-
npx hypequery init --
|
|
20
|
+
npx hypequery init --style datasets
|
|
77
21
|
```
|
|
78
22
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Runs the local serve runtime with docs and hot reload.
|
|
23
|
+
For embedded analytics without a ClickHouse server:
|
|
82
24
|
|
|
83
25
|
```bash
|
|
84
|
-
npx hypequery
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Options:
|
|
88
|
-
|
|
89
|
-
- `--port <port>`: default `4000`
|
|
90
|
-
- `--hostname <host>`: default `localhost`
|
|
91
|
-
- `--path <path>`: analytics directory to load (`<path>/api.ts` or `<path>/queries.ts`)
|
|
92
|
-
- `--no-watch`: disable file watching
|
|
93
|
-
- `--open`: open the browser automatically
|
|
94
|
-
- `--quiet`: reduce startup output
|
|
95
|
-
|
|
96
|
-
The CLI understands TypeScript entry files directly, so `analytics/queries.ts` works without an extra runner.
|
|
97
|
-
|
|
98
|
-
### `hypequery generate`
|
|
99
|
-
|
|
100
|
-
Regenerates schema types from ClickHouse or embedded chDB.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
npx hypequery generate
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Options:
|
|
107
|
-
|
|
108
|
-
- `--output <path>`: default `analytics/schema.ts`
|
|
109
|
-
- `--path <path>`: analytics directory (derives `<path>/schema.ts`)
|
|
110
|
-
- `--tables <names>`: comma-separated table list
|
|
111
|
-
- `--database <type>`: `clickhouse` or `chdb`; chDB generation must be selected explicitly
|
|
112
|
-
- `--chdb-path <path>`: persistent chDB data directory; omit for an in-memory session
|
|
113
|
-
|
|
114
|
-
For a persistent chDB scaffold, pass the same path used by `init`:
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
npx hypequery generate --database chdb --chdb-path ./analytics.chdb
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
`hypequery generate:types` is an alias for `hypequery generate`.
|
|
121
|
-
|
|
122
|
-
### `hypequery generate:datasets`
|
|
123
|
-
|
|
124
|
-
Generates dataset (semantic layer) definitions from ClickHouse.
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
npx hypequery generate:datasets
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Options:
|
|
131
|
-
|
|
132
|
-
- `--output <path>`: default `src/datasets/generated.ts`
|
|
133
|
-
- `--path <path>`: analytics directory (derives `<path>/datasets.ts`)
|
|
134
|
-
- `--tables <names>`: comma-separated table list
|
|
135
|
-
- `--exclude-tables <names>`: comma-separated tables to exclude
|
|
136
|
-
|
|
137
|
-
### `hypequery generate:manifest`
|
|
138
|
-
|
|
139
|
-
Generates a static React hook route manifest from an exported HypeQuery API.
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
npx hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The output is the exact serializable JSON returned by `api.manifest()`, including
|
|
146
|
-
semantic keys such as `dataset:orders`.
|
|
147
|
-
|
|
148
|
-
### `hypequery deployment:build`
|
|
149
|
-
|
|
150
|
-
Builds a closed deployment bundle for an exported HypeQuery API. The bundle
|
|
151
|
-
contains canonical deployment metadata, every referenced runtime artifact, and
|
|
152
|
-
a manifest that binds their exact bytes and identities. It is the first step of
|
|
153
|
-
a deployment pipeline, and produces the input `deployment:release` binds to a
|
|
154
|
-
target.
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
npx hypequery deployment:build analytics/api.ts
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
The default output is `analytics/hypequery-deployment/`. Named Serve handlers
|
|
161
|
-
are bundled into a Node runtime artifact automatically. Dataset-only APIs do
|
|
162
|
-
not produce a runtime artifact. For a separately built Node or Python runtime,
|
|
163
|
-
provide both its expected digest and file path:
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
npx hypequery deployment:build analytics/api.ts \
|
|
167
|
-
--runtime python \
|
|
168
|
-
--runtime-artifact <sha256> \
|
|
169
|
-
--runtime-file dist/runtime.pyz
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Options:
|
|
173
|
-
|
|
174
|
-
- `--bundle-output <directory>`: default `analytics/hypequery-deployment`
|
|
175
|
-
- `--runtime <runtime>`: `node` (default) or `python`
|
|
176
|
-
- `--runtime-artifact <sha256>`: lowercase SHA-256 of a prebuilt runtime artifact
|
|
177
|
-
- `--runtime-file <path>`: bytes for the prebuilt runtime artifact
|
|
178
|
-
- `--entrypoint-prefix <prefix>`: default `queries`
|
|
179
|
-
|
|
180
|
-
The compatibility options `--output`, `--runtime-output`, and `--hash-output`
|
|
181
|
-
still emit the earlier metadata files instead of a complete bundle. They cannot
|
|
182
|
-
be combined with `--bundle-output`.
|
|
183
|
-
|
|
184
|
-
### `hypequery deployment:validate`
|
|
185
|
-
|
|
186
|
-
Verifies a complete deployment bundle before returning any contained metadata.
|
|
187
|
-
Verification rejects missing or undeclared files, symbolic links, path
|
|
188
|
-
traversal, byte-length or hash mismatches, deployment identity mismatches, and
|
|
189
|
-
runtime files not referenced by the deployment. Legacy deployment JSON files
|
|
190
|
-
are still accepted for metadata-only validation.
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
npx hypequery deployment:validate analytics/hypequery-deployment
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### `hypequery deployment:release`
|
|
197
|
-
|
|
198
|
-
Prepares a deterministic release request from a verified deployment bundle and
|
|
199
|
-
a project/environment target. This command does not upload, authorize, or
|
|
200
|
-
execute the release.
|
|
201
|
-
|
|
202
|
-
Pass the target as both flags. A half-specified target is rejected rather than
|
|
203
|
-
completed from anywhere else, so an unset shell variable fails loudly instead of
|
|
204
|
-
silently retargeting the release. The resolved target is printed alongside the
|
|
205
|
-
release identity:
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
npx hypequery deployment:release analytics/hypequery-deployment \
|
|
209
|
-
--project my-project \
|
|
210
|
-
--environment production
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
The default output is `analytics/hypequery-deployment.release.json`. It is
|
|
214
|
-
written beside the bundle because adding it inside the closed bundle would
|
|
215
|
-
invalidate bundle verification.
|
|
216
|
-
|
|
217
|
-
Options:
|
|
218
|
-
|
|
219
|
-
- `--project <project>`: target project identifier; must be paired with
|
|
220
|
-
`--environment`
|
|
221
|
-
- `--environment <environment>`: target environment identifier; must be paired
|
|
222
|
-
with `--project`
|
|
223
|
-
- `--output <path>`: release JSON path, default beside the bundle
|
|
224
|
-
|
|
225
|
-
### `hypequery deployment:submit`
|
|
226
|
-
|
|
227
|
-
Submits a verified deployment bundle with an already-prepared target-bound
|
|
228
|
-
release. The command verifies both inputs again, requires their bundle
|
|
229
|
-
identities to match, and streams only the files declared by the bundle.
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
npx hypequery deployment:submit analytics/hypequery-deployment \
|
|
233
|
-
--release analytics/hypequery-deployment.release.json
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Set `HYPEQUERY_API_TOKEN` together with either `--endpoint` or
|
|
237
|
-
`HYPEQUERY_DEPLOYMENT_ENDPOINT`. Tokens are never accepted as command-line
|
|
238
|
-
arguments, keeping them out of shell history. The submission endpoint must not
|
|
239
|
-
contain credentials or a URL fragment, and must use HTTPS except for
|
|
240
|
-
`127.0.0.1`/`localhost`, which is permitted for local development and warns that
|
|
241
|
-
the token is sent in cleartext. The release identity is sent as the idempotency
|
|
242
|
-
key, so an unchanged release can be submitted safely again.
|
|
243
|
-
|
|
244
|
-
This command submits immutable deployment inputs. Activation, status changes,
|
|
245
|
-
promotion, and rollback remain control-plane operations.
|
|
246
|
-
|
|
247
|
-
Options:
|
|
248
|
-
|
|
249
|
-
- `--release <path>`: required target-bound release JSON
|
|
250
|
-
- `--endpoint <url>`: HTTPS submission endpoint; requires `HYPEQUERY_API_TOKEN`
|
|
251
|
-
|
|
252
|
-
## Non-interactive Setup
|
|
253
|
-
|
|
254
|
-
### Cloud deployment targets in CI
|
|
255
|
-
|
|
256
|
-
Login selects a stable Cloud deployment target. The deployment itself captures
|
|
257
|
-
the checked-out Git branch, commit, and dirty state as source provenance; none
|
|
258
|
-
of those values select or create an environment. Select the destination
|
|
259
|
-
explicitly when the same branch or commit deploys to more than one environment:
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
npx hypequery login --environment development
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
For CI, create a target-scoped API key in Cloud for each environment and store
|
|
266
|
-
it as a separate secret. The explicit release target and its matching key make
|
|
267
|
-
the destination independent of the source branch:
|
|
268
|
-
|
|
269
|
-
```bash
|
|
270
|
-
HYPEQUERY_API_TOKEN="$HYPEQUERY_DEV_TOKEN" npx hypequery deploy analytics/api.ts \
|
|
271
|
-
--project acme:analytics \
|
|
272
|
-
--environment development
|
|
273
|
-
|
|
274
|
-
HYPEQUERY_API_TOKEN="$HYPEQUERY_PROD_TOKEN" npx hypequery deploy analytics/api.ts \
|
|
275
|
-
--project acme:analytics \
|
|
276
|
-
--environment production
|
|
26
|
+
npx hypequery init \
|
|
27
|
+
--database chdb \
|
|
28
|
+
--chdb-path ./analytics.chdb
|
|
277
29
|
```
|
|
278
30
|
|
|
279
|
-
|
|
280
|
-
so a production key cannot submit a development release or vice versa.
|
|
31
|
+
## The commands you will use
|
|
281
32
|
|
|
282
|
-
|
|
33
|
+
| Command | Outcome |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `hypequery init` | A working typed analytics project |
|
|
36
|
+
| `hypequery dev` | Local API, docs, and hot reload |
|
|
37
|
+
| `hypequery generate` | Fresh TypeScript types from ClickHouse or chDB |
|
|
38
|
+
| `hypequery generate:datasets` | Dataset definitions scaffolded from tables |
|
|
39
|
+
| `hypequery generate:manifest` | A browser-safe route manifest for React hooks |
|
|
40
|
+
| `hypequery login` | An authenticated Cloud target |
|
|
41
|
+
| `hypequery deploy` | A verified deployment of the analytics API |
|
|
42
|
+
| `hypequery pull` / `diff` | Live source inspection and comparison |
|
|
283
43
|
|
|
284
|
-
- `CLICKHOUSE_URL`
|
|
285
|
-
- `CLICKHOUSE_DATABASE`
|
|
286
|
-
- `CLICKHOUSE_USERNAME` or `CLICKHOUSE_USER`
|
|
287
|
-
- `CLICKHOUSE_PASSWORD`
|
|
44
|
+
Non-interactive ClickHouse commands read `CLICKHOUSE_URL`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_USERNAME`, and `CLICKHOUSE_PASSWORD`.
|
|
288
45
|
|
|
289
|
-
##
|
|
46
|
+
## Why use the CLI
|
|
290
47
|
|
|
291
|
-
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
48
|
+
- Get correct ClickHouse runtime types instead of guessing interfaces.
|
|
49
|
+
- Move from one local query to datasets, APIs, React, and MCP without changing tools.
|
|
50
|
+
- Keep generated types and route manifests reproducible in CI.
|
|
51
|
+
- Build closed, hash-verified deployment artifacts from the same source.
|
|
295
52
|
|
|
296
|
-
##
|
|
53
|
+
## Learn more
|
|
297
54
|
|
|
298
55
|
- [Quick start](https://hypequery.com/docs/quick-start)
|
|
299
56
|
- [CLI reference](https://hypequery.com/docs/reference/api/cli)
|
|
57
|
+
- [Current capabilities](https://hypequery.com/docs/capabilities)
|
|
300
58
|
|
|
301
59
|
## License
|
|
302
60
|
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AA4PD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { generateManifestCommand } from './commands/generate-manifest.js';
|
|
|
9
9
|
import { buildDeploymentCommand, prepareDeploymentReleaseCommand, validateDeploymentCommand, } from './commands/deployment.js';
|
|
10
10
|
import { deployCommand, submitDeploymentCommand, } from './commands/deploy.js';
|
|
11
11
|
import { loginCommand, logoutCommand, } from './commands/login.js';
|
|
12
|
+
import { diffCommand, pullCommand, } from './commands/live-source.js';
|
|
12
13
|
import { isPromptCancelled } from './utils/prompts.js';
|
|
13
14
|
const program = new Command();
|
|
14
15
|
function getCliVersion() {
|
|
@@ -44,6 +45,25 @@ program
|
|
|
44
45
|
.action(runCommand(async () => {
|
|
45
46
|
await logoutCommand();
|
|
46
47
|
}));
|
|
48
|
+
program
|
|
49
|
+
.command('pull')
|
|
50
|
+
.description('Download the source snapshot from the live deployment')
|
|
51
|
+
.option('-o, --output <directory>', 'New destination directory')
|
|
52
|
+
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
53
|
+
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
54
|
+
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
55
|
+
.action(runCommand(async (options) => {
|
|
56
|
+
await pullCommand(options);
|
|
57
|
+
}));
|
|
58
|
+
program
|
|
59
|
+
.command('diff [source]')
|
|
60
|
+
.description('Compare local source with the live deployment')
|
|
61
|
+
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
62
|
+
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
63
|
+
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
64
|
+
.action(runCommand(async (source, options) => {
|
|
65
|
+
await diffCommand(source, options);
|
|
66
|
+
}));
|
|
47
67
|
function runCommand(action) {
|
|
48
68
|
return async (...args) => {
|
|
49
69
|
try {
|
|
@@ -167,6 +187,7 @@ program
|
|
|
167
187
|
.description('Submit a prebuilt deployment bundle and release')
|
|
168
188
|
.requiredOption('--release <path>', 'Target-bound release JSON path')
|
|
169
189
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
190
|
+
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
170
191
|
.action(runCommand(async (bundle, options) => {
|
|
171
192
|
await submitDeploymentCommand(bundle, options);
|
|
172
193
|
}));
|
|
@@ -180,6 +201,7 @@ program
|
|
|
180
201
|
.option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
|
|
181
202
|
.option('--no-source', 'Exclude project source files from the deployment bundle')
|
|
182
203
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
204
|
+
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
183
205
|
.action(runCommand(async (source, options) => {
|
|
184
206
|
await deployCommand(source, options);
|
|
185
207
|
}));
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHttpDeploymentUploadTransport, type DeploymentSubmissionResponse } from '../utils/deployment-upload.js';
|
|
2
2
|
import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
|
|
3
|
+
import { fetchLiveDeployment } from '../utils/live-deployment.js';
|
|
3
4
|
import { buildDeploymentCommand, prepareDeploymentReleaseCommand } from './deployment.js';
|
|
4
5
|
export interface SubmitDeploymentOptions {
|
|
5
6
|
release?: string;
|
|
6
7
|
endpoint?: string;
|
|
8
|
+
replaceRestored?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export interface DeployOptions extends SubmitDeploymentOptions {
|
|
9
11
|
project?: string;
|
|
@@ -16,6 +18,7 @@ export interface SubmitDeploymentDependencies {
|
|
|
16
18
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
17
19
|
readonly createTransport?: typeof createHttpDeploymentUploadTransport;
|
|
18
20
|
readonly loadCredential?: () => Promise<StoredCloudCredential | null>;
|
|
21
|
+
readonly fetchLive?: typeof fetchLiveDeployment;
|
|
19
22
|
}
|
|
20
23
|
export interface DeployDependencies extends SubmitDeploymentDependencies {
|
|
21
24
|
readonly buildDeployment?: typeof buildDeploymentCommand;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AASA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AASA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EACL,sBAAsB,EACtB,+BAA+B,EAChC,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAc,SAAQ,uBAAuB;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,mCAAmC,CAAC;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,mBAAmB,CAAC;CACjD;AAED,MAAM,WAAW,kBAAmB,SAAQ,4BAA4B;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACzD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,+BAA+B,CAAC;IAC3E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,uBAAuB,CAAC;CAC5D;AA4DD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,uBAA4B,EACrC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CA0EvC;AAmDD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAqDvC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,7 +4,8 @@ import { prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
|
|
|
4
4
|
import { DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
|
|
5
5
|
import { createHttpDeploymentUploadTransport, DeploymentUploadError, } from '../utils/deployment-upload.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
|
-
import {
|
|
7
|
+
import { resolveDeploymentCredential } from '../utils/cloud-deployment-access.js';
|
|
8
|
+
import { fetchLiveDeployment } from '../utils/live-deployment.js';
|
|
8
9
|
import { buildDeploymentCommand, prepareDeploymentReleaseCommand, } from './deployment.js';
|
|
9
10
|
const MAX_RELEASE_FILE_BYTES = 16 * 1024;
|
|
10
11
|
const DEFAULT_BUNDLE_PATH = 'analytics/hypequery-deployment';
|
|
@@ -64,39 +65,6 @@ function requiredConfiguration(value, message) {
|
|
|
64
65
|
throw new Error(message);
|
|
65
66
|
return value;
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Resolves the submission endpoint and token. Kept separate from
|
|
69
|
-
* `submitDeploymentCommand` so `deployCommand` can run it as a preflight
|
|
70
|
-
* before the bundle build.
|
|
71
|
-
*/
|
|
72
|
-
async function resolveDeploymentCredential(endpointOption, dependencies) {
|
|
73
|
-
const env = dependencies.env ?? process.env;
|
|
74
|
-
let deploymentEndpoint = endpointOption ?? env.HYPEQUERY_DEPLOYMENT_ENDPOINT;
|
|
75
|
-
let token = env.HYPEQUERY_API_TOKEN;
|
|
76
|
-
const hasExplicitEndpoint = Boolean(deploymentEndpoint);
|
|
77
|
-
const hasExplicitToken = Boolean(token);
|
|
78
|
-
if (hasExplicitEndpoint !== hasExplicitToken) {
|
|
79
|
-
throw new Error(hasExplicitEndpoint
|
|
80
|
-
? 'An explicit deployment endpoint requires HYPEQUERY_API_TOKEN.'
|
|
81
|
-
: 'HYPEQUERY_API_TOKEN requires --endpoint or HYPEQUERY_DEPLOYMENT_ENDPOINT.\n\n'
|
|
82
|
-
+ 'If you meant to use `hypequery login`, unset HYPEQUERY_API_TOKEN — '
|
|
83
|
-
+ 'the CLI also reads it from a project .env file.');
|
|
84
|
-
}
|
|
85
|
-
if (!hasExplicitEndpoint) {
|
|
86
|
-
const credential = await (dependencies.loadCredential ?? loadCloudCredential)();
|
|
87
|
-
if (credential) {
|
|
88
|
-
if (Date.parse(credential.expiresAt) <= Date.now()) {
|
|
89
|
-
throw new Error('The stored Cloud credential has expired. Run `hypequery login` again.');
|
|
90
|
-
}
|
|
91
|
-
deploymentEndpoint = credential.deploymentEndpoint;
|
|
92
|
-
token = credential.token;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
endpoint: requiredConfiguration(deploymentEndpoint, 'Missing deployment endpoint. Run `hypequery login`, pass --endpoint, or set HYPEQUERY_DEPLOYMENT_ENDPOINT.'),
|
|
97
|
-
token: requiredConfiguration(token, 'Missing deployment credential. Run `hypequery login` or set HYPEQUERY_API_TOKEN.'),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
68
|
export async function submitDeploymentCommand(bundlePath, options = {}, dependencies = {}) {
|
|
101
69
|
if (!bundlePath) {
|
|
102
70
|
throw new Error('Missing deployment bundle path.\n\n'
|
|
@@ -125,10 +93,26 @@ export async function submitDeploymentCommand(bundlePath, options = {}, dependen
|
|
|
125
93
|
if (release.release.bundleIdentity !== bundle.identity) {
|
|
126
94
|
throw new DeploymentUploadError('HQ_UPLOAD_IDENTITY_MISMATCH', 'Release bundle identity does not match the verified deployment bundle.');
|
|
127
95
|
}
|
|
96
|
+
const live = await (dependencies.fetchLive ?? fetchLiveDeployment)({
|
|
97
|
+
endpoint: deploymentEndpoint,
|
|
98
|
+
token,
|
|
99
|
+
target: release.release.target,
|
|
100
|
+
resource: 'state',
|
|
101
|
+
});
|
|
102
|
+
if (live?.active?.restored
|
|
103
|
+
&& live.active.releaseIdentity !== release.identity
|
|
104
|
+
&& !options.replaceRestored) {
|
|
105
|
+
throw new Error('The live deployment is a restored release. '
|
|
106
|
+
+ 'Run the command again with --replace-restored to replace it intentionally.');
|
|
107
|
+
}
|
|
128
108
|
const createTransport = dependencies.createTransport ?? createHttpDeploymentUploadTransport;
|
|
129
109
|
const transportOptions = {
|
|
130
110
|
endpoint: deploymentEndpoint,
|
|
131
111
|
token,
|
|
112
|
+
...(live
|
|
113
|
+
? { expectedActivationRevision: live.active?.revision ?? null }
|
|
114
|
+
: {}),
|
|
115
|
+
...(options.replaceRestored ? { replaceRestored: true } : {}),
|
|
132
116
|
};
|
|
133
117
|
const result = await createTransport(transportOptions).submit(bundle, release);
|
|
134
118
|
logger.success(result.status === 'accepted'
|
|
@@ -190,6 +174,7 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
|
|
|
190
174
|
return submitDeploymentCommand(sourcePath, {
|
|
191
175
|
release: options.release,
|
|
192
176
|
endpoint: options.endpoint,
|
|
177
|
+
replaceRestored: options.replaceRestored,
|
|
193
178
|
}, dependencies);
|
|
194
179
|
}
|
|
195
180
|
await rejectBundleDirectorySource(sourcePath);
|
|
@@ -220,5 +205,6 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
|
|
|
220
205
|
return submit(bundlePath, {
|
|
221
206
|
release: releasePath,
|
|
222
207
|
endpoint: options.endpoint,
|
|
208
|
+
replaceRestored: options.replaceRestored,
|
|
223
209
|
}, dependencies);
|
|
224
210
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CloudDeploymentAccessDependencies } from '../utils/cloud-deployment-access.js';
|
|
2
|
+
import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
|
|
3
|
+
import { fetchLiveDeployment } from '../utils/live-deployment.js';
|
|
4
|
+
export interface LiveSourceOptions {
|
|
5
|
+
readonly endpoint?: string;
|
|
6
|
+
readonly project?: string;
|
|
7
|
+
readonly environment?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PullOptions extends LiveSourceOptions {
|
|
10
|
+
readonly output?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LiveSourceDependencies extends CloudDeploymentAccessDependencies {
|
|
13
|
+
readonly fetchLive?: typeof fetchLiveDeployment;
|
|
14
|
+
readonly captureSource?: typeof captureDeploymentSourceSnapshot;
|
|
15
|
+
}
|
|
16
|
+
export type LiveSourceDifference = {
|
|
17
|
+
readonly status: 'A' | 'M' | 'D';
|
|
18
|
+
readonly path: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function pullCommand(options?: PullOptions, dependencies?: LiveSourceDependencies): Promise<string>;
|
|
21
|
+
export declare function diffCommand(sourcePath: string | undefined, options?: LiveSourceOptions, dependencies?: LiveSourceDependencies): Promise<readonly LiveSourceDifference[]>;
|
|
22
|
+
//# sourceMappingURL=live-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-source.d.ts","sourceRoot":"","sources":["../../src/commands/live-source.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,+BAA+B,EAAE,MAAM,wCAAwC,CAAC;AACzF,OAAO,EACL,mBAAmB,EAEpB,MAAM,6BAA6B,CAAC;AAGrC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAuB,SAAQ,iCAAiC;IAC/E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,mBAAmB,CAAC;IAChD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,+BAA+B,CAAC;CACjE;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAkEF,wBAAsB,WAAW,CAC/B,OAAO,GAAE,WAAgB,EACzB,YAAY,GAAE,sBAA2B,mBAuC1C;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,iBAAsB,EAC/B,YAAY,GAAE,sBAA2B,GACxC,OAAO,CAAC,SAAS,oBAAoB,EAAE,CAAC,CA0B1C"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
|
|
5
|
+
import { CLOUD_SOURCE_SCOPE, } from '../utils/cloud-credential-store.js';
|
|
6
|
+
import { resolveDeploymentCredential, } from '../utils/cloud-deployment-access.js';
|
|
7
|
+
import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
|
|
8
|
+
import { fetchLiveDeployment, } from '../utils/live-deployment.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
function targetFromOptions(options, credential) {
|
|
11
|
+
if ((options.project === undefined) !== (options.environment === undefined)) {
|
|
12
|
+
throw new Error('Pass both --project and --environment, or omit both.');
|
|
13
|
+
}
|
|
14
|
+
const input = options.project === undefined
|
|
15
|
+
? credential?.target
|
|
16
|
+
: { project: options.project, environment: options.environment };
|
|
17
|
+
if (!input) {
|
|
18
|
+
throw new Error('Missing deployment target. Run `hypequery login` or pass both '
|
|
19
|
+
+ '--project and --environment.');
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return validateProtocolDeploymentReleaseTarget(input);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('The deployment target is invalid.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function liveSource(options, dependencies) {
|
|
29
|
+
const access = await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
30
|
+
if (access.storedCredential
|
|
31
|
+
&& access.storedCredential.scope !== CLOUD_SOURCE_SCOPE) {
|
|
32
|
+
throw new Error('The stored CLI credential cannot read deployed source. Run `hypequery login` again.');
|
|
33
|
+
}
|
|
34
|
+
const target = targetFromOptions(options, access.storedCredential);
|
|
35
|
+
const live = await (dependencies.fetchLive ?? fetchLiveDeployment)({
|
|
36
|
+
endpoint: access.endpoint,
|
|
37
|
+
token: access.token,
|
|
38
|
+
target,
|
|
39
|
+
resource: 'source',
|
|
40
|
+
});
|
|
41
|
+
if (!live?.active?.source) {
|
|
42
|
+
throw new Error('The live deployment does not include a source snapshot.');
|
|
43
|
+
}
|
|
44
|
+
return live;
|
|
45
|
+
}
|
|
46
|
+
function digest(bytes) {
|
|
47
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
async function exists(input) {
|
|
50
|
+
try {
|
|
51
|
+
await stat(input);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (typeof error === 'object' && error !== null && 'code' in error
|
|
56
|
+
&& error.code === 'ENOENT')
|
|
57
|
+
return false;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function pullCommand(options = {}, dependencies = {}) {
|
|
62
|
+
const live = await liveSource(options, dependencies);
|
|
63
|
+
const active = live.active;
|
|
64
|
+
const source = active.source;
|
|
65
|
+
const destination = path.resolve(options.output
|
|
66
|
+
?? path.join('.hypequery', 'live', live.target.environment, active.releaseIdentity.slice(0, 12)));
|
|
67
|
+
if (destination === path.parse(destination).root || await exists(destination)) {
|
|
68
|
+
throw new Error(`Refusing to overwrite an existing pull destination: ${destination}`);
|
|
69
|
+
}
|
|
70
|
+
const parent = path.dirname(destination);
|
|
71
|
+
await mkdir(parent, { recursive: true });
|
|
72
|
+
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
73
|
+
await mkdir(staging, { mode: 0o700 });
|
|
74
|
+
try {
|
|
75
|
+
for (const file of source.files) {
|
|
76
|
+
const output = path.join(staging, ...file.path.split('/'));
|
|
77
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
78
|
+
await writeFile(output, file.bytes, { flag: 'wx' });
|
|
79
|
+
}
|
|
80
|
+
await rename(staging, destination);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
logger.success(`Live source pulled to ${destination}`);
|
|
87
|
+
logger.info(`Release identity: ${active.releaseIdentity}`);
|
|
88
|
+
logger.info(`Entrypoint: ${source.entrypoint}`);
|
|
89
|
+
return destination;
|
|
90
|
+
}
|
|
91
|
+
export async function diffCommand(sourcePath, options = {}, dependencies = {}) {
|
|
92
|
+
const live = await liveSource(options, dependencies);
|
|
93
|
+
const active = live.active;
|
|
94
|
+
const source = active.source;
|
|
95
|
+
const local = await (dependencies.captureSource ?? captureDeploymentSourceSnapshot)(sourcePath ?? source.entrypoint);
|
|
96
|
+
const liveFiles = new Map(source.files.map(file => [file.path, file.sha256]));
|
|
97
|
+
const localFiles = new Map(local.files.map(file => [file.path, digest(file.bytes)]));
|
|
98
|
+
const paths = [...new Set([...liveFiles.keys(), ...localFiles.keys()])].sort();
|
|
99
|
+
const differences = [];
|
|
100
|
+
for (const file of paths) {
|
|
101
|
+
const liveDigest = liveFiles.get(file);
|
|
102
|
+
const localDigest = localFiles.get(file);
|
|
103
|
+
if (liveDigest === undefined)
|
|
104
|
+
differences.push({ status: 'A', path: file });
|
|
105
|
+
else if (localDigest === undefined)
|
|
106
|
+
differences.push({ status: 'D', path: file });
|
|
107
|
+
else if (liveDigest !== localDigest)
|
|
108
|
+
differences.push({ status: 'M', path: file });
|
|
109
|
+
}
|
|
110
|
+
if (differences.length === 0) {
|
|
111
|
+
logger.success('Local source matches the live deployment');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
for (const difference of differences) {
|
|
115
|
+
logger.info(`${difference.status} ${difference.path}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze(differences);
|
|
119
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { validateProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
|
|
5
|
-
import {
|
|
5
|
+
import { CLOUD_SOURCE_SCOPE, deleteCloudCredential, loadCloudCredential, normalizeCloudDeploymentEndpoint, normalizeCloudOrigin, saveCloudCredential, } from '../utils/cloud-credential-store.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
const DEFAULT_CLOUD_URL = 'https://cloud.hypequery.com';
|
|
8
8
|
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -131,7 +131,7 @@ function tokenResponse(input, origin, now) {
|
|
|
131
131
|
|| value.token_type !== 'Bearer'
|
|
132
132
|
|| typeof value.expires_at !== 'string'
|
|
133
133
|
|| !Number.isFinite(Date.parse(value.expires_at))
|
|
134
|
-
|| value.scope !==
|
|
134
|
+
|| value.scope !== CLOUD_SOURCE_SCOPE
|
|
135
135
|
|| typeof value.deployment_endpoint !== 'string'
|
|
136
136
|
|| value.deployment_target === undefined) {
|
|
137
137
|
throw new Error('Cloud returned an invalid CLI token response.');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
|
|
2
2
|
export declare const CLOUD_DEPLOYMENT_SCOPE = "deploy:submit";
|
|
3
|
+
export declare const CLOUD_SOURCE_SCOPE = "deploy:submit deploy:read-source";
|
|
3
4
|
export interface StoredCloudCredential {
|
|
4
5
|
readonly cloudUrl: string;
|
|
5
6
|
readonly deploymentEndpoint: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-credential-store.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-credential-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"cloud-credential-store.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-credential-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,eAAO,MAAM,kBAAkB,qCAAqC,CAAC;AAGrE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,+BAA+B,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAYD,UAAU,YAAY;IACpB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,WAAW,IAAI,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,cAAc,IAAI,OAAO,CAAC;CAC3B;AAQD,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACrC;AAyCD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAc1D;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAmBR;AAmFD,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,qBAAqB,EACjC,YAAY,GAAE,gCAAqC,iBAkEpD;AAED,wBAAsB,mBAAmB,CACvC,YAAY,GAAE,gCAAqC,GAClD,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAsBvC;AA0BD,wBAAsB,qBAAqB,CACzC,YAAY,GAAE,gCAAqC,iBA0BpD"}
|
|
@@ -6,6 +6,7 @@ import { validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
|
|
|
6
6
|
const KEYCHAIN_SERVICE = 'dev.hypequery.cli';
|
|
7
7
|
const PROFILE_FILE = 'cloud-profile.json';
|
|
8
8
|
export const CLOUD_DEPLOYMENT_SCOPE = 'deploy:submit';
|
|
9
|
+
export const CLOUD_SOURCE_SCOPE = 'deploy:submit deploy:read-source';
|
|
9
10
|
const MAX_KEYCHAIN_ACCOUNT_LENGTH = 2048;
|
|
10
11
|
function defaultConfigDirectory(env, platform) {
|
|
11
12
|
if (env.HYPEQUERY_CONFIG_DIR)
|
|
@@ -97,7 +98,7 @@ function parseProfile(input) {
|
|
|
97
98
|
deploymentEndpoint = normalizeCloudDeploymentEndpoint(value.deploymentEndpoint, cloudUrl);
|
|
98
99
|
if (value.keychainAccount !== cloudUrl
|
|
99
100
|
|| !Number.isFinite(Date.parse(value.expiresAt))
|
|
100
|
-
|| value.scope !== CLOUD_DEPLOYMENT_SCOPE) {
|
|
101
|
+
|| (value.scope !== CLOUD_DEPLOYMENT_SCOPE && value.scope !== CLOUD_SOURCE_SCOPE)) {
|
|
101
102
|
throw new Error('profile invariant mismatch');
|
|
102
103
|
}
|
|
103
104
|
target = value.target === undefined
|
|
@@ -164,7 +165,8 @@ export async function saveCloudCredential(credential, dependencies = {}) {
|
|
|
164
165
|
const cloudUrl = normalizeCloudOrigin(credential.cloudUrl);
|
|
165
166
|
const deploymentEndpoint = normalizeCloudDeploymentEndpoint(credential.deploymentEndpoint, cloudUrl);
|
|
166
167
|
if (!Number.isFinite(Date.parse(credential.expiresAt))
|
|
167
|
-
|| credential.scope !== CLOUD_DEPLOYMENT_SCOPE
|
|
168
|
+
|| (credential.scope !== CLOUD_DEPLOYMENT_SCOPE
|
|
169
|
+
&& credential.scope !== CLOUD_SOURCE_SCOPE)) {
|
|
168
170
|
throw new Error('Cannot store an invalid Hypequery Cloud credential.');
|
|
169
171
|
}
|
|
170
172
|
const keychainAccount = cloudUrl;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type StoredCloudCredential } from './cloud-credential-store.js';
|
|
2
|
+
export interface CloudDeploymentAccessDependencies {
|
|
3
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
4
|
+
readonly loadCredential?: () => Promise<StoredCloudCredential | null>;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolvedDeploymentCredential {
|
|
7
|
+
readonly endpoint: string;
|
|
8
|
+
readonly token: string;
|
|
9
|
+
readonly storedCredential?: StoredCloudCredential;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveDeploymentCredential(endpointOption: string | undefined, dependencies: CloudDeploymentAccessDependencies): Promise<ResolvedDeploymentCredential>;
|
|
12
|
+
//# sourceMappingURL=cloud-deployment-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-deployment-access.d.ts","sourceRoot":"","sources":["../../src/utils/cloud-deployment-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CACnD;AAUD,wBAAsB,2BAA2B,CAC/C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,YAAY,EAAE,iCAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CAsCvC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { loadCloudCredential, } from './cloud-credential-store.js';
|
|
2
|
+
function requiredConfiguration(value, message) {
|
|
3
|
+
if (value === undefined || value.length === 0)
|
|
4
|
+
throw new Error(message);
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
export async function resolveDeploymentCredential(endpointOption, dependencies) {
|
|
8
|
+
const env = dependencies.env ?? process.env;
|
|
9
|
+
let deploymentEndpoint = endpointOption ?? env.HYPEQUERY_DEPLOYMENT_ENDPOINT;
|
|
10
|
+
let token = env.HYPEQUERY_API_TOKEN;
|
|
11
|
+
let storedCredential;
|
|
12
|
+
const hasExplicitEndpoint = Boolean(deploymentEndpoint);
|
|
13
|
+
const hasExplicitToken = Boolean(token);
|
|
14
|
+
if (hasExplicitEndpoint !== hasExplicitToken) {
|
|
15
|
+
throw new Error(hasExplicitEndpoint
|
|
16
|
+
? 'An explicit deployment endpoint requires HYPEQUERY_API_TOKEN.'
|
|
17
|
+
: 'HYPEQUERY_API_TOKEN requires --endpoint or HYPEQUERY_DEPLOYMENT_ENDPOINT.\n\n'
|
|
18
|
+
+ 'If you meant to use `hypequery login`, unset HYPEQUERY_API_TOKEN — '
|
|
19
|
+
+ 'the CLI also reads it from a project .env file.');
|
|
20
|
+
}
|
|
21
|
+
if (!hasExplicitEndpoint) {
|
|
22
|
+
const credential = await (dependencies.loadCredential ?? loadCloudCredential)();
|
|
23
|
+
if (credential) {
|
|
24
|
+
if (Date.parse(credential.expiresAt) <= Date.now()) {
|
|
25
|
+
throw new Error('The stored Cloud credential has expired. Run `hypequery login` again.');
|
|
26
|
+
}
|
|
27
|
+
deploymentEndpoint = credential.deploymentEndpoint;
|
|
28
|
+
token = credential.token;
|
|
29
|
+
storedCredential = credential;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
endpoint: requiredConfiguration(deploymentEndpoint, 'Missing deployment endpoint. Run `hypequery login`, pass --endpoint, or set HYPEQUERY_DEPLOYMENT_ENDPOINT.'),
|
|
34
|
+
token: requiredConfiguration(token, 'Missing deployment credential. Run `hypequery login` or set HYPEQUERY_API_TOKEN.'),
|
|
35
|
+
...(storedCredential ? { storedCredential } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -27,6 +27,8 @@ export type DeploymentFetch = (input: string, init: DeploymentFetchInit) => Prom
|
|
|
27
27
|
export interface HttpDeploymentUploadTransportOptions {
|
|
28
28
|
readonly endpoint: string;
|
|
29
29
|
readonly token: string;
|
|
30
|
+
readonly expectedActivationRevision?: string | null;
|
|
31
|
+
readonly replaceRestored?: boolean;
|
|
30
32
|
readonly timeoutMs?: number;
|
|
31
33
|
readonly fetch?: DeploymentFetch;
|
|
32
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-upload.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,KAAK,yCAAyC,EAC/C,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE1E,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAQ1E,MAAM,MAAM,yBAAyB,GACjC,yBAAyB,GACzB,6BAA6B,GAC7B,0BAA0B,GAC1B,mBAAmB,GACnB,oBAAoB,GACpB,4BAA4B,CAAC;AAEjC,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAM9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAErC,MAAM,WAAW,oCAAoC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CACJ,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,yCAAyC,GACjD,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC1C;AA+WD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,yBAAyB,
|
|
1
|
+
{"version":3,"file":"deployment-upload.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,KAAK,yCAAyC,EAC/C,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE1E,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAQ1E,MAAM,MAAM,yBAAyB,GACjC,yBAAyB,GACzB,6BAA6B,GAC7B,0BAA0B,GAC1B,mBAAmB,GACnB,oBAAoB,GACpB,4BAA4B,CAAC;AAEjC,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAM9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAErC,MAAM,WAAW,oCAAoC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CACJ,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,yCAAyC,GACjD,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC1C;AA+WD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,yBAAyB,CA+F3B"}
|
|
@@ -347,6 +347,14 @@ export function createHttpDeploymentUploadTransport(options) {
|
|
|
347
347
|
'Idempotency-Key': preparedRelease.identity,
|
|
348
348
|
'X-HypeQuery-Bundle-Identity': manifest.identity,
|
|
349
349
|
'X-HypeQuery-Release-Identity': preparedRelease.identity,
|
|
350
|
+
...(options.expectedActivationRevision !== undefined
|
|
351
|
+
? {
|
|
352
|
+
'X-HypeQuery-Expected-Activation-Revision': options.expectedActivationRevision ?? 'none',
|
|
353
|
+
}
|
|
354
|
+
: {}),
|
|
355
|
+
...(options.replaceRestored
|
|
356
|
+
? { 'X-HypeQuery-Replace-Restored': 'true' }
|
|
357
|
+
: {}),
|
|
350
358
|
},
|
|
351
359
|
body: multipartBody(boundary, parts),
|
|
352
360
|
duplex: 'half',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ProtocolDeploymentReleaseTarget } from '@hypequery/protocol';
|
|
2
|
+
export type LiveDeploymentSourceFile = {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly sha256: string;
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
export type LiveDeploymentSource = {
|
|
8
|
+
readonly entrypoint: string;
|
|
9
|
+
readonly files: readonly LiveDeploymentSourceFile[];
|
|
10
|
+
readonly revision?: {
|
|
11
|
+
readonly kind: 'git';
|
|
12
|
+
readonly commit: string;
|
|
13
|
+
readonly branch?: string;
|
|
14
|
+
readonly dirty: boolean;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type LiveDeployment = {
|
|
18
|
+
readonly target: ProtocolDeploymentReleaseTarget;
|
|
19
|
+
readonly active: null | {
|
|
20
|
+
readonly revision: string;
|
|
21
|
+
readonly releaseIdentity: string;
|
|
22
|
+
readonly activatedAt: string;
|
|
23
|
+
readonly restored: boolean;
|
|
24
|
+
readonly hasSource: boolean;
|
|
25
|
+
readonly source?: LiveDeploymentSource;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export type LiveDeploymentFetch = typeof fetch;
|
|
29
|
+
export declare function fetchLiveDeployment(input: {
|
|
30
|
+
readonly endpoint: string;
|
|
31
|
+
readonly token: string;
|
|
32
|
+
readonly target: ProtocolDeploymentReleaseTarget;
|
|
33
|
+
readonly resource: 'state' | 'source';
|
|
34
|
+
readonly fetch?: LiveDeploymentFetch;
|
|
35
|
+
}): Promise<LiveDeployment | undefined>;
|
|
36
|
+
//# sourceMappingURL=live-deployment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-deployment.d.ts","sourceRoot":"","sources":["../../src/utils/live-deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,+BAA+B,EACrC,MAAM,qBAAqB,CAAC;AAO7B,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;QAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,OAAO,KAAK,CAAC;AA8K/C,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,+BAA+B,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;CACtC,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAuCtC"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
|
|
4
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
5
|
+
const MAX_RESPONSE_BYTES = Math.ceil(DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceBytes * 1.4) + 1_000_000;
|
|
6
|
+
function liveUrl(endpoint, target, resource) {
|
|
7
|
+
const url = new URL(endpoint);
|
|
8
|
+
if (!/\/v1\/deployments\/submissions\/?$/.test(url.pathname)) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const loopbackHttp = url.protocol === 'http:'
|
|
12
|
+
&& (url.hostname === '127.0.0.1' || url.hostname === 'localhost');
|
|
13
|
+
if ((url.protocol !== 'https:' && !loopbackHttp)
|
|
14
|
+
|| url.username
|
|
15
|
+
|| url.password
|
|
16
|
+
|| url.hash) {
|
|
17
|
+
throw new Error('The deployment endpoint cannot be used to read live state safely.');
|
|
18
|
+
}
|
|
19
|
+
url.pathname = `/v1/deployments/targets/${encodeURIComponent(target.project)}`
|
|
20
|
+
+ `/${encodeURIComponent(target.environment)}/${resource}`;
|
|
21
|
+
url.search = '';
|
|
22
|
+
url.hash = '';
|
|
23
|
+
return url.toString();
|
|
24
|
+
}
|
|
25
|
+
function safePath(input) {
|
|
26
|
+
return typeof input === 'string'
|
|
27
|
+
&& input.length > 0
|
|
28
|
+
&& input.length <= 1024
|
|
29
|
+
&& input.split(path.sep).join('/') === input
|
|
30
|
+
&& !path.isAbsolute(input)
|
|
31
|
+
&& input.split('/').every(segment => segment && segment !== '.' && segment !== '..');
|
|
32
|
+
}
|
|
33
|
+
function decodeFile(input) {
|
|
34
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
35
|
+
throw new Error('Cloud returned an invalid live source snapshot.');
|
|
36
|
+
}
|
|
37
|
+
const value = input;
|
|
38
|
+
if (!safePath(value.path)
|
|
39
|
+
|| typeof value.sha256 !== 'string'
|
|
40
|
+
|| !SHA256_PATTERN.test(value.sha256)
|
|
41
|
+
|| !Number.isSafeInteger(value.byteLength)
|
|
42
|
+
|| value.byteLength < 0
|
|
43
|
+
|| value.byteLength
|
|
44
|
+
> DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceFileBytes
|
|
45
|
+
|| typeof value.contentsBase64 !== 'string'
|
|
46
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value.contentsBase64)) {
|
|
47
|
+
throw new Error('Cloud returned an invalid live source snapshot.');
|
|
48
|
+
}
|
|
49
|
+
const bytes = Buffer.from(value.contentsBase64, 'base64');
|
|
50
|
+
if (bytes.byteLength !== value.byteLength
|
|
51
|
+
|| createHash('sha256').update(bytes).digest('hex') !== value.sha256) {
|
|
52
|
+
throw new Error('Cloud returned a corrupt live source snapshot.');
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze({ path: value.path, sha256: value.sha256, bytes });
|
|
55
|
+
}
|
|
56
|
+
function parseSource(input) {
|
|
57
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
58
|
+
throw new Error('Cloud returned an invalid live source snapshot.');
|
|
59
|
+
}
|
|
60
|
+
const value = input;
|
|
61
|
+
if (!safePath(value.entrypoint) || !Array.isArray(value.files)
|
|
62
|
+
|| value.files.length < 1
|
|
63
|
+
|| value.files.length > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceFiles) {
|
|
64
|
+
throw new Error('Cloud returned an invalid live source snapshot.');
|
|
65
|
+
}
|
|
66
|
+
const files = value.files.map(decodeFile);
|
|
67
|
+
const paths = new Set();
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const folded = file.path.toLowerCase();
|
|
71
|
+
if (paths.has(folded))
|
|
72
|
+
throw new Error('Cloud returned duplicate live source paths.');
|
|
73
|
+
paths.add(folded);
|
|
74
|
+
total += file.bytes.byteLength;
|
|
75
|
+
}
|
|
76
|
+
if (total > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxSourceBytes
|
|
77
|
+
|| !files.some(file => file.path === value.entrypoint)) {
|
|
78
|
+
throw new Error('Cloud returned an invalid live source snapshot.');
|
|
79
|
+
}
|
|
80
|
+
let revision;
|
|
81
|
+
if (value.revision !== undefined) {
|
|
82
|
+
if (typeof value.revision !== 'object' || value.revision === null
|
|
83
|
+
|| Array.isArray(value.revision)) {
|
|
84
|
+
throw new Error('Cloud returned an invalid live source revision.');
|
|
85
|
+
}
|
|
86
|
+
const candidate = value.revision;
|
|
87
|
+
if (candidate.kind !== 'git'
|
|
88
|
+
|| typeof candidate.commit !== 'string'
|
|
89
|
+
|| !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.commit)
|
|
90
|
+
|| typeof candidate.dirty !== 'boolean'
|
|
91
|
+
|| (candidate.branch !== undefined && typeof candidate.branch !== 'string')) {
|
|
92
|
+
throw new Error('Cloud returned an invalid live source revision.');
|
|
93
|
+
}
|
|
94
|
+
revision = {
|
|
95
|
+
kind: 'git',
|
|
96
|
+
commit: candidate.commit,
|
|
97
|
+
dirty: candidate.dirty,
|
|
98
|
+
...(candidate.branch !== undefined ? { branch: candidate.branch } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
entrypoint: value.entrypoint,
|
|
103
|
+
files: Object.freeze(files),
|
|
104
|
+
...(revision ? { revision: Object.freeze(revision) } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function parseResponse(input, expectedTarget) {
|
|
108
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
109
|
+
throw new Error('Cloud returned an invalid live deployment response.');
|
|
110
|
+
}
|
|
111
|
+
const value = input;
|
|
112
|
+
let target;
|
|
113
|
+
try {
|
|
114
|
+
target = validateProtocolDeploymentReleaseTarget(value.target);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new Error('Cloud returned an invalid live deployment target.');
|
|
118
|
+
}
|
|
119
|
+
if (value.kind !== 'hypequery-live-deployment' || value.version !== 1
|
|
120
|
+
|| target.project !== expectedTarget.project
|
|
121
|
+
|| target.environment !== expectedTarget.environment) {
|
|
122
|
+
throw new Error('Cloud returned a mismatched live deployment response.');
|
|
123
|
+
}
|
|
124
|
+
if (value.active === null)
|
|
125
|
+
return Object.freeze({ target, active: null });
|
|
126
|
+
if (typeof value.active !== 'object' || Array.isArray(value.active)) {
|
|
127
|
+
throw new Error('Cloud returned an invalid live deployment response.');
|
|
128
|
+
}
|
|
129
|
+
const active = value.active;
|
|
130
|
+
if (typeof active.revision !== 'string' || !SHA256_PATTERN.test(active.revision)
|
|
131
|
+
|| typeof active.releaseIdentity !== 'string'
|
|
132
|
+
|| !SHA256_PATTERN.test(active.releaseIdentity)
|
|
133
|
+
|| typeof active.activatedAt !== 'string'
|
|
134
|
+
|| !Number.isFinite(Date.parse(active.activatedAt))
|
|
135
|
+
|| typeof active.restored !== 'boolean'
|
|
136
|
+
|| typeof active.hasSource !== 'boolean'
|
|
137
|
+
|| (active.source !== undefined && !active.hasSource)) {
|
|
138
|
+
throw new Error('Cloud returned an invalid live deployment response.');
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
target,
|
|
142
|
+
active: Object.freeze({
|
|
143
|
+
revision: active.revision,
|
|
144
|
+
releaseIdentity: active.releaseIdentity,
|
|
145
|
+
activatedAt: active.activatedAt,
|
|
146
|
+
restored: active.restored,
|
|
147
|
+
hasSource: active.hasSource,
|
|
148
|
+
...(active.source !== undefined ? { source: parseSource(active.source) } : {}),
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function errorMessage(response) {
|
|
153
|
+
try {
|
|
154
|
+
const input = await response.json();
|
|
155
|
+
const code = typeof input.error?.code === 'string' ? `${input.error.code}: ` : '';
|
|
156
|
+
const message = typeof input.error?.message === 'string'
|
|
157
|
+
? input.error.message
|
|
158
|
+
: response.statusText;
|
|
159
|
+
return `${code}${message}`;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return response.statusText;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function fetchLiveDeployment(input) {
|
|
166
|
+
if (input.token.length < 1 || input.token.length > 4096
|
|
167
|
+
|| input.token.trim() !== input.token
|
|
168
|
+
|| [...input.token].some(character => {
|
|
169
|
+
const code = character.charCodeAt(0);
|
|
170
|
+
return code < 0x21 || code > 0x7e;
|
|
171
|
+
})) {
|
|
172
|
+
throw new Error('The deployment credential contains invalid characters or length.');
|
|
173
|
+
}
|
|
174
|
+
const request = input.fetch ?? fetch;
|
|
175
|
+
const url = liveUrl(input.endpoint, input.target, input.resource);
|
|
176
|
+
if (!url)
|
|
177
|
+
return undefined;
|
|
178
|
+
const response = await request(url, {
|
|
179
|
+
method: 'GET',
|
|
180
|
+
headers: { Accept: 'application/json', Authorization: `Bearer ${input.token}` },
|
|
181
|
+
redirect: 'error',
|
|
182
|
+
signal: AbortSignal.timeout(30_000),
|
|
183
|
+
});
|
|
184
|
+
if (input.resource === 'state' && response.status === 404)
|
|
185
|
+
return undefined;
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(`Could not read the live deployment (${response.status}): ${await errorMessage(response)}`);
|
|
188
|
+
}
|
|
189
|
+
const contentLength = Number(response.headers.get('content-length'));
|
|
190
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
|
|
191
|
+
throw new Error('Cloud returned an oversized live deployment response.');
|
|
192
|
+
}
|
|
193
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
194
|
+
if (bytes.byteLength > MAX_RESPONSE_BYTES) {
|
|
195
|
+
throw new Error('Cloud returned an oversized live deployment response.');
|
|
196
|
+
}
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new Error('Cloud returned an invalid live deployment response.');
|
|
203
|
+
}
|
|
204
|
+
return parseResponse(parsed, input.target);
|
|
205
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.16.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.16.3",
|
|
4
|
+
"description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"clickhouse",
|
|
7
|
+
"typescript",
|
|
8
|
+
"analytics",
|
|
9
|
+
"semantic-layer",
|
|
10
|
+
"query-builder",
|
|
11
|
+
"orm",
|
|
12
|
+
"code-generation",
|
|
13
|
+
"chdb",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
5
16
|
"license": "Apache-2.0",
|
|
6
17
|
"type": "module",
|
|
7
18
|
"bin": {
|
|
@@ -19,8 +30,8 @@
|
|
|
19
30
|
"open": "^10.0.0",
|
|
20
31
|
"ora": "^8.0.1",
|
|
21
32
|
"prompts": "^2.4.2",
|
|
22
|
-
"@hypequery/deployment": "0.7.
|
|
23
|
-
"@hypequery/protocol": "0.10.
|
|
33
|
+
"@hypequery/deployment": "0.7.2",
|
|
34
|
+
"@hypequery/protocol": "0.10.2"
|
|
24
35
|
},
|
|
25
36
|
"optionalDependencies": {
|
|
26
37
|
"@napi-rs/keyring": "1.3.0"
|
|
@@ -34,13 +45,14 @@
|
|
|
34
45
|
"@vitest/coverage-v8": "^3.2.6",
|
|
35
46
|
"typescript": "^5.7.3",
|
|
36
47
|
"vitest": "^3.2.6",
|
|
37
|
-
"@hypequery/serve": "0.15.
|
|
48
|
+
"@hypequery/serve": "0.15.2"
|
|
38
49
|
},
|
|
39
50
|
"repository": {
|
|
40
51
|
"type": "git",
|
|
41
|
-
"url": "https://github.com/hypequery/hypequery.git"
|
|
52
|
+
"url": "git+https://github.com/hypequery/hypequery.git",
|
|
53
|
+
"directory": "packages/cli"
|
|
42
54
|
},
|
|
43
|
-
"homepage": "https://
|
|
55
|
+
"homepage": "https://hypequery.com",
|
|
44
56
|
"bugs": {
|
|
45
57
|
"url": "https://github.com/hypequery/hypequery/issues"
|
|
46
58
|
},
|