@hypequery/cli 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -9
- package/dist/commands/deploy.d.ts +3 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +5 -42
- package/dist/commands/deployment.d.ts +0 -7
- package/dist/commands/deployment.d.ts.map +1 -1
- package/dist/commands/deployment.js +23 -153
- package/dist/utils/deployment-bundle.d.ts +2 -8
- package/dist/utils/deployment-bundle.d.ts.map +1 -1
- package/dist/utils/deployment-bundle.js +4 -99
- package/package.json +5 -5
- package/dist/utils/deployment-runtime-artifact.d.ts +0 -9
- package/dist/utils/deployment-runtime-artifact.d.ts.map +0 -1
- package/dist/utils/deployment-runtime-artifact.js +0 -162
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;AAiCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiCpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AA6QD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -170,13 +170,6 @@ program
|
|
|
170
170
|
.command('deployment:build <api>')
|
|
171
171
|
.description('Build a verified deployment bundle')
|
|
172
172
|
.option('--bundle-output <directory>', 'Bundle directory (default: analytics/hypequery-deployment)')
|
|
173
|
-
.option('-o, --output <path>', 'Output JSON file (default: analytics/hypequery-deployment.json)')
|
|
174
|
-
.option('--runtime <runtime>', 'Runtime for non-portable handlers: node or python (default: node)')
|
|
175
|
-
.option('--runtime-artifact <sha256>', 'Use a prebuilt runtime artifact with this SHA-256 identity')
|
|
176
|
-
.option('--runtime-file <path>', 'Include the bytes for a prebuilt runtime artifact')
|
|
177
|
-
.option('--runtime-output <path>', 'Bundled Node runtime path (default: beside deployment JSON)')
|
|
178
|
-
.option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
|
|
179
|
-
.option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
|
|
180
173
|
.option('--no-source', 'Exclude project source files from the deployment bundle')
|
|
181
174
|
.option('--allow-unsupported-config', 'Deploy even though some Serve config will not be honoured in Cloud')
|
|
182
175
|
.action(runCommand(async (api, options) => {
|
|
@@ -184,7 +177,7 @@ program
|
|
|
184
177
|
}));
|
|
185
178
|
program
|
|
186
179
|
.command('deployment:validate <artifact>')
|
|
187
|
-
.description('Verify a deployment bundle or validate a
|
|
180
|
+
.description('Verify a deployment bundle or validate a deployment contract JSON file')
|
|
188
181
|
.action(runCommand(async (artifact) => {
|
|
189
182
|
await validateDeploymentCommand(artifact);
|
|
190
183
|
}));
|
|
@@ -213,7 +206,6 @@ program
|
|
|
213
206
|
.option('--release-output <path>', 'Release JSON path (default: beside the bundle)')
|
|
214
207
|
.option('--project <project>', 'Target project identifier (advanced override)')
|
|
215
208
|
.option('--environment <environment>', 'Target environment identifier (advanced override)')
|
|
216
|
-
.option('--release <path>', 'Submit a prebuilt bundle with this release (legacy)')
|
|
217
209
|
.option('--no-source', 'Exclude project source files from the deployment bundle')
|
|
218
210
|
.option('--endpoint <url>', 'HTTPS submission endpoint; requires HYPEQUERY_API_TOKEN')
|
|
219
211
|
.option('--replace-restored', 'Intentionally replace a restored live release')
|
|
@@ -7,7 +7,9 @@ export interface SubmitDeploymentOptions {
|
|
|
7
7
|
endpoint?: string;
|
|
8
8
|
replaceRestored?: boolean;
|
|
9
9
|
}
|
|
10
|
-
export interface DeployOptions
|
|
10
|
+
export interface DeployOptions {
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
replaceRestored?: boolean;
|
|
11
13
|
project?: string;
|
|
12
14
|
environment?: string;
|
|
13
15
|
bundleOutput?: string;
|
|
@@ -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,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,
|
|
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,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,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;IACjB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;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;AAsBD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,aAAkB,EAC3B,YAAY,GAAE,kBAAuB,GACpC,OAAO,CAAC,4BAA4B,CAAC,CAwCvC"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -140,43 +140,11 @@ async function rejectBundleDirectorySource(sourcePath) {
|
|
|
140
140
|
+ 'To upload this bundle, use `hypequery deployment:submit '
|
|
141
141
|
+ `${sourcePath} --release <file>\`.`);
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
144
|
-
* `deploy` exposes no runtime flags, so a runtime error from the shared build
|
|
145
|
-
* command would otherwise point at flags this command does not accept.
|
|
146
|
-
*/
|
|
147
|
-
function withRuntimeFlagHint(error) {
|
|
148
|
-
if (!(error instanceof Error) || !error.message.includes('--runtime'))
|
|
149
|
-
return error;
|
|
150
|
-
return new Error(`${error.message}\n\n`
|
|
151
|
-
+ 'Runtime overrides are not available on `hypequery deploy`. Build with '
|
|
152
|
-
+ '`hypequery deployment:build` (which accepts --runtime, --runtime-artifact, '
|
|
153
|
-
+ 'and --runtime-file), then upload with `hypequery deployment:submit`.');
|
|
154
|
-
}
|
|
155
|
-
function rejectLegacyOrchestrationOptions(options) {
|
|
156
|
-
if (options.project !== undefined
|
|
157
|
-
|| options.environment !== undefined
|
|
158
|
-
|| options.bundleOutput !== undefined
|
|
159
|
-
|| options.releaseOutput !== undefined) {
|
|
160
|
-
throw new Error('--release selects prebuilt submission mode and cannot be combined with '
|
|
161
|
-
+ '--project, --environment, --bundle-output, or --release-output. '
|
|
162
|
-
+ 'Use `hypequery deployment:submit` for explicit prebuilt uploads.');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
143
|
export async function deployCommand(sourcePath, options = {}, dependencies = {}) {
|
|
166
144
|
if (!sourcePath) {
|
|
167
145
|
throw new Error('Missing API module path.\n\n'
|
|
168
146
|
+ 'Usage: hypequery deploy analytics/api.ts');
|
|
169
147
|
}
|
|
170
|
-
if (options.release !== undefined) {
|
|
171
|
-
rejectLegacyOrchestrationOptions(options);
|
|
172
|
-
logger.warn('`hypequery deploy <bundle> --release <file>` is deprecated. '
|
|
173
|
-
+ 'Use `hypequery deployment:submit <bundle> --release <file>` instead.');
|
|
174
|
-
return submitDeploymentCommand(sourcePath, {
|
|
175
|
-
release: options.release,
|
|
176
|
-
endpoint: options.endpoint,
|
|
177
|
-
replaceRestored: options.replaceRestored,
|
|
178
|
-
}, dependencies);
|
|
179
|
-
}
|
|
180
148
|
await rejectBundleDirectorySource(sourcePath);
|
|
181
149
|
const bundlePath = options.bundleOutput ?? DEFAULT_BUNDLE_PATH;
|
|
182
150
|
const releasePath = options.releaseOutput
|
|
@@ -188,16 +156,11 @@ export async function deployCommand(sourcePath, options = {}, dependencies = {})
|
|
|
188
156
|
// Resolve credentials up front. Submission is the last step, so without this
|
|
189
157
|
// a missing or expired login only surfaces after a full bundle build.
|
|
190
158
|
await resolveDeploymentCredential(options.endpoint, dependencies);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
throw withRuntimeFlagHint(error);
|
|
200
|
-
}
|
|
159
|
+
await build(sourcePath, {
|
|
160
|
+
bundleOutput: bundlePath,
|
|
161
|
+
source: options.source,
|
|
162
|
+
allowUnsupportedConfig: options.allowUnsupportedConfig,
|
|
163
|
+
});
|
|
201
164
|
await prepareRelease(bundlePath, {
|
|
202
165
|
project: options.project,
|
|
203
166
|
environment: options.environment,
|
|
@@ -2,13 +2,6 @@ import { type ProtocolDeploymentContract, type ProtocolDeploymentReleaseEnvelope
|
|
|
2
2
|
import { type StoredCloudCredential } from '../utils/cloud-credential-store.js';
|
|
3
3
|
export interface BuildDeploymentOptions {
|
|
4
4
|
bundleOutput?: string;
|
|
5
|
-
output?: string;
|
|
6
|
-
runtime?: 'node' | 'python';
|
|
7
|
-
runtimeArtifact?: string;
|
|
8
|
-
runtimeFile?: string;
|
|
9
|
-
runtimeOutput?: string;
|
|
10
|
-
entrypointPrefix?: string;
|
|
11
|
-
hashOutput?: string;
|
|
12
5
|
source?: boolean;
|
|
13
6
|
allowUnsupportedConfig?: boolean;
|
|
14
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACvC,MAAM,qBAAqB,CAAC;AAS7B,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oCAAoC;IACnD,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IAC7D;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAYD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAyCrC;AAUD,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAqErC;AAiED,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,GAAE,+BAAoC,EAC7C,YAAY,GAAE,oCAAyC,GACtD,OAAO,CAAC,iCAAiC,CAAC,CAkE5C"}
|
|
@@ -1,177 +1,49 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { lstat, mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { prepareProtocolDeploymentReleaseEnvelope, prepareProtocolDeploymentContract, } from '@hypequery/protocol';
|
|
5
|
-
import { writeDeploymentBundle, verifyDeploymentBundle,
|
|
6
|
-
import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../utils/deployment-runtime-artifact.js';
|
|
4
|
+
import { writeDeploymentBundle, verifyDeploymentBundle, } from '../utils/deployment-bundle.js';
|
|
7
5
|
import { captureDeploymentSourceSnapshot } from '../utils/deployment-source-snapshot.js';
|
|
8
6
|
import { loadApiModule } from '../utils/load-api.js';
|
|
9
7
|
import { logger } from '../utils/logger.js';
|
|
10
8
|
import { reportCloudDiagnostic } from '../utils/cloud-diagnostic.js';
|
|
11
9
|
import { loadCloudCredential, } from '../utils/cloud-credential-store.js';
|
|
12
|
-
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
13
10
|
const DEFAULT_BUNDLE_OUTPUT = 'analytics/hypequery-deployment';
|
|
14
|
-
function runtimeName(options) {
|
|
15
|
-
const runtime = options.runtime ?? 'node';
|
|
16
|
-
if (runtime !== 'node' && runtime !== 'python') {
|
|
17
|
-
throw new Error('--runtime must be either node or python.');
|
|
18
|
-
}
|
|
19
|
-
return runtime;
|
|
20
|
-
}
|
|
21
|
-
function configuredRuntimeArtifact(options, runtime) {
|
|
22
|
-
if (options.runtimeArtifact === undefined)
|
|
23
|
-
return undefined;
|
|
24
|
-
if (!SHA256_PATTERN.test(options.runtimeArtifact)) {
|
|
25
|
-
throw new Error('--runtime-artifact must be a lowercase 64-character SHA-256 digest.');
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
runtime,
|
|
29
|
-
artifactSha256: options.runtimeArtifact,
|
|
30
|
-
...(options.entrypointPrefix !== undefined
|
|
31
|
-
? { entrypointPrefix: options.entrypointPrefix }
|
|
32
|
-
: {}),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function assertDistinctOutputPaths(paths) {
|
|
36
|
-
const seen = new Map();
|
|
37
|
-
for (const [label, value] of Object.entries(paths)) {
|
|
38
|
-
if (value === undefined)
|
|
39
|
-
continue;
|
|
40
|
-
const resolved = path.resolve(value);
|
|
41
|
-
const existing = seen.get(resolved);
|
|
42
|
-
if (existing) {
|
|
43
|
-
throw new Error(`${label} must use a different path from ${existing}.`);
|
|
44
|
-
}
|
|
45
|
-
seen.set(resolved, label);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
11
|
export async function buildDeploymentCommand(apiPath, options = {}) {
|
|
49
12
|
if (!apiPath) {
|
|
50
13
|
throw new Error('Missing API module path.\n\n'
|
|
51
14
|
+ 'Usage: hypequery deployment:build analytics/api.ts');
|
|
52
15
|
}
|
|
53
|
-
const
|
|
54
|
-
const configuredArtifact = configuredRuntimeArtifact(options, runtime);
|
|
55
|
-
const legacyOutputRequested = options.output !== undefined
|
|
56
|
-
|| options.hashOutput !== undefined
|
|
57
|
-
|| options.runtimeOutput !== undefined;
|
|
58
|
-
if (options.bundleOutput !== undefined && legacyOutputRequested) {
|
|
59
|
-
throw new Error('--bundle-output cannot be combined with --output, --hash-output, or --runtime-output.');
|
|
60
|
-
}
|
|
61
|
-
const bundleOutput = options.bundleOutput
|
|
62
|
-
?? (legacyOutputRequested ? undefined : DEFAULT_BUNDLE_OUTPUT);
|
|
63
|
-
if (options.runtimeFile !== undefined && configuredArtifact === undefined) {
|
|
64
|
-
throw new Error('--runtime-file requires --runtime-artifact.');
|
|
65
|
-
}
|
|
66
|
-
if (options.runtimeFile !== undefined && bundleOutput === undefined) {
|
|
67
|
-
throw new Error('--runtime-file is only supported when building a deployment bundle.');
|
|
68
|
-
}
|
|
69
|
-
if (configuredArtifact && bundleOutput !== undefined && options.runtimeFile === undefined) {
|
|
70
|
-
throw new Error('A complete deployment bundle requires --runtime-file with a prebuilt --runtime-artifact.');
|
|
71
|
-
}
|
|
72
|
-
if (configuredArtifact && options.runtimeOutput !== undefined) {
|
|
73
|
-
throw new Error('--runtime-output cannot be used with a prebuilt --runtime-artifact.');
|
|
74
|
-
}
|
|
75
|
-
const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
|
|
76
|
-
const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
|
|
77
|
-
if (options.runtimeOutput !== undefined) {
|
|
78
|
-
assertDistinctOutputPaths({
|
|
79
|
-
'--output': outputPath,
|
|
80
|
-
'--hash-output': hashOutputPath,
|
|
81
|
-
'--runtime-output': options.runtimeOutput,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
16
|
+
const bundleOutput = options.bundleOutput ?? DEFAULT_BUNDLE_OUTPUT;
|
|
84
17
|
const api = await loadApiModule(apiPath);
|
|
85
18
|
if (typeof api.deploymentContract !== 'function') {
|
|
86
19
|
throw new Error(`Invalid API module: ${apiPath}\n\n`
|
|
87
20
|
+ 'The exported API must provide deploymentContract(). '
|
|
88
21
|
+ 'Upgrade @hypequery/serve and export the value returned by createAPI() or serve().');
|
|
89
22
|
}
|
|
90
|
-
let builtArtifact;
|
|
91
|
-
let artifact = configuredArtifact;
|
|
92
|
-
if (!artifact) {
|
|
93
|
-
const runtimeEntrypoints = getDeploymentRuntimeEntrypoints(api);
|
|
94
|
-
if (runtimeEntrypoints.length > 0) {
|
|
95
|
-
if (runtime !== 'node') {
|
|
96
|
-
throw new Error('Automatic runtime artifact builds currently support Node only. '
|
|
97
|
-
+ 'Provide --runtime-artifact for Python deployments.');
|
|
98
|
-
}
|
|
99
|
-
builtArtifact = await buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, options.entrypointPrefix);
|
|
100
|
-
artifact = {
|
|
101
|
-
runtime: 'node',
|
|
102
|
-
artifactSha256: builtArtifact.artifactSha256,
|
|
103
|
-
entrypointPrefix: builtArtifact.entrypointPrefix,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
23
|
const contract = api.deploymentContract({
|
|
108
|
-
...(artifact ? { runtimeArtifact: artifact } : {}),
|
|
109
24
|
...(options.allowUnsupportedConfig ? { allowUnsupportedConfig: true } : {}),
|
|
110
25
|
onCloudDiagnostic: diagnostic => reportCloudDiagnostic(diagnostic, options.allowUnsupportedConfig === true),
|
|
111
26
|
});
|
|
112
27
|
const prepared = prepareProtocolDeploymentContract(contract);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const sourceSnapshot = options.source === false
|
|
133
|
-
? undefined
|
|
134
|
-
: await captureDeploymentSourceSnapshot(apiPath);
|
|
135
|
-
const bundle = await writeDeploymentBundle(bundleOutput, prepared, runtimeFiles, sourceSnapshot);
|
|
136
|
-
logger.success(`Deployment bundle written to ${bundle.directory}`);
|
|
137
|
-
if (bundle.manifest.source) {
|
|
138
|
-
logger.info(`Captured ${bundle.manifest.source.files.length} source `
|
|
139
|
-
+ `${bundle.manifest.source.files.length === 1 ? 'file' : 'files'}`);
|
|
140
|
-
}
|
|
141
|
-
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
142
|
-
logger.info(`Deployment identity: ${digest}`);
|
|
143
|
-
return validated;
|
|
144
|
-
}
|
|
145
|
-
const runtimeOutputPath = builtArtifact
|
|
146
|
-
? options.runtimeOutput ?? path.join(path.dirname(outputPath), 'hypequery-runtime.mjs')
|
|
147
|
-
: undefined;
|
|
148
|
-
assertDistinctOutputPaths({
|
|
149
|
-
'--output': outputPath,
|
|
150
|
-
'--hash-output': hashOutputPath,
|
|
151
|
-
'--runtime-output': runtimeOutputPath,
|
|
152
|
-
});
|
|
153
|
-
const identitySidecar = [
|
|
154
|
-
'# Hypequery deployment identity v1; not a file checksum or sha256sum input.',
|
|
155
|
-
'# SHA-256(UTF-8("hypequery:deployment:v1") || 0x00 || RFC 8785 canonical bytes); '
|
|
156
|
-
+ 'the output newline is excluded.',
|
|
157
|
-
`${digest} ${path.basename(outputPath)}`,
|
|
158
|
-
'',
|
|
159
|
-
].join('\n');
|
|
160
|
-
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
161
|
-
await mkdir(path.dirname(hashOutputPath), { recursive: true });
|
|
162
|
-
if (runtimeOutputPath && builtArtifact) {
|
|
163
|
-
await mkdir(path.dirname(runtimeOutputPath), { recursive: true });
|
|
164
|
-
await writeFile(runtimeOutputPath, builtArtifact.bytes);
|
|
165
|
-
}
|
|
166
|
-
await writeFile(outputPath, `${canonical}\n`, 'utf8');
|
|
167
|
-
await writeFile(hashOutputPath, identitySidecar, 'utf8');
|
|
168
|
-
if (runtimeOutputPath && builtArtifact) {
|
|
169
|
-
logger.success(`Runtime artifact written to ${runtimeOutputPath}`);
|
|
170
|
-
logger.info(`Runtime artifact SHA-256: ${builtArtifact.artifactSha256}`);
|
|
171
|
-
}
|
|
172
|
-
logger.success(`Deployment contract written to ${outputPath}`);
|
|
173
|
-
logger.info(`Identity: ${digest}`);
|
|
174
|
-
return validated;
|
|
28
|
+
const sourceSnapshot = options.source === false
|
|
29
|
+
? undefined
|
|
30
|
+
: await captureDeploymentSourceSnapshot(apiPath);
|
|
31
|
+
const bundle = await writeDeploymentBundle(bundleOutput, prepared, sourceSnapshot);
|
|
32
|
+
logger.success(`Deployment bundle written to ${bundle.directory}`);
|
|
33
|
+
if (bundle.manifest.source) {
|
|
34
|
+
logger.info(`Captured ${bundle.manifest.source.files.length} source `
|
|
35
|
+
+ `${bundle.manifest.source.files.length === 1 ? 'file' : 'files'}`);
|
|
36
|
+
}
|
|
37
|
+
logger.info(`${prepared.contract.datasets.length} `
|
|
38
|
+
+ `${prepared.contract.datasets.length === 1 ? 'dataset' : 'datasets'}, 0 runtime artifacts`);
|
|
39
|
+
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
40
|
+
logger.info(`Deployment identity: ${prepared.identity}`);
|
|
41
|
+
return prepared.contract;
|
|
42
|
+
}
|
|
43
|
+
function describeContract(contract) {
|
|
44
|
+
const datasets = `${contract.datasets.length} `
|
|
45
|
+
+ `${contract.datasets.length === 1 ? 'dataset' : 'datasets'}`;
|
|
46
|
+
return datasets;
|
|
175
47
|
}
|
|
176
48
|
export async function validateDeploymentCommand(artifactPath) {
|
|
177
49
|
if (!artifactPath) {
|
|
@@ -194,8 +66,7 @@ export async function validateDeploymentCommand(artifactPath) {
|
|
|
194
66
|
const bundle = await verifyDeploymentBundle(artifactPath);
|
|
195
67
|
const contract = bundle.contract;
|
|
196
68
|
logger.success(`Valid deployment bundle: ${artifactPath}`);
|
|
197
|
-
logger.info(
|
|
198
|
-
+ `${contract.artifacts.length} runtime artifacts`);
|
|
69
|
+
logger.info(describeContract(contract));
|
|
199
70
|
logger.info(`Bundle identity: ${bundle.identity}`);
|
|
200
71
|
logger.info(`Deployment identity: ${bundle.manifest.deployment.identity}`);
|
|
201
72
|
return contract;
|
|
@@ -226,8 +97,7 @@ export async function validateDeploymentCommand(artifactPath) {
|
|
|
226
97
|
}
|
|
227
98
|
const { contract, identity: digest } = prepared;
|
|
228
99
|
logger.success(`Valid deployment contract: ${artifactPath}`);
|
|
229
|
-
logger.info(
|
|
230
|
-
+ `${contract.artifacts.length} runtime artifacts`);
|
|
100
|
+
logger.info(describeContract(contract));
|
|
231
101
|
logger.info(`Identity: ${digest}`);
|
|
232
102
|
return contract;
|
|
233
103
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { type PreparedProtocolDeploymentContract, type
|
|
1
|
+
import { type PreparedProtocolDeploymentContract, type ProtocolDeploymentContract, type ProtocolDeploymentBundleManifest } from '@hypequery/protocol';
|
|
2
2
|
import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle } from '@hypequery/deployment';
|
|
3
3
|
import type { VerifiedDeploymentBundle } from '@hypequery/deployment';
|
|
4
4
|
export { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, };
|
|
5
5
|
export type { VerifiedDeploymentBundle };
|
|
6
|
-
export interface DeploymentBundleRuntimeFile {
|
|
7
|
-
readonly runtime: 'node' | 'python';
|
|
8
|
-
readonly sha256: string;
|
|
9
|
-
readonly bytes: Uint8Array;
|
|
10
|
-
}
|
|
11
6
|
export interface DeploymentBundleSourceFile {
|
|
12
7
|
readonly path: string;
|
|
13
8
|
readonly bytes: Uint8Array;
|
|
@@ -28,6 +23,5 @@ export interface WrittenDeploymentBundle {
|
|
|
28
23
|
readonly identity: string;
|
|
29
24
|
readonly contract: ProtocolDeploymentContract;
|
|
30
25
|
}
|
|
31
|
-
export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract,
|
|
32
|
-
export declare function readDeploymentRuntimeFile(filePath: string): Promise<Uint8Array>;
|
|
26
|
+
export declare function writeDeploymentBundle(outputDirectory: string, prepared: PreparedProtocolDeploymentContract, sourceSnapshot?: DeploymentBundleSourceSnapshot): Promise<WrittenDeploymentBundle>;
|
|
33
27
|
//# 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":"
|
|
1
|
+
{"version":3,"file":"deployment-bundle.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-bundle.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,kCAAkC,EACvC,KAAK,0BAA0B,EAC/B,KAAK,gCAAgC,EACtC,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,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;AA4ED,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,kCAAkC,EAC5C,cAAc,CAAC,EAAE,8BAA8B,GAC9C,OAAO,CAAC,uBAAuB,CAAC,CAkFlC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { lstat, mkdir, mkdtemp, rename, rm, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS, prepareProtocolDeploymentBundleManifest, } from '@hypequery/protocol';
|
|
5
5
|
import { DEPLOYMENT_BUNDLE_CONTRACT, DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from '@hypequery/deployment';
|
|
@@ -9,9 +9,6 @@ const utf8Encoder = new TextEncoder();
|
|
|
9
9
|
function sha256(bytes) {
|
|
10
10
|
return createHash('sha256').update(bytes).digest('hex');
|
|
11
11
|
}
|
|
12
|
-
function runtimeArtifactPath(runtime, digest) {
|
|
13
|
-
return `artifacts/${digest}.${runtime === 'node' ? 'mjs' : 'pyz'}`;
|
|
14
|
-
}
|
|
15
12
|
const SOURCE_ROOT = 'source';
|
|
16
13
|
function errorCode(error) {
|
|
17
14
|
return typeof error === 'object' && error !== null && 'code' in error
|
|
@@ -37,53 +34,6 @@ async function existingVerifiedBundle(outputDirectory) {
|
|
|
37
34
|
}
|
|
38
35
|
return true;
|
|
39
36
|
}
|
|
40
|
-
function validateRuntimeFiles(prepared, files) {
|
|
41
|
-
requireClosedContractArtifactSet(prepared.contract);
|
|
42
|
-
const declared = new Map(prepared.contract.artifacts.map(artifact => [artifact.artifactSha256, artifact.runtime]));
|
|
43
|
-
const supplied = new Map();
|
|
44
|
-
for (const file of files) {
|
|
45
|
-
if (sha256(file.bytes) !== file.sha256) {
|
|
46
|
-
throw new Error(`Runtime artifact bytes do not match SHA-256 ${file.sha256}.`);
|
|
47
|
-
}
|
|
48
|
-
if (declared.get(file.sha256) !== file.runtime) {
|
|
49
|
-
throw new Error(`Runtime artifact ${file.sha256} is not declared by the deployment contract as ${file.runtime}.`);
|
|
50
|
-
}
|
|
51
|
-
if (supplied.has(file.sha256)) {
|
|
52
|
-
throw new Error(`Duplicate runtime artifact bytes supplied for ${file.sha256}.`);
|
|
53
|
-
}
|
|
54
|
-
if (file.bytes.byteLength < 1
|
|
55
|
-
|| file.bytes.byteLength > DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes) {
|
|
56
|
-
throw new Error(`Runtime artifact ${file.sha256} exceeds the bundle byte limits.`);
|
|
57
|
-
}
|
|
58
|
-
supplied.set(file.sha256, file.runtime);
|
|
59
|
-
}
|
|
60
|
-
for (const [digest, runtime] of declared) {
|
|
61
|
-
if (supplied.get(digest) !== runtime) {
|
|
62
|
-
throw new Error(`Deployment bundle is missing ${runtime} runtime artifact ${digest}.`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return Object.freeze([...files].sort((left, right) => {
|
|
66
|
-
const leftPath = runtimeArtifactPath(left.runtime, left.sha256);
|
|
67
|
-
const rightPath = runtimeArtifactPath(right.runtime, right.sha256);
|
|
68
|
-
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
function requireClosedContractArtifactSet(contract) {
|
|
72
|
-
const referenced = new Map();
|
|
73
|
-
for (const query of contract.queries) {
|
|
74
|
-
if (query.implementation.kind === 'runtime-reference') {
|
|
75
|
-
referenced.set(query.implementation.artifactSha256, query.implementation.runtime);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (referenced.size !== contract.artifacts.length) {
|
|
79
|
-
throw new Error('Deployment contract contains missing or unreferenced runtime artifacts.');
|
|
80
|
-
}
|
|
81
|
-
for (const artifact of contract.artifacts) {
|
|
82
|
-
if (referenced.get(artifact.artifactSha256) !== artifact.runtime) {
|
|
83
|
-
throw new Error(`Deployment runtime artifact ${artifact.artifactSha256} is not referenced by a named query.`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
37
|
function validateSourceSnapshot(source) {
|
|
88
38
|
if (!source)
|
|
89
39
|
return undefined;
|
|
@@ -122,12 +72,11 @@ function validateSourceSnapshot(source) {
|
|
|
122
72
|
...(source.revision ? { revision: Object.freeze({ ...source.revision }) } : {}),
|
|
123
73
|
});
|
|
124
74
|
}
|
|
125
|
-
export async function writeDeploymentBundle(outputDirectory, prepared,
|
|
75
|
+
export async function writeDeploymentBundle(outputDirectory, prepared, sourceSnapshot) {
|
|
126
76
|
const destination = path.resolve(outputDirectory);
|
|
127
77
|
if (destination === path.parse(destination).root) {
|
|
128
78
|
throw new Error('The deployment bundle output cannot be a filesystem root.');
|
|
129
79
|
}
|
|
130
|
-
const files = validateRuntimeFiles(prepared, runtimeFiles);
|
|
131
80
|
const source = validateSourceSnapshot(sourceSnapshot);
|
|
132
81
|
const deploymentBytes = utf8Encoder.encode(`${prepared.canonical}\n`);
|
|
133
82
|
const manifestInput = {
|
|
@@ -139,12 +88,8 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
|
|
|
139
88
|
sha256: sha256(deploymentBytes),
|
|
140
89
|
byteLength: deploymentBytes.byteLength,
|
|
141
90
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
path: runtimeArtifactPath(file.runtime, file.sha256),
|
|
145
|
-
sha256: file.sha256,
|
|
146
|
-
byteLength: file.bytes.byteLength,
|
|
147
|
-
})),
|
|
91
|
+
// A deployment carries datasets: there is nothing else to put here.
|
|
92
|
+
artifacts: [],
|
|
148
93
|
...(source ? {
|
|
149
94
|
source: {
|
|
150
95
|
root: SOURCE_ROOT,
|
|
@@ -164,12 +109,7 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
|
|
|
164
109
|
await mkdir(parent, { recursive: true });
|
|
165
110
|
const staging = await mkdtemp(path.join(parent, `.${path.basename(destination)}.tmp-`));
|
|
166
111
|
try {
|
|
167
|
-
if (files.length > 0)
|
|
168
|
-
await mkdir(path.join(staging, 'artifacts'));
|
|
169
112
|
await writeFile(path.join(staging, DEPLOYMENT_BUNDLE_CONTRACT), deploymentBytes, { flag: 'wx' });
|
|
170
|
-
for (const file of files) {
|
|
171
|
-
await writeFile(path.join(staging, runtimeArtifactPath(file.runtime, file.sha256)), file.bytes, { flag: 'wx' });
|
|
172
|
-
}
|
|
173
113
|
for (const file of source?.files ?? []) {
|
|
174
114
|
const outputPath = path.join(staging, SOURCE_ROOT, ...file.path.split('/'));
|
|
175
115
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -209,38 +149,3 @@ export async function writeDeploymentBundle(outputDirectory, prepared, runtimeFi
|
|
|
209
149
|
contract: prepared.contract,
|
|
210
150
|
});
|
|
211
151
|
}
|
|
212
|
-
async function readBoundedAbsoluteRegularFile(absolutePath, displayPath, maximum) {
|
|
213
|
-
let handle;
|
|
214
|
-
try {
|
|
215
|
-
const initialStat = await lstat(absolutePath);
|
|
216
|
-
if (initialStat.isSymbolicLink()) {
|
|
217
|
-
throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
|
|
218
|
-
}
|
|
219
|
-
if (!initialStat.isFile()) {
|
|
220
|
-
throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
|
|
221
|
-
}
|
|
222
|
-
if (initialStat.size < 1 || initialStat.size > maximum) {
|
|
223
|
-
throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
|
|
224
|
-
}
|
|
225
|
-
handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
226
|
-
const stat = await handle.stat();
|
|
227
|
-
if (!stat.isFile())
|
|
228
|
-
throw new Error(`Bundle entry is not a regular file: ${displayPath}`);
|
|
229
|
-
if (stat.size < 1 || stat.size > maximum) {
|
|
230
|
-
throw new Error(`Bundle entry exceeds its byte limit: ${displayPath}`);
|
|
231
|
-
}
|
|
232
|
-
return await handle.readFile();
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
if (errorCode(error) === 'ELOOP') {
|
|
236
|
-
throw new Error(`Bundle entry must not be a symbolic link: ${displayPath}`);
|
|
237
|
-
}
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
finally {
|
|
241
|
-
await handle?.close();
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
export async function readDeploymentRuntimeFile(filePath) {
|
|
245
|
-
return readBoundedAbsoluteRegularFile(path.resolve(filePath), filePath, DEFAULT_PROTOCOL_DEPLOYMENT_BUNDLE_LIMITS.maxArtifactBytes);
|
|
246
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "CLI for scaffolding type-safe ClickHouse analytics, semantic datasets, React manifests, and deployments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"open": "^10.0.0",
|
|
31
31
|
"ora": "^8.0.1",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
|
-
"@hypequery/deployment": "0.
|
|
34
|
-
"@hypequery/protocol": "0.
|
|
33
|
+
"@hypequery/deployment": "0.9.0",
|
|
34
|
+
"@hypequery/protocol": "0.14.0"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
37
|
"@napi-rs/keyring": "1.3.0"
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@vitest/coverage-v8": "^3.2.6",
|
|
52
52
|
"typescript": "^5.7.3",
|
|
53
53
|
"vitest": "^3.2.6",
|
|
54
|
-
"@hypequery/mcp": "0.
|
|
55
|
-
"@hypequery/serve": "0.
|
|
54
|
+
"@hypequery/mcp": "0.8.1",
|
|
55
|
+
"@hypequery/serve": "0.18.0"
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export interface NodeRuntimeArtifact {
|
|
2
|
-
readonly bytes: Uint8Array;
|
|
3
|
-
readonly artifactSha256: string;
|
|
4
|
-
readonly entrypointPrefix: string;
|
|
5
|
-
readonly runtimeEntrypoints: readonly string[];
|
|
6
|
-
}
|
|
7
|
-
export declare function getDeploymentRuntimeEntrypoints(input: unknown): readonly string[];
|
|
8
|
-
export declare function buildNodeRuntimeArtifact(apiPath: string, runtimeEntrypoints: readonly string[], entrypointPrefix?: string): Promise<NodeRuntimeArtifact>;
|
|
9
|
-
//# sourceMappingURL=deployment-runtime-artifact.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-runtime-artifact.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-runtime-artifact.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AASD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CA6BjF;AAiED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,gBAAgB,SAAY,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAmD9B"}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, } from '@hypequery/protocol';
|
|
4
|
-
import { build } from 'esbuild';
|
|
5
|
-
import { findNearestTsconfig } from './load-api.js';
|
|
6
|
-
const deploymentBuildSourceSymbol = Symbol.for('hypequery.deployment-build-source.v1');
|
|
7
|
-
function executableQuery(query) {
|
|
8
|
-
return typeof query === 'function'
|
|
9
|
-
|| (typeof query === 'object'
|
|
10
|
-
&& query !== null
|
|
11
|
-
&& typeof query.run === 'function');
|
|
12
|
-
}
|
|
13
|
-
export function getDeploymentRuntimeEntrypoints(input) {
|
|
14
|
-
if (typeof input !== 'object' || input === null) {
|
|
15
|
-
throw new Error('Invalid API module: deployment build metadata is unavailable.');
|
|
16
|
-
}
|
|
17
|
-
const api = input;
|
|
18
|
-
const source = api[deploymentBuildSourceSymbol];
|
|
19
|
-
if (source?.version !== 1 || !Array.isArray(source.runtimeEntrypoints)) {
|
|
20
|
-
throw new Error('The exported API does not expose deployment build metadata. '
|
|
21
|
-
+ 'Upgrade @hypequery/serve before building runtime artifacts.');
|
|
22
|
-
}
|
|
23
|
-
const names = source.runtimeEntrypoints.map((name) => {
|
|
24
|
-
try {
|
|
25
|
-
return parseProtocolIdentifier(name);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
throw new Error(`Invalid runtime entrypoint name: ${String(name)}`);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
if (new Set(names).size !== names.length) {
|
|
32
|
-
throw new Error('Deployment build metadata contains duplicate runtime entrypoints.');
|
|
33
|
-
}
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
if (!executableQuery(api.queries?.[name]?.query)) {
|
|
36
|
-
throw new Error(`Serve query "${name}" does not expose an executable runtime handler.`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return Object.freeze([...names].sort());
|
|
40
|
-
}
|
|
41
|
-
function projectRelativeImport(apiPath) {
|
|
42
|
-
const resolved = path.resolve(process.cwd(), apiPath);
|
|
43
|
-
const relative = path.relative(process.cwd(), resolved);
|
|
44
|
-
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
45
|
-
throw new Error('The API module must be inside the current project for deterministic bundling.');
|
|
46
|
-
}
|
|
47
|
-
const portable = relative.split(path.sep).join('/');
|
|
48
|
-
return {
|
|
49
|
-
resolved,
|
|
50
|
-
specifier: portable.startsWith('.') ? portable : `./${portable}`,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function runtimeEntrySource(apiSpecifier, runtimeEntrypoints, entrypointPrefix) {
|
|
54
|
-
const prefix = parseProtocolQualifiedIdentifier(entrypointPrefix).split('.');
|
|
55
|
-
const names = JSON.stringify(runtimeEntrypoints);
|
|
56
|
-
let registryExpression = 'runtimeEntries';
|
|
57
|
-
for (let index = prefix.length - 1; index >= 1; index -= 1) {
|
|
58
|
-
registryExpression = `Object.freeze({ ${prefix[index]}: ${registryExpression} })`;
|
|
59
|
-
}
|
|
60
|
-
return [
|
|
61
|
-
`import * as apiModule from ${JSON.stringify(apiSpecifier)};`,
|
|
62
|
-
'const api = apiModule.api ?? Reflect.get(apiModule, "default");',
|
|
63
|
-
'if (!api || typeof api !== "object") throw new Error("Runtime API export is unavailable.");',
|
|
64
|
-
'const resolveQuery = (name) => {',
|
|
65
|
-
' if (typeof api.execute === "function") {',
|
|
66
|
-
' // Serve entrypoint names are validated against api.queries before bundling.',
|
|
67
|
-
' return (argument = {}) => api.execute(name, {',
|
|
68
|
-
' input: argument.input,',
|
|
69
|
-
' ...(argument.context === undefined ? {} : { context: argument.context }),',
|
|
70
|
-
' ...(argument.request === undefined ? {} : { request: argument.request }),',
|
|
71
|
-
' ...(argument.requestId === undefined ? {} : { requestId: argument.requestId }),',
|
|
72
|
-
// A null principal means "no trusted principal"; forwarding it would skip the
|
|
73
|
-
// API's own auth strategies instead of falling through to them.
|
|
74
|
-
' ...(argument.trustedAuth == null ? {} : { trustedAuth: argument.trustedAuth }),',
|
|
75
|
-
' });',
|
|
76
|
-
' }',
|
|
77
|
-
' const entry = api.queries?.[name];',
|
|
78
|
-
' const query = entry?.query;',
|
|
79
|
-
' let run;',
|
|
80
|
-
' if (typeof query === "function") run = query;',
|
|
81
|
-
' else if (query && typeof query === "object" && typeof query.run === "function") run = query.run.bind(query);',
|
|
82
|
-
' if (!run) throw new Error(`Runtime entrypoint ${name} is unavailable.`);',
|
|
83
|
-
' // Without a Serve execute() pipeline there is nothing to enforce roles, scopes,',
|
|
84
|
-
' // or tenancy, so refuse a trusted principal instead of silently ignoring it.',
|
|
85
|
-
' return (argument = {}) => {',
|
|
86
|
-
' if (argument?.trustedAuth != null) {',
|
|
87
|
-
' throw new Error(`Runtime entrypoint ${name} cannot enforce a trusted principal without a Serve execute() pipeline.`);',
|
|
88
|
-
' }',
|
|
89
|
-
' return run(argument);',
|
|
90
|
-
' };',
|
|
91
|
-
'};',
|
|
92
|
-
`const runtimeEntries = Object.freeze(Object.fromEntries(${names}.map(name => [name, resolveQuery(name)])));`,
|
|
93
|
-
`export const ${prefix[0]} = ${registryExpression};`,
|
|
94
|
-
'',
|
|
95
|
-
].join('\n');
|
|
96
|
-
}
|
|
97
|
-
export async function buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, entrypointPrefix = 'queries') {
|
|
98
|
-
if (runtimeEntrypoints.length === 0) {
|
|
99
|
-
throw new Error('Cannot build a runtime artifact without Serve query entrypoints.');
|
|
100
|
-
}
|
|
101
|
-
const names = getValidatedEntrypointNames(runtimeEntrypoints);
|
|
102
|
-
const { resolved, specifier } = projectRelativeImport(apiPath);
|
|
103
|
-
let source;
|
|
104
|
-
try {
|
|
105
|
-
source = runtimeEntrySource(specifier, names, entrypointPrefix);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
throw new Error(`Invalid runtime entrypoint prefix: ${entrypointPrefix}`);
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
const result = await build({
|
|
112
|
-
stdin: {
|
|
113
|
-
contents: source,
|
|
114
|
-
loader: 'ts',
|
|
115
|
-
resolveDir: process.cwd(),
|
|
116
|
-
sourcefile: 'hypequery-runtime-entry.ts',
|
|
117
|
-
},
|
|
118
|
-
absWorkingDir: process.cwd(),
|
|
119
|
-
bundle: true,
|
|
120
|
-
charset: 'utf8',
|
|
121
|
-
format: 'esm',
|
|
122
|
-
legalComments: 'none',
|
|
123
|
-
logLevel: 'warning',
|
|
124
|
-
minify: true,
|
|
125
|
-
platform: 'node',
|
|
126
|
-
sourcemap: false,
|
|
127
|
-
target: ['node18'],
|
|
128
|
-
treeShaking: true,
|
|
129
|
-
tsconfig: await findNearestTsconfig(resolved) ?? undefined,
|
|
130
|
-
write: false,
|
|
131
|
-
});
|
|
132
|
-
const output = result.outputFiles?.find(file => file.path.endsWith('.js'))
|
|
133
|
-
?? result.outputFiles?.[0];
|
|
134
|
-
if (!output)
|
|
135
|
-
throw new Error('esbuild produced no output');
|
|
136
|
-
const bytes = output.contents;
|
|
137
|
-
return Object.freeze({
|
|
138
|
-
bytes,
|
|
139
|
-
artifactSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
140
|
-
entrypointPrefix,
|
|
141
|
-
runtimeEntrypoints: names,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
throw new Error(`Failed to build the Node runtime artifact from ${apiPath}.\n`
|
|
146
|
-
+ `Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function getValidatedEntrypointNames(input) {
|
|
150
|
-
const names = input.map((name) => {
|
|
151
|
-
try {
|
|
152
|
-
return parseProtocolIdentifier(name);
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
throw new Error(`Invalid runtime entrypoint name: ${name}`);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
if (new Set(names).size !== names.length) {
|
|
159
|
-
throw new Error('Runtime entrypoint names must be unique.');
|
|
160
|
-
}
|
|
161
|
-
return Object.freeze([...names].sort());
|
|
162
|
-
}
|