@hypequery/cli 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -239,23 +239,53 @@ arguments, keeping them out of shell history. The submission endpoint must not
239
239
  contain credentials or a URL fragment, and must use HTTPS except for
240
240
  `127.0.0.1`/`localhost`, which is permitted for local development and warns that
241
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.
242
+ key, so an unchanged release can be submitted safely again. An accepted release
243
+ becomes live immediately. The CLI pins the upload to the current activation
244
+ revision, so a concurrent deploy or restore returns a conflict. If the current
245
+ release was restored, pass `--replace-restored` to confirm that a different
246
+ release should replace it.
246
247
 
247
248
  Options:
248
249
 
249
250
  - `--release <path>`: required target-bound release JSON
250
251
  - `--endpoint <url>`: HTTPS submission endpoint; requires `HYPEQUERY_API_TOKEN`
252
+ - `--replace-restored`: intentionally replace a restored live release
253
+
254
+ ### `hypequery pull`
255
+
256
+ Downloads the exact multi-file TypeScript source snapshot stored with the live
257
+ release. Pull requires an interactive Cloud credential with source-read access;
258
+ run `hypequery login` again if the credential predates this capability.
259
+
260
+ ```bash
261
+ npx hypequery pull
262
+ ```
263
+
264
+ By default the snapshot is written to a new release-specific directory under
265
+ `.hypequery/live/<environment>/`. Use `--output <directory>` to choose another
266
+ new directory. Pull never overwrites an existing path.
267
+
268
+ ### `hypequery diff [source]`
269
+
270
+ Compares the local TypeScript dependency graph with the live release snapshot
271
+ and reports added (`A`), modified (`M`), and deleted (`D`) files. The deployed
272
+ entrypoint is used when `source` is omitted.
273
+
274
+ ```bash
275
+ npx hypequery diff analytics/api.ts
276
+ ```
277
+
278
+ Both commands use the target selected by `hypequery login`. Advanced and CI
279
+ usage can pass `--project`, `--environment`, and `--endpoint` explicitly.
251
280
 
252
281
  ## Non-interactive Setup
253
282
 
254
283
  ### Cloud deployment targets in CI
255
284
 
256
- Git branch context is only a default for interactive login. Select a stable
257
- target explicitly when the same branch or commit deploys to more than one
258
- environment:
285
+ Login selects a stable Cloud deployment target. The deployment itself captures
286
+ the checked-out Git branch, commit, and dirty state as source provenance; none
287
+ of those values select or create an environment. Select the destination
288
+ explicitly when the same branch or commit deploys to more than one environment:
259
289
 
260
290
  ```bash
261
291
  npx hypequery login --environment development
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;AA0BpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAgOD,OAAO,EAAE,OAAO,EAAE,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() {
@@ -34,8 +35,7 @@ program
34
35
  .command('login')
35
36
  .description('Authorize this machine with Hypequery Cloud')
36
37
  .option('--cloud-url <url>', 'Cloud origin (or HYPEQUERY_CLOUD_URL)')
37
- .option('--environment <environment>', 'Stable deployment target; takes precedence over the Git branch')
38
- .option('--no-branch', 'Do not send the current Git branch (or HYPEQUERY_CLI_SEND_BRANCH=0)')
38
+ .option('--environment <environment>', 'Stable deployment target')
39
39
  .action(runCommand(async (options) => {
40
40
  await loginCommand(options);
41
41
  }));
@@ -45,6 +45,25 @@ program
45
45
  .action(runCommand(async () => {
46
46
  await logoutCommand();
47
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
+ }));
48
67
  function runCommand(action) {
49
68
  return async (...args) => {
50
69
  try {
@@ -168,6 +187,7 @@ program
168
187
  .description('Submit a prebuilt deployment bundle and release')
169
188
  .requiredOption('--release <path>', 'Target-bound release JSON path')
170
189
  .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
190
+ .option('--replace-restored', 'Intentionally replace a restored live release')
171
191
  .action(runCommand(async (bundle, options) => {
172
192
  await submitDeploymentCommand(bundle, options);
173
193
  }));
@@ -181,6 +201,7 @@ program
181
201
  .option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
182
202
  .option('--no-source', 'Exclude project source files from the deployment bundle')
183
203
  .option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
204
+ .option('--replace-restored', 'Intentionally replace a restored live release')
184
205
  .action(runCommand(async (source, options) => {
185
206
  await deployCommand(source, options);
186
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,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAC5C,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;CACnB;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;CACvE;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;AA8GD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,uBAA4B,EACrC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CAwDvC;AAmDD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAmDvC"}
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"}
@@ -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 { loadCloudCredential, } from '../utils/cloud-credential-store.js';
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
+ }
@@ -1,14 +1,11 @@
1
1
  import { deleteCloudCredential, loadCloudCredential, saveCloudCredential } from '../utils/cloud-credential-store.js';
2
2
  export interface LoginOptions {
3
3
  readonly cloudUrl?: string;
4
- /** Commander sets this to false for `--no-branch`. */
5
- readonly branch?: boolean;
6
- /** Stable Cloud deployment target. This always takes precedence over Git branch context. */
4
+ /** Stable Cloud deployment target. */
7
5
  readonly environment?: string;
8
6
  }
9
7
  export interface LoginDependencies {
10
8
  readonly fetch?: typeof fetch;
11
- readonly getGitBranch?: () => Promise<string | null>;
12
9
  readonly openBrowser?: (url: string) => Promise<unknown>;
13
10
  readonly now?: () => number;
14
11
  readonly requestTimeoutMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,qBAAqB,EACrB,mBAAmB,EAGnB,mBAAmB,EAEpB,MAAM,oCAAoC,CAAC;AAiB5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,4FAA4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAaD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACzD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAsJD,wBAAsB,YAAY,CAChC,OAAO,GAAE,YAAiB,EAC1B,YAAY,GAAE,iBAAsB,iBAgFrC;AAED,wBAAsB,aAAa,CAAC,YAAY,GAAE,kBAAuB,iBAoCxE"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,qBAAqB,EACrB,mBAAmB,EAGnB,mBAAmB,EAEpB,MAAM,oCAAoC,CAAC;AAgB5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACrD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACzD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAsJD,wBAAsB,YAAY,CAChC,OAAO,GAAE,YAAiB,EAC1B,YAAY,GAAE,iBAAsB,iBAyErC;AAED,wBAAsB,aAAa,CAAC,YAAY,GAAE,kBAAuB,iBAoCxE"}
@@ -2,8 +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 { CLOUD_DEPLOYMENT_SCOPE, deleteCloudCredential, loadCloudCredential, normalizeCloudDeploymentEndpoint, normalizeCloudOrigin, saveCloudCredential, } from '../utils/cloud-credential-store.js';
6
- import { currentGitBranch } from '../utils/git-branch.js';
5
+ import { CLOUD_SOURCE_SCOPE, deleteCloudCredential, loadCloudCredential, normalizeCloudDeploymentEndpoint, normalizeCloudOrigin, saveCloudCredential, } from '../utils/cloud-credential-store.js';
7
6
  import { logger } from '../utils/logger.js';
8
7
  const DEFAULT_CLOUD_URL = 'https://cloud.hypequery.com';
9
8
  const LOGIN_TIMEOUT_MS = 5 * 60_000;
@@ -17,17 +16,6 @@ const MAX_TOKEN_LIFETIME_MS = 13 * 60 * 60_000;
17
16
  // CLI the day Cloud rotates its token format.
18
17
  const TOKEN_PATTERN = /^hqdp_[A-Za-z0-9_-]{16,512}$/;
19
18
  const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
20
- /**
21
- * Branch names carry internal detail (customer names, embargoed identifiers),
22
- * so both an explicit `--no-branch` and the environment variable suppress the
23
- * parameter. The flag wins because it is the more specific signal.
24
- */
25
- function branchContextEnabled(options) {
26
- if (options.branch === false)
27
- return false;
28
- const configured = process.env.HYPEQUERY_CLI_SEND_BRANCH?.trim().toLowerCase();
29
- return configured !== '0' && configured !== 'false' && configured !== 'no';
30
- }
31
19
  function callbackHtml(success) {
32
20
  const title = success ? 'CLI authorized' : 'Authorization failed';
33
21
  const message = success
@@ -143,7 +131,7 @@ function tokenResponse(input, origin, now) {
143
131
  || value.token_type !== 'Bearer'
144
132
  || typeof value.expires_at !== 'string'
145
133
  || !Number.isFinite(Date.parse(value.expires_at))
146
- || value.scope !== CLOUD_DEPLOYMENT_SCOPE
134
+ || value.scope !== CLOUD_SOURCE_SCOPE
147
135
  || typeof value.deployment_endpoint !== 'string'
148
136
  || value.deployment_target === undefined) {
149
137
  throw new Error('Cloud returned an invalid CLI token response.');
@@ -174,12 +162,6 @@ export async function loginCommand(options = {}, dependencies = {}) {
174
162
  const state = randomBytes(24).toString('base64url');
175
163
  const verifier = randomBytes(32).toString('base64url');
176
164
  const challenge = createHash('sha256').update(verifier).digest('base64url');
177
- // Resolve branch context before the loopback listener opens: shelling out to
178
- // git should not hold a server socket, and a caller-supplied resolver that
179
- // rejects must not leak one.
180
- const branch = branchContextEnabled(options)
181
- ? await (dependencies.getGitBranch ?? currentGitBranch)()
182
- : null;
183
165
  let environment;
184
166
  if (options.environment !== undefined) {
185
167
  try {
@@ -198,8 +180,6 @@ export async function loginCommand(options = {}, dependencies = {}) {
198
180
  authorizeUrl.searchParams.set('code_challenge', challenge);
199
181
  authorizeUrl.searchParams.set('code_challenge_method', 'S256');
200
182
  authorizeUrl.searchParams.set('state', state);
201
- if (branch)
202
- authorizeUrl.searchParams.set('branch', branch);
203
183
  if (environment)
204
184
  authorizeUrl.searchParams.set('environment', environment);
205
185
  try {
@@ -229,8 +209,8 @@ export async function loginCommand(options = {}, dependencies = {}) {
229
209
  const credential = tokenResponse(body, origin, (dependencies.now ?? Date.now)());
230
210
  await (dependencies.saveCredential ?? saveCloudCredential)(credential);
231
211
  logger.success('Logged in to Hypequery Cloud');
232
- // Cloud resolves the target, and branch context only expresses intent, so
233
- // print what was actually issued rather than what was requested.
212
+ // Cloud resolves the target, so print what was actually issued rather than
213
+ // what was requested.
234
214
  if (credential.target) {
235
215
  logger.info(`Deployments target ${credential.target.project} / ${credential.target.environment}`);
236
216
  }
@@ -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;AAGtD,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,iBAiEpD;AAED,wBAAsB,mBAAmB,CACvC,YAAY,GAAE,gCAAqC,GAClD,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAsBvC;AA0BD,wBAAsB,qBAAqB,CACzC,YAAY,GAAE,gCAAqC,iBA0BpD"}
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
+ }
@@ -19,6 +19,7 @@ export interface DeploymentBundleSourceSnapshot {
19
19
  readonly kind: 'git';
20
20
  readonly commit: string;
21
21
  readonly dirty: boolean;
22
+ readonly branch?: string;
22
23
  };
23
24
  }
24
25
  export interface WrittenDeploymentBundle {
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAGtE,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,CAAC;AACF,YAAY,EAAE,wBAAwB,EAAE,CAAC;AAEzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,gCAAgC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;CAC/C;AA0ID,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,EACpD,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CA+FlC;AAoCD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF"}
1
+ {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAGtE,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,CAAC;AACF,YAAY,EAAE,wBAAwB,EAAE,CAAC;AAEzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,gCAAgC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;CAC/C;AA0ID,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,EACpD,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CA+FlC;AAoCD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF"}
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-source-snapshot.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-source-snapshot.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AAqEhC,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,8BAA8B,CAAC,CAkDzC"}
1
+ {"version":3,"file":"deployment-source-snapshot.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-source-snapshot.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AA0EhC,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,8BAA8B,CAAC,CAkDzC"}
@@ -3,6 +3,7 @@ import { constants, lstat, open, realpath } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, } from '@hypequery/protocol';
5
5
  import { build } from 'esbuild';
6
+ import { currentGitBranch } from './git-branch.js';
6
7
  import { findNearestTsconfig } from './load-api.js';
7
8
  function portableProjectPath(projectRoot, absolutePath) {
8
9
  const relative = path.relative(projectRoot, absolutePath);
@@ -47,7 +48,10 @@ function gitOutput(projectRoot, arguments_) {
47
48
  });
48
49
  }
49
50
  async function sourceRevision(projectRoot) {
50
- const commit = await gitOutput(projectRoot, ['rev-parse', 'HEAD']);
51
+ const [commit, branch] = await Promise.all([
52
+ gitOutput(projectRoot, ['rev-parse', 'HEAD']),
53
+ currentGitBranch(projectRoot),
54
+ ]);
51
55
  if (!commit || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(commit))
52
56
  return undefined;
53
57
  const status = await gitOutput(projectRoot, ['status', '--porcelain', '--untracked-files=all']);
@@ -57,6 +61,7 @@ async function sourceRevision(projectRoot) {
57
61
  kind: 'git',
58
62
  commit,
59
63
  dirty: status.length > 0,
64
+ ...(branch ? { branch } : {}),
60
65
  });
61
66
  }
62
67
  export async function captureDeploymentSourceSnapshot(apiPath) {
@@ -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,CAsF3B"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "Command-line interface for hypequery",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "open": "^10.0.0",
20
20
  "ora": "^8.0.1",
21
21
  "prompts": "^2.4.2",
22
- "@hypequery/deployment": "0.7.0",
23
- "@hypequery/protocol": "0.10.0"
22
+ "@hypequery/deployment": "0.7.1",
23
+ "@hypequery/protocol": "0.10.1"
24
24
  },
25
25
  "optionalDependencies": {
26
26
  "@napi-rs/keyring": "1.3.0"