@hypequery/cli 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,6 +204,33 @@ Options:
204
204
  - `--environment <environment>`: required target environment identifier
205
205
  - `--output <path>`: release JSON path, default beside the bundle
206
206
 
207
+ ### `hypequery deploy`
208
+
209
+ Submits a verified deployment bundle with an already-prepared target-bound
210
+ release. The command verifies both inputs again, requires their bundle
211
+ identities to match, and streams only the files declared by the bundle.
212
+
213
+ ```bash
214
+ HYPEQUERY_API_TOKEN=<token> \
215
+ npx hypequery deploy analytics/hypequery-deployment \
216
+ --release analytics/hypequery-deployment.release.json \
217
+ --endpoint https://deploy.example.com/v1/releases
218
+ ```
219
+
220
+ The token is accepted only through `HYPEQUERY_API_TOKEN`, keeping it out of
221
+ shell history. The submission endpoint must use HTTPS, may also be supplied by
222
+ `HYPEQUERY_DEPLOYMENT_ENDPOINT`, and must not contain credentials or a URL
223
+ fragment. The release identity is sent as the idempotency key, so an unchanged
224
+ release can be submitted safely again.
225
+
226
+ This command submits immutable deployment inputs. Activation, status changes,
227
+ promotion, and rollback remain control-plane operations.
228
+
229
+ Options:
230
+
231
+ - `--release <path>`: required target-bound release JSON
232
+ - `--endpoint <url>`: HTTPS submission endpoint; defaults to `HYPEQUERY_DEPLOYMENT_ENDPOINT`
233
+
207
234
  ## Non-interactive Setup
208
235
 
209
236
  For ClickHouse, `hypequery init --no-interactive` reads:
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;AAcpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAsKD,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;AAkBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAgLD,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { generateCommand } from './commands/generate.js';
7
7
  import { generateDatasetsCommand } from './commands/generate-datasets.js';
8
8
  import { generateManifestCommand } from './commands/generate-manifest.js';
9
9
  import { buildDeploymentCommand, prepareDeploymentReleaseCommand, validateDeploymentCommand, } from './commands/deployment.js';
10
+ import { deployCommand, } from './commands/deploy.js';
10
11
  const program = new Command();
11
12
  function getCliVersion() {
12
13
  try {
@@ -138,6 +139,14 @@ program
138
139
  .action(runCommand(async (bundle, options) => {
139
140
  await prepareDeploymentReleaseCommand(bundle, options);
140
141
  }));
142
+ program
143
+ .command('deploy <bundle>')
144
+ .description('Submit a verified deployment bundle and release')
145
+ .requiredOption('--release <path>', 'Target-bound release JSON path')
146
+ .option('--endpoint <url>', 'HTTPS submission endpoint (or HYPEQUERY_DEPLOYMENT_ENDPOINT)')
147
+ .action(runCommand(async (bundle, options) => {
148
+ await deployCommand(bundle, options);
149
+ }));
141
150
  // Help command
142
151
  program
143
152
  .command('help [command]')
@@ -171,6 +180,7 @@ program.on('--help', () => {
171
180
  console.log(' hypequery deployment:build analytics/api.ts');
172
181
  console.log(' hypequery deployment:validate analytics/hypequery-deployment');
173
182
  console.log(' hypequery deployment:release analytics/hypequery-deployment --project my-project --environment production');
183
+ console.log(' hypequery deploy analytics/hypequery-deployment --release analytics/hypequery-deployment.release.json');
174
184
  console.log('');
175
185
  console.log('Docs: https://hypequery.com/docs');
176
186
  });
@@ -0,0 +1,11 @@
1
+ import { createHttpDeploymentUploadTransport, type DeploymentSubmissionResponse } from '../utils/deployment-upload.js';
2
+ export interface DeployOptions {
3
+ release?: string;
4
+ endpoint?: string;
5
+ }
6
+ export interface DeployDependencies {
7
+ readonly env?: Readonly<Record<string, string | undefined>>;
8
+ readonly createTransport?: typeof createHttpDeploymentUploadTransport;
9
+ }
10
+ export declare function deployCommand(bundlePath: string | undefined, options?: DeployOptions, dependencies?: DeployDependencies): Promise<DeploymentSubmissionResponse>;
11
+ //# sourceMappingURL=deploy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,mCAAmC,EAEnC,KAAK,4BAA4B,EAElC,MAAM,+BAA+B,CAAC;AAMvC,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,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;CACvE;AA4DD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CA0DvC"}
@@ -0,0 +1,102 @@
1
+ import { constants, open } from 'node:fs/promises';
2
+ import { prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
3
+ import { verifyDeploymentBundle } from '../utils/deployment-bundle.js';
4
+ import { createHttpDeploymentUploadTransport, DeploymentUploadError, } from '../utils/deployment-upload.js';
5
+ import { logger } from '../utils/logger.js';
6
+ const MAX_RELEASE_FILE_BYTES = 16 * 1024;
7
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
8
+ async function readReleaseFile(releasePath) {
9
+ let handle;
10
+ let bytes;
11
+ let fileFailure;
12
+ try {
13
+ handle = await open(releasePath, constants.O_RDONLY | constants.O_NOFOLLOW);
14
+ const stat = await handle.stat();
15
+ if (!stat.isFile())
16
+ throw new Error('Release JSON must be a regular file.');
17
+ if (stat.size > MAX_RELEASE_FILE_BYTES) {
18
+ throw new Error('Release JSON exceeds 16384 bytes.');
19
+ }
20
+ const buffer = new Uint8Array(MAX_RELEASE_FILE_BYTES + 1);
21
+ let total = 0;
22
+ while (total < buffer.byteLength) {
23
+ const { bytesRead } = await handle.read(buffer, total, buffer.byteLength - total, total);
24
+ if (bytesRead === 0)
25
+ break;
26
+ total += bytesRead;
27
+ }
28
+ if (total > MAX_RELEASE_FILE_BYTES) {
29
+ throw new Error('Release JSON exceeds 16384 bytes.');
30
+ }
31
+ bytes = buffer.subarray(0, total);
32
+ }
33
+ catch (error) {
34
+ fileFailure = { error };
35
+ }
36
+ if (handle) {
37
+ try {
38
+ await handle.close();
39
+ }
40
+ catch (error) {
41
+ fileFailure ??= { error };
42
+ }
43
+ }
44
+ if (fileFailure) {
45
+ throw new Error(`Cannot read deployment release file: ${releasePath}\n\n`
46
+ + (fileFailure.error instanceof Error
47
+ ? fileFailure.error.message
48
+ : String(fileFailure.error)));
49
+ }
50
+ try {
51
+ return JSON.parse(utf8Decoder.decode(bytes));
52
+ }
53
+ catch (error) {
54
+ throw new Error(`Invalid deployment release JSON: ${releasePath}\n\n`
55
+ + (error instanceof Error ? error.message : String(error)));
56
+ }
57
+ }
58
+ function requiredConfiguration(value, message) {
59
+ if (value === undefined || value.length === 0)
60
+ throw new Error(message);
61
+ return value;
62
+ }
63
+ export async function deployCommand(bundlePath, options = {}, dependencies = {}) {
64
+ if (!bundlePath) {
65
+ throw new Error('Missing deployment bundle path.\n\n'
66
+ + 'Usage: hypequery deploy analytics/hypequery-deployment '
67
+ + '--release analytics/hypequery-deployment.release.json');
68
+ }
69
+ const releasePath = requiredConfiguration(options.release, 'Missing required --release <path>.');
70
+ const env = dependencies.env ?? process.env;
71
+ const endpoint = requiredConfiguration(options.endpoint ?? env.HYPEQUERY_DEPLOYMENT_ENDPOINT, 'Missing deployment endpoint. Pass --endpoint or set HYPEQUERY_DEPLOYMENT_ENDPOINT.');
72
+ const token = requiredConfiguration(env.HYPEQUERY_API_TOKEN, 'Missing HYPEQUERY_API_TOKEN.');
73
+ let bundle;
74
+ try {
75
+ bundle = await verifyDeploymentBundle(bundlePath);
76
+ }
77
+ catch (error) {
78
+ throw new Error(`Cannot push an invalid deployment bundle: ${bundlePath}\n\n`
79
+ + (error instanceof Error ? error.message : String(error)));
80
+ }
81
+ const releaseInput = await readReleaseFile(releasePath);
82
+ let release;
83
+ try {
84
+ release = prepareProtocolDeploymentReleaseEnvelope(releaseInput);
85
+ }
86
+ catch (error) {
87
+ throw new Error(`Invalid deployment release: ${releasePath}\n\n`
88
+ + (error instanceof Error ? error.message : String(error)));
89
+ }
90
+ if (release.release.bundleIdentity !== bundle.identity) {
91
+ throw new DeploymentUploadError('HQ_UPLOAD_IDENTITY_MISMATCH', 'Release bundle identity does not match the verified deployment bundle.');
92
+ }
93
+ const createTransport = dependencies.createTransport ?? createHttpDeploymentUploadTransport;
94
+ const transportOptions = { endpoint, token };
95
+ const result = await createTransport(transportOptions).submit(bundle, release);
96
+ logger.success(result.status === 'accepted'
97
+ ? 'Deployment release accepted'
98
+ : 'Deployment release already exists');
99
+ logger.info(`Release identity: ${result.releaseIdentity}`);
100
+ logger.info(`Bundle identity: ${result.bundleIdentity}`);
101
+ return result;
102
+ }
@@ -1,6 +1,8 @@
1
1
  import { type PreparedProtocolDeploymentContract, type ProtocolDeploymentBundleManifest, type ProtocolDeploymentContract } from '@hypequery/protocol';
2
- export declare const DEPLOYMENT_BUNDLE_MANIFEST = "bundle.json";
3
- export declare const DEPLOYMENT_BUNDLE_CONTRACT = "deployment.json";
2
+ import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle } from '@hypequery/deployment';
3
+ import type { VerifiedDeploymentBundle } from '@hypequery/deployment';
4
+ export { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, };
5
+ export type { VerifiedDeploymentBundle };
4
6
  export interface DeploymentBundleRuntimeFile {
5
7
  readonly runtime: 'node' | 'python';
6
8
  readonly sha256: string;
@@ -12,8 +14,6 @@ export interface WrittenDeploymentBundle {
12
14
  readonly identity: string;
13
15
  readonly contract: ProtocolDeploymentContract;
14
16
  }
15
- export type VerifiedDeploymentBundle = WrittenDeploymentBundle;
16
17
  export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract, runtimeFiles: readonly DeploymentBundleRuntimeFile[]): Promise<WrittenDeploymentBundle>;
17
18
  export declare function readDeploymentRuntimeFile(filePath: string): Promise<Uint8Array>;
18
- export declare function verifyDeploymentBundle(bundleDirectory: string): Promise<VerifiedDeploymentBundle>;
19
19
  //# sourceMappingURL=deployment-bundle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAaA,OAAO,EAIL,KAAK,kCAAkC,EAEvC,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,0BAA0B,gBAAgB,CAAC;AACxD,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAE5D,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,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;AAED,MAAM,MAAM,wBAAwB,GAAG,uBAAuB,CAAC;AA+F/D,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,GACnD,OAAO,CAAC,uBAAuB,CAAC,CA6ElC;AAgDD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF;AA+FD,wBAAsB,sBAAsB,CAC1C,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,wBAAwB,CAAC,CA+DnC"}
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,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;AA+FD,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,YAAY,EAAE,SAAS,2BAA2B,EAAE,GACnD,OAAO,CAAC,uBAAuB,CAAC,CA6ElC;AAoCD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMrF"}
@@ -1,11 +1,10 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { constants, lstat, mkdir, mkdtemp, open, readdir, rename, rm, writeFile, } from 'node:fs/promises';
2
+ import { constants, lstat, mkdir, mkdtemp, open, rename, rm, writeFile, } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, prepareProtocolDeploymentBundleManifest, prepareProtocolDeploymentContract, } from '@hypequery/protocol';
4
+ import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, prepareProtocolDeploymentBundleManifest, } from '@hypequery/protocol';
5
+ import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '@hypequery/deployment';
5
6
  import { logger } from './logger.js';
6
- export const DEPLOYMENT_BUNDLE_MANIFEST = 'bundle.json';
7
- export const DEPLOYMENT_BUNDLE_CONTRACT = 'deployment.json';
8
- const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
7
+ export { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, };
9
8
  const utf8Encoder = new TextEncoder();
10
9
  function sha256(bytes) {
11
10
  return createHash('sha256').update(bytes).digest('hex');
@@ -185,138 +184,6 @@ async function readBoundedAbsoluteRegularFile(absolutePath, displayPath, maximum
185
184
  await handle?.close();
186
185
  }
187
186
  }
188
- async function readBoundedRegularFile(root, relativePath, maximum) {
189
- return readBoundedAbsoluteRegularFile(path.join(root, ...relativePath.split('/')), relativePath, maximum);
190
- }
191
187
  export async function readDeploymentRuntimeFile(filePath) {
192
188
  return readBoundedAbsoluteRegularFile(path.resolve(filePath), filePath, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
193
189
  }
194
- function expectedDirectories(files) {
195
- const result = new Set();
196
- for (const file of files) {
197
- const segments = file.split('/');
198
- for (let index = 1; index < segments.length; index += 1) {
199
- result.add(segments.slice(0, index).join('/'));
200
- }
201
- }
202
- return result;
203
- }
204
- async function verifyExactEntries(root, manifest) {
205
- const expectedFiles = new Set([
206
- DEPLOYMENT_BUNDLE_MANIFEST,
207
- manifest.deployment.path,
208
- ...manifest.artifacts.map(artifact => artifact.path),
209
- ]);
210
- const expectedDirectoryPaths = expectedDirectories([...expectedFiles]);
211
- const seenFiles = new Set();
212
- const seenDirectories = new Set();
213
- async function visit(relativeDirectory) {
214
- const absoluteDirectory = relativeDirectory
215
- ? path.join(root, ...relativeDirectory.split('/'))
216
- : root;
217
- const names = await readdir(absoluteDirectory);
218
- names.sort();
219
- for (const name of names) {
220
- const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
221
- const stat = await lstat(path.join(absoluteDirectory, name));
222
- if (stat.isSymbolicLink()) {
223
- throw new Error(`Bundle entries must not be symbolic links: ${relativePath}`);
224
- }
225
- if (stat.isDirectory()) {
226
- if (!expectedDirectoryPaths.has(relativePath)) {
227
- throw new Error(`Deployment bundle contains an undeclared directory: ${relativePath}`);
228
- }
229
- seenDirectories.add(relativePath);
230
- await visit(relativePath);
231
- }
232
- else if (stat.isFile()) {
233
- if (!expectedFiles.has(relativePath)) {
234
- throw new Error(`Deployment bundle contains an undeclared file: ${relativePath}`);
235
- }
236
- seenFiles.add(relativePath);
237
- }
238
- else {
239
- throw new Error(`Bundle entry is not a regular file or directory: ${relativePath}`);
240
- }
241
- }
242
- }
243
- await visit('');
244
- const missingFiles = [...expectedFiles].filter(file => !seenFiles.has(file)).sort();
245
- if (missingFiles.length > 0) {
246
- throw new Error(`Deployment bundle is missing files: ${missingFiles.join(', ')}`);
247
- }
248
- const missingDirectories = [...expectedDirectoryPaths]
249
- .filter(directory => !seenDirectories.has(directory))
250
- .sort();
251
- if (missingDirectories.length > 0) {
252
- throw new Error(`Deployment bundle is missing directories: ${missingDirectories.join(', ')}`);
253
- }
254
- }
255
- function verifyFile(bytes, file) {
256
- if (bytes.byteLength !== file.byteLength) {
257
- throw new Error(`Bundle entry byte length does not match the manifest: ${file.path}`);
258
- }
259
- if (sha256(bytes) !== file.sha256) {
260
- throw new Error(`Bundle entry SHA-256 does not match the manifest: ${file.path}`);
261
- }
262
- }
263
- function verifyRuntimeReferences(contract, artifacts) {
264
- requireClosedContractArtifactSet(contract);
265
- const declared = new Map(contract.artifacts.map(artifact => [artifact.artifactSha256, artifact.runtime]));
266
- if (declared.size !== artifacts.length) {
267
- throw new Error('Bundle runtime artifacts do not match the deployment contract.');
268
- }
269
- for (const artifact of artifacts) {
270
- if (declared.get(artifact.sha256) !== artifact.runtime) {
271
- throw new Error(`Bundle artifact ${artifact.sha256} does not match a deployment runtime reference.`);
272
- }
273
- }
274
- }
275
- export async function verifyDeploymentBundle(bundleDirectory) {
276
- const root = path.resolve(bundleDirectory);
277
- const rootStat = await lstat(root);
278
- if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
279
- throw new Error(`Deployment bundle path must be a regular directory: ${bundleDirectory}`);
280
- }
281
- const manifestBytes = await readBoundedRegularFile(root, DEPLOYMENT_BUNDLE_MANIFEST, 1024 * 1024);
282
- let input;
283
- try {
284
- input = JSON.parse(utf8Decoder.decode(manifestBytes));
285
- }
286
- catch (error) {
287
- throw new Error(`Invalid deployment bundle manifest: ${path.join(root, DEPLOYMENT_BUNDLE_MANIFEST)}\n\n`
288
- + (error instanceof Error ? error.message : String(error)));
289
- }
290
- const preparedManifest = prepareProtocolDeploymentBundleManifest(input);
291
- const expectedManifestBytes = utf8Encoder.encode(`${preparedManifest.canonical}\n`);
292
- if (!Buffer.from(manifestBytes).equals(Buffer.from(expectedManifestBytes))) {
293
- throw new Error('Deployment bundle manifest must contain canonical JSON followed by one newline.');
294
- }
295
- await verifyExactEntries(root, preparedManifest.manifest);
296
- const deployment = preparedManifest.manifest.deployment;
297
- const deploymentBytes = await readBoundedRegularFile(root, deployment.path, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxDeploymentBytes);
298
- verifyFile(deploymentBytes, deployment);
299
- let contractInput;
300
- try {
301
- contractInput = JSON.parse(utf8Decoder.decode(deploymentBytes));
302
- }
303
- catch (error) {
304
- throw new Error(`Invalid deployment JSON in bundle: ${deployment.path}\n\n`
305
- + (error instanceof Error ? error.message : String(error)));
306
- }
307
- const preparedContract = prepareProtocolDeploymentContract(contractInput);
308
- if (preparedContract.identity !== deployment.identity) {
309
- throw new Error('Deployment identity does not match the bundle manifest.');
310
- }
311
- for (const artifact of preparedManifest.manifest.artifacts) {
312
- const bytes = await readBoundedRegularFile(root, artifact.path, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
313
- verifyFile(bytes, artifact);
314
- }
315
- verifyRuntimeReferences(preparedContract.contract, preparedManifest.manifest.artifacts);
316
- return Object.freeze({
317
- directory: root,
318
- manifest: preparedManifest.manifest,
319
- identity: preparedManifest.identity,
320
- contract: preparedContract.contract,
321
- });
322
- }
@@ -0,0 +1,37 @@
1
+ import type { ReadableStream } from 'node:stream/web';
2
+ import { type PreparedProtocolDeploymentReleaseEnvelope } from '@hypequery/protocol';
3
+ import { type VerifiedDeploymentBundle } from './deployment-bundle.js';
4
+ import type { DeploymentSubmissionResponse } from '@hypequery/deployment';
5
+ export type { DeploymentSubmissionResponse } from '@hypequery/deployment';
6
+ export type DeploymentUploadErrorCode = 'HQ_UPLOAD_CONFIGURATION' | 'HQ_UPLOAD_IDENTITY_MISMATCH' | 'HQ_UPLOAD_BUNDLE_CHANGED' | 'HQ_UPLOAD_NETWORK' | 'HQ_UPLOAD_REJECTED' | 'HQ_UPLOAD_INVALID_RESPONSE';
7
+ export declare class DeploymentUploadError extends Error {
8
+ readonly code: DeploymentUploadErrorCode;
9
+ readonly status?: number;
10
+ constructor(code: DeploymentUploadErrorCode, message: string, status?: number);
11
+ }
12
+ export interface DeploymentHttpResponse {
13
+ readonly ok: boolean;
14
+ readonly status: number;
15
+ readonly statusText: string;
16
+ readonly body: ReadableStream<Uint8Array> | null;
17
+ }
18
+ export interface DeploymentFetchInit {
19
+ readonly method: 'POST';
20
+ readonly headers: Readonly<Record<string, string>>;
21
+ readonly body: AsyncIterable<Uint8Array>;
22
+ readonly duplex: 'half';
23
+ readonly redirect: 'error';
24
+ readonly signal: AbortSignal;
25
+ }
26
+ export type DeploymentFetch = (input: string, init: DeploymentFetchInit) => Promise<DeploymentHttpResponse>;
27
+ export interface HttpDeploymentUploadTransportOptions {
28
+ readonly endpoint: string;
29
+ readonly token: string;
30
+ readonly timeoutMs?: number;
31
+ readonly fetch?: DeploymentFetch;
32
+ }
33
+ export interface DeploymentUploadTransport {
34
+ submit(bundle: VerifiedDeploymentBundle, release: PreparedProtocolDeploymentReleaseEnvelope): Promise<DeploymentSubmissionResponse>;
35
+ }
36
+ export declare function createHttpDeploymentUploadTransport(options: HttpDeploymentUploadTransportOptions): DeploymentUploadTransport;
37
+ //# sourceMappingURL=deployment-upload.d.ts.map
@@ -0,0 +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;AAChC,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;AAmVD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,yBAAyB,CAsF3B"}
@@ -0,0 +1,359 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants, open } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { prepareProtocolDeploymentBundleManifest, prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
5
+ import { DEPLOYMENT_BUNDLE_MANIFEST, } from './deployment-bundle.js';
6
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
7
+ const MAX_RESPONSE_BYTES = 64 * 1024;
8
+ const DEFAULT_TIMEOUT_MS = 120_000;
9
+ const textEncoder = new TextEncoder();
10
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
11
+ export class DeploymentUploadError extends Error {
12
+ code;
13
+ status;
14
+ constructor(code, message, status) {
15
+ super(`${code}: ${message}`);
16
+ this.name = 'DeploymentUploadError';
17
+ this.code = code;
18
+ if (status !== undefined)
19
+ this.status = status;
20
+ }
21
+ }
22
+ function configurationError(message) {
23
+ throw new DeploymentUploadError('HQ_UPLOAD_CONFIGURATION', message);
24
+ }
25
+ function endpointUrl(input) {
26
+ let url;
27
+ try {
28
+ url = new URL(input);
29
+ }
30
+ catch {
31
+ configurationError('Deployment endpoint must be an absolute HTTPS URL.');
32
+ }
33
+ if (url.protocol !== 'https:') {
34
+ configurationError('Deployment endpoint must use HTTPS.');
35
+ }
36
+ if (url.username || url.password) {
37
+ configurationError('Deployment endpoint must not contain credentials.');
38
+ }
39
+ if (url.hash)
40
+ configurationError('Deployment endpoint must not contain a URL fragment.');
41
+ return url.toString();
42
+ }
43
+ function bearerToken(input) {
44
+ if (input.length < 1 || input.length > 4096 || input.trim() !== input
45
+ || [...input].some(character => {
46
+ const code = character.charCodeAt(0);
47
+ return code < 0x21 || code > 0x7e;
48
+ })) {
49
+ configurationError('HYPEQUERY_API_TOKEN contains invalid characters or length.');
50
+ }
51
+ return input;
52
+ }
53
+ function timeout(input) {
54
+ const value = input ?? DEFAULT_TIMEOUT_MS;
55
+ if (!Number.isSafeInteger(value) || value < 1 || value > 10 * 60_000) {
56
+ configurationError('Deployment upload timeout must be between 1 and 600000 milliseconds.');
57
+ }
58
+ return value;
59
+ }
60
+ function multipartBundlePath(input) {
61
+ if (!/^[\x21-\x7e]+$/.test(input) || input.includes('"') || input.includes('\\')) {
62
+ throw new DeploymentUploadError('HQ_UPLOAD_BUNDLE_CHANGED', 'A bundle path cannot be safely encoded as a multipart header.');
63
+ }
64
+ return input;
65
+ }
66
+ function validateMultipartBundlePaths(bundle) {
67
+ multipartBundlePath(bundle.manifest.deployment.path);
68
+ for (const artifact of bundle.manifest.artifacts)
69
+ multipartBundlePath(artifact.path);
70
+ }
71
+ function partHeader(boundary, part) {
72
+ const headers = [
73
+ `--${boundary}`,
74
+ `Content-Disposition: form-data; name="${part.name}"; filename="${part.filename}"`,
75
+ `Content-Type: ${part.contentType}`,
76
+ ...(part.bundlePath
77
+ ? [`X-HypeQuery-Bundle-Path: ${multipartBundlePath(part.bundlePath)}`]
78
+ : []),
79
+ '',
80
+ '',
81
+ ].join('\r\n');
82
+ return textEncoder.encode(headers);
83
+ }
84
+ function uploadParts(bundle, release, manifest) {
85
+ const root = bundle.directory;
86
+ return Object.freeze([
87
+ {
88
+ kind: 'bytes',
89
+ name: 'release',
90
+ filename: 'release.json',
91
+ contentType: 'application/json',
92
+ bytes: release.bytes,
93
+ },
94
+ {
95
+ kind: 'bytes',
96
+ name: 'bundle',
97
+ filename: DEPLOYMENT_BUNDLE_MANIFEST,
98
+ contentType: 'application/json',
99
+ bundlePath: DEPLOYMENT_BUNDLE_MANIFEST,
100
+ bytes: textEncoder.encode(`${manifest.canonical}\n`),
101
+ },
102
+ {
103
+ kind: 'file',
104
+ name: 'bundle',
105
+ filename: path.basename(manifest.manifest.deployment.path),
106
+ contentType: 'application/json',
107
+ absolutePath: path.join(root, ...manifest.manifest.deployment.path.split('/')),
108
+ bundlePath: manifest.manifest.deployment.path,
109
+ byteLength: manifest.manifest.deployment.byteLength,
110
+ sha256: manifest.manifest.deployment.sha256,
111
+ },
112
+ ...manifest.manifest.artifacts.map(artifact => ({
113
+ kind: 'file',
114
+ name: 'bundle',
115
+ filename: path.basename(artifact.path),
116
+ contentType: 'application/octet-stream',
117
+ absolutePath: path.join(root, ...artifact.path.split('/')),
118
+ bundlePath: artifact.path,
119
+ byteLength: artifact.byteLength,
120
+ sha256: artifact.sha256,
121
+ })),
122
+ ]);
123
+ }
124
+ async function* streamVerifiedFile(part) {
125
+ let handle;
126
+ try {
127
+ handle = await open(part.absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
128
+ const initial = await handle.stat();
129
+ if (!initial.isFile() || initial.size !== part.byteLength) {
130
+ throw new DeploymentUploadError('HQ_UPLOAD_BUNDLE_CHANGED', `Bundle entry changed after verification: ${part.bundlePath}`);
131
+ }
132
+ const digest = createHash('sha256');
133
+ let bytesRead = 0;
134
+ for await (const value of handle.createReadStream({ autoClose: false })) {
135
+ const chunk = value;
136
+ bytesRead += chunk.byteLength;
137
+ if (bytesRead > part.byteLength) {
138
+ throw new DeploymentUploadError('HQ_UPLOAD_BUNDLE_CHANGED', `Bundle entry changed after verification: ${part.bundlePath}`);
139
+ }
140
+ digest.update(chunk);
141
+ yield chunk;
142
+ }
143
+ if (bytesRead !== part.byteLength || digest.digest('hex') !== part.sha256) {
144
+ throw new DeploymentUploadError('HQ_UPLOAD_BUNDLE_CHANGED', `Bundle entry changed after verification: ${part.bundlePath}`);
145
+ }
146
+ }
147
+ catch (error) {
148
+ if (error instanceof DeploymentUploadError)
149
+ throw error;
150
+ throw new DeploymentUploadError('HQ_UPLOAD_BUNDLE_CHANGED', `Cannot safely read verified bundle entry ${part.bundlePath}: `
151
+ + (error instanceof Error ? error.message : String(error)));
152
+ }
153
+ finally {
154
+ await handle?.close();
155
+ }
156
+ }
157
+ function multipartLength(boundary, parts) {
158
+ const separatorBytes = 2;
159
+ const ending = textEncoder.encode(`--${boundary}--\r\n`).byteLength;
160
+ return parts.reduce((total, part) => (total + partHeader(boundary, part).byteLength
161
+ + (part.kind === 'bytes' ? part.bytes.byteLength : part.byteLength)
162
+ + separatorBytes), ending);
163
+ }
164
+ async function* multipartBody(boundary, parts) {
165
+ const separator = textEncoder.encode('\r\n');
166
+ for (const part of parts) {
167
+ yield partHeader(boundary, part);
168
+ if (part.kind === 'bytes') {
169
+ yield part.bytes;
170
+ }
171
+ else {
172
+ yield* streamVerifiedFile(part);
173
+ }
174
+ yield separator;
175
+ }
176
+ yield textEncoder.encode(`--${boundary}--\r\n`);
177
+ }
178
+ async function readBoundedResponse(response) {
179
+ if (!response.body)
180
+ return '';
181
+ const reader = response.body.getReader();
182
+ const chunks = [];
183
+ let total = 0;
184
+ try {
185
+ for (;;) {
186
+ const { done, value } = await reader.read();
187
+ if (done)
188
+ break;
189
+ total += value.byteLength;
190
+ if (total > MAX_RESPONSE_BYTES) {
191
+ await reader.cancel().catch(() => undefined);
192
+ throw new DeploymentUploadError('HQ_UPLOAD_INVALID_RESPONSE', 'Deployment service response exceeds 65536 bytes.', response.status);
193
+ }
194
+ chunks.push(value);
195
+ }
196
+ }
197
+ finally {
198
+ reader.releaseLock();
199
+ }
200
+ const bytes = new Uint8Array(total);
201
+ let offset = 0;
202
+ for (const chunk of chunks) {
203
+ bytes.set(chunk, offset);
204
+ offset += chunk.byteLength;
205
+ }
206
+ try {
207
+ return textDecoder.decode(bytes);
208
+ }
209
+ catch {
210
+ throw new DeploymentUploadError('HQ_UPLOAD_INVALID_RESPONSE', 'Deployment service response is not valid UTF-8.', response.status);
211
+ }
212
+ }
213
+ function exactRecord(value, fields) {
214
+ if (typeof value !== 'object' || value === null || Array.isArray(value)
215
+ || Object.getPrototypeOf(value) !== Object.prototype)
216
+ return undefined;
217
+ const record = value;
218
+ const keys = Object.keys(record).sort();
219
+ const expected = [...fields].sort();
220
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
221
+ return undefined;
222
+ }
223
+ return record;
224
+ }
225
+ function validateSubmissionResponse(input, bundleIdentity, releaseIdentity, status) {
226
+ const value = exactRecord(input, [
227
+ 'kind',
228
+ 'version',
229
+ 'status',
230
+ 'releaseIdentity',
231
+ 'bundleIdentity',
232
+ ]);
233
+ if (!value
234
+ || value.kind !== 'hypequery-deployment-submission'
235
+ || value.version !== 1
236
+ || (value.status !== 'accepted' && value.status !== 'already-exists')
237
+ || typeof value.releaseIdentity !== 'string'
238
+ || typeof value.bundleIdentity !== 'string'
239
+ || !SHA256_PATTERN.test(value.releaseIdentity)
240
+ || !SHA256_PATTERN.test(value.bundleIdentity)
241
+ || value.releaseIdentity !== releaseIdentity
242
+ || value.bundleIdentity !== bundleIdentity) {
243
+ throw new DeploymentUploadError('HQ_UPLOAD_INVALID_RESPONSE', 'Deployment service returned an invalid or mismatched submission response.', status);
244
+ }
245
+ return Object.freeze({
246
+ kind: value.kind,
247
+ version: value.version,
248
+ status: value.status,
249
+ releaseIdentity: value.releaseIdentity,
250
+ bundleIdentity: value.bundleIdentity,
251
+ });
252
+ }
253
+ function rejectedMessage(response, body) {
254
+ let detail = '';
255
+ try {
256
+ const parsed = JSON.parse(body);
257
+ const error = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
258
+ ? parsed.error
259
+ : undefined;
260
+ if (typeof error === 'object' && error !== null && !Array.isArray(error)) {
261
+ const code = error.code;
262
+ const message = error.message;
263
+ if (typeof code === 'string' && /^[A-Z0-9_:-]{1,64}$/.test(code))
264
+ detail += ` ${code}`;
265
+ if (typeof message === 'string') {
266
+ const safe = [...message].map(character => {
267
+ const codePoint = character.codePointAt(0) ?? 0;
268
+ return codePoint <= 0x1f || codePoint === 0x7f ? ' ' : character;
269
+ }).join('').slice(0, 512).trim();
270
+ if (safe)
271
+ detail += `: ${safe}`;
272
+ }
273
+ }
274
+ }
275
+ catch {
276
+ // Rejection bodies are optional; status remains authoritative.
277
+ }
278
+ return `Deployment service rejected the submission (${response.status} ${response.statusText})${detail}.`;
279
+ }
280
+ function nestedUploadError(input) {
281
+ const seen = new Set();
282
+ let current = input;
283
+ while (typeof current === 'object' && current !== null && !seen.has(current)) {
284
+ if (current instanceof DeploymentUploadError)
285
+ return current;
286
+ seen.add(current);
287
+ if (!('cause' in current))
288
+ return undefined;
289
+ current = current.cause;
290
+ }
291
+ return undefined;
292
+ }
293
+ const defaultFetch = async (input, init) => fetch(input, init);
294
+ export function createHttpDeploymentUploadTransport(options) {
295
+ const endpoint = endpointUrl(options.endpoint);
296
+ const token = bearerToken(options.token);
297
+ const timeoutMs = timeout(options.timeoutMs);
298
+ const fetchImplementation = options.fetch ?? defaultFetch;
299
+ const transport = {
300
+ async submit(bundle, release) {
301
+ validateMultipartBundlePaths(bundle);
302
+ const manifest = prepareProtocolDeploymentBundleManifest(bundle.manifest);
303
+ const preparedRelease = prepareProtocolDeploymentReleaseEnvelope(release.release);
304
+ if (manifest.identity !== bundle.identity
305
+ || preparedRelease.identity !== release.identity
306
+ || preparedRelease.release.bundleIdentity !== manifest.identity) {
307
+ throw new DeploymentUploadError('HQ_UPLOAD_IDENTITY_MISMATCH', 'Release and bundle identities do not match their verified content.');
308
+ }
309
+ const boundary = `hypequery-${randomUUID()}`;
310
+ const parts = uploadParts(bundle, preparedRelease, manifest);
311
+ let response;
312
+ try {
313
+ response = await fetchImplementation(endpoint, {
314
+ method: 'POST',
315
+ headers: {
316
+ Accept: 'application/json',
317
+ Authorization: `Bearer ${token}`,
318
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
319
+ 'Content-Length': String(multipartLength(boundary, parts)),
320
+ 'Idempotency-Key': preparedRelease.identity,
321
+ 'X-HypeQuery-Bundle-Identity': manifest.identity,
322
+ 'X-HypeQuery-Release-Identity': preparedRelease.identity,
323
+ },
324
+ body: multipartBody(boundary, parts),
325
+ duplex: 'half',
326
+ redirect: 'error',
327
+ signal: AbortSignal.timeout(timeoutMs),
328
+ });
329
+ }
330
+ catch (error) {
331
+ const uploadError = nestedUploadError(error);
332
+ if (uploadError)
333
+ throw uploadError;
334
+ throw new DeploymentUploadError('HQ_UPLOAD_NETWORK', `Deployment service request failed: ${error instanceof Error ? error.message : String(error)}`);
335
+ }
336
+ let body;
337
+ try {
338
+ body = await readBoundedResponse(response);
339
+ }
340
+ catch (error) {
341
+ if (error instanceof DeploymentUploadError)
342
+ throw error;
343
+ throw new DeploymentUploadError('HQ_UPLOAD_NETWORK', `Deployment service response failed: ${error instanceof Error ? error.message : String(error)}`, response.status);
344
+ }
345
+ if (!response.ok) {
346
+ throw new DeploymentUploadError('HQ_UPLOAD_REJECTED', rejectedMessage(response, body), response.status);
347
+ }
348
+ let input;
349
+ try {
350
+ input = JSON.parse(body);
351
+ }
352
+ catch {
353
+ throw new DeploymentUploadError('HQ_UPLOAD_INVALID_RESPONSE', 'Deployment service returned invalid JSON.', response.status);
354
+ }
355
+ return validateSubmissionResponse(input, manifest.identity, preparedRelease.identity, response.status);
356
+ },
357
+ };
358
+ return Object.freeze(transport);
359
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "Command-line interface for hypequery",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,7 +19,8 @@
19
19
  "open": "^10.0.0",
20
20
  "ora": "^8.0.1",
21
21
  "prompts": "^2.4.2",
22
- "@hypequery/protocol": "0.5.0"
22
+ "@hypequery/deployment": "0.2.0",
23
+ "@hypequery/protocol": "0.6.0"
23
24
  },
24
25
  "peerDependencies": {
25
26
  "@hypequery/clickhouse": "*",
@@ -37,7 +38,7 @@
37
38
  "typescript": "^5.7.3",
38
39
  "vitest": "^3.2.6",
39
40
  "@hypequery/clickhouse": "2.4.0",
40
- "@hypequery/serve": "0.13.2"
41
+ "@hypequery/serve": "0.13.3"
41
42
  },
42
43
  "repository": {
43
44
  "type": "git",