@hypequery/deployment 0.0.0-canary-20260719200737 → 0.1.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.
@@ -1,418 +0,0 @@
1
- import { constants } from 'node:fs';
2
- import { lstat, mkdir, mkdtemp, open, readdir, rename, rm, } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { prepareProtocolDeploymentReleaseEnvelope, } from '@hypequery/protocol';
5
- import { DEPLOYMENT_BUNDLE_MANIFEST, verifyDeploymentBundle, } from './bundle.js';
6
- const RELEASE_FILE = 'release.json';
7
- const MAX_MANIFEST_BYTES = 1024 * 1024;
8
- const MAX_RELEASE_BYTES = 16 * 1024;
9
- const COPY_BUFFER_BYTES = 64 * 1024;
10
- const IDENTITY_PATTERN = /^[0-9a-f]{64}$/;
11
- const textDecoder = new TextDecoder('utf-8', { fatal: true });
12
- export class FileSystemDeploymentStoreError extends Error {
13
- code;
14
- constructor(code, message, options = {}) {
15
- super(message, options.cause === undefined ? undefined : { cause: options.cause });
16
- this.name = 'FileSystemDeploymentStoreError';
17
- this.code = code;
18
- }
19
- }
20
- function storeError(code, message, cause) {
21
- return new FileSystemDeploymentStoreError(code, message, { cause });
22
- }
23
- function errorCode(error) {
24
- return typeof error === 'object' && error !== null && 'code' in error
25
- ? String(error.code)
26
- : undefined;
27
- }
28
- async function pathExists(filePath) {
29
- try {
30
- await lstat(filePath);
31
- return true;
32
- }
33
- catch (error) {
34
- if (errorCode(error) === 'ENOENT')
35
- return false;
36
- throw error;
37
- }
38
- }
39
- async function requireRegularDirectory(directory, description, code = 'HQ_DEPLOYMENT_STORE_CORRUPT_STATE') {
40
- const stat = await lstat(directory);
41
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
42
- throw storeError(code, `${description} must be a regular directory: ${directory}`);
43
- }
44
- }
45
- async function ensureRegularStoreDirectory(directory, description) {
46
- let created = false;
47
- try {
48
- created = await mkdir(directory, { recursive: true, mode: 0o700 }) !== undefined;
49
- }
50
- catch (error) {
51
- if (errorCode(error) !== 'EEXIST')
52
- throw error;
53
- }
54
- await requireRegularDirectory(directory, description);
55
- return created;
56
- }
57
- async function ensureStoreDirectories(root) {
58
- const rootCreated = await ensureRegularStoreDirectory(root, 'Deployment store root');
59
- const bundles = path.join(root, 'bundles');
60
- const releases = path.join(root, 'releases');
61
- const bundlesCreated = await ensureRegularStoreDirectory(bundles, 'Deployment bundle store');
62
- const releasesCreated = await ensureRegularStoreDirectory(releases, 'Deployment release store');
63
- if (bundlesCreated || releasesCreated)
64
- await syncDirectory(root);
65
- if (rootCreated)
66
- await syncDirectory(path.dirname(root));
67
- return Object.freeze({ bundles, releases });
68
- }
69
- async function syncDirectory(directory) {
70
- if (process.platform === 'win32')
71
- return;
72
- const handle = await open(directory, constants.O_RDONLY);
73
- try {
74
- await handle.sync();
75
- }
76
- finally {
77
- await handle.close();
78
- }
79
- }
80
- async function writeAll(handle, bytes) {
81
- let offset = 0;
82
- while (offset < bytes.byteLength) {
83
- const result = await handle.write(bytes, offset, bytes.byteLength - offset);
84
- if (result.bytesWritten < 1)
85
- throw new Error('Filesystem write made no progress.');
86
- offset += result.bytesWritten;
87
- }
88
- }
89
- async function writeExclusiveFile(filePath, bytes) {
90
- const handle = await open(filePath, 'wx', 0o600);
91
- try {
92
- await writeAll(handle, bytes);
93
- await handle.sync();
94
- }
95
- finally {
96
- await handle.close();
97
- }
98
- }
99
- async function copyRegularFile(sourcePath, destinationPath, maximumBytes, expectedBytes) {
100
- const initial = await lstat(sourcePath);
101
- if (initial.isSymbolicLink() || !initial.isFile()) {
102
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', `Verified bundle entry is no longer a regular file: ${sourcePath}`);
103
- }
104
- if (initial.size < 1 || initial.size > maximumBytes
105
- || (expectedBytes !== undefined && initial.size !== expectedBytes)) {
106
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', `Verified bundle entry changed before persistence: ${sourcePath}`);
107
- }
108
- let source;
109
- let destination;
110
- try {
111
- source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
112
- const stat = await source.stat();
113
- if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes
114
- || (expectedBytes !== undefined && stat.size !== expectedBytes)) {
115
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', `Verified bundle entry changed before persistence: ${sourcePath}`);
116
- }
117
- destination = await open(destinationPath, 'wx', 0o600);
118
- const buffer = Buffer.allocUnsafe(COPY_BUFFER_BYTES);
119
- let total = 0;
120
- for (;;) {
121
- const { bytesRead } = await source.read(buffer, 0, buffer.byteLength, null);
122
- if (bytesRead === 0)
123
- break;
124
- total += bytesRead;
125
- if (total > maximumBytes || (expectedBytes !== undefined && total > expectedBytes)) {
126
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', `Verified bundle entry changed before persistence: ${sourcePath}`);
127
- }
128
- await writeAll(destination, buffer.subarray(0, bytesRead));
129
- }
130
- if (expectedBytes !== undefined && total !== expectedBytes) {
131
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', `Verified bundle entry changed before persistence: ${sourcePath}`);
132
- }
133
- await destination.sync();
134
- }
135
- catch (error) {
136
- if (errorCode(error) === 'ELOOP') {
137
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', `Verified bundle entry became a symbolic link: ${sourcePath}`, error);
138
- }
139
- throw error;
140
- }
141
- finally {
142
- try {
143
- await destination?.close();
144
- }
145
- finally {
146
- await source?.close();
147
- }
148
- }
149
- }
150
- function bundleFilePaths(bundle) {
151
- return Object.freeze([
152
- { path: DEPLOYMENT_BUNDLE_MANIFEST },
153
- {
154
- path: bundle.manifest.deployment.path,
155
- byteLength: bundle.manifest.deployment.byteLength,
156
- },
157
- ...bundle.manifest.artifacts.map(artifact => ({
158
- path: artifact.path,
159
- byteLength: artifact.byteLength,
160
- })),
161
- ]);
162
- }
163
- async function syncBundleDirectories(bundleRoot, files) {
164
- const directories = new Set([bundleRoot]);
165
- for (const file of files) {
166
- const segments = file.path.split('/');
167
- for (let index = 1; index < segments.length; index += 1) {
168
- directories.add(path.join(bundleRoot, ...segments.slice(0, index)));
169
- }
170
- }
171
- const ordered = [...directories].sort((left, right) => (right.split(path.sep).length - left.split(path.sep).length));
172
- for (const directory of ordered)
173
- await syncDirectory(directory);
174
- }
175
- async function copyBundleToStaging(bundle, staging) {
176
- await requireRegularDirectory(bundle.directory, 'Verified deployment bundle', 'HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION');
177
- const files = bundleFilePaths(bundle);
178
- for (const file of files) {
179
- const destination = path.join(staging, ...file.path.split('/'));
180
- await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
181
- await copyRegularFile(path.join(bundle.directory, ...file.path.split('/')), destination, file.byteLength ?? MAX_MANIFEST_BYTES, file.byteLength);
182
- }
183
- await syncBundleDirectories(staging, files);
184
- let verified;
185
- try {
186
- verified = await verifyDeploymentBundle(staging);
187
- }
188
- catch (error) {
189
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', 'Deployment bundle changed before it could be persisted.', error);
190
- }
191
- if (verified.identity !== bundle.identity) {
192
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', 'Persisted bundle identity does not match the verified submission.');
193
- }
194
- return verified;
195
- }
196
- async function readRegularFile(filePath, maximumBytes) {
197
- const initial = await lstat(filePath);
198
- if (initial.isSymbolicLink() || !initial.isFile()
199
- || initial.size < 1 || initial.size > maximumBytes) {
200
- throw new Error(`Stored entry is not a bounded regular file: ${filePath}`);
201
- }
202
- const handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
203
- try {
204
- const stat = await handle.stat();
205
- if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes) {
206
- throw new Error(`Stored entry is not a bounded regular file: ${filePath}`);
207
- }
208
- const buffer = Buffer.allocUnsafe(Math.min(COPY_BUFFER_BYTES, maximumBytes + 1));
209
- const chunks = [];
210
- let total = 0;
211
- while (total <= maximumBytes) {
212
- const remaining = maximumBytes + 1 - total;
213
- const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.byteLength, remaining), null);
214
- if (bytesRead === 0)
215
- break;
216
- total += bytesRead;
217
- if (total > maximumBytes) {
218
- throw new Error(`Stored entry is not a bounded regular file: ${filePath}`);
219
- }
220
- chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
221
- }
222
- if (total < 1) {
223
- throw new Error(`Stored entry is not a bounded regular file: ${filePath}`);
224
- }
225
- return Buffer.concat(chunks, total);
226
- }
227
- finally {
228
- await handle.close();
229
- }
230
- }
231
- async function readStoredRelease(releaseDirectory, expectedIdentity) {
232
- await requireRegularDirectory(releaseDirectory, 'Stored deployment release');
233
- const entries = (await readdir(releaseDirectory)).sort();
234
- if (entries.length !== 1 || entries[0] !== RELEASE_FILE) {
235
- throw new Error('Stored deployment release contains undeclared or missing entries.');
236
- }
237
- const bytes = await readRegularFile(path.join(releaseDirectory, RELEASE_FILE), MAX_RELEASE_BYTES);
238
- let input;
239
- try {
240
- input = JSON.parse(textDecoder.decode(bytes));
241
- }
242
- catch (error) {
243
- throw new Error('Stored deployment release is not valid UTF-8 JSON.', { cause: error });
244
- }
245
- const prepared = prepareProtocolDeploymentReleaseEnvelope(input);
246
- if (prepared.identity !== expectedIdentity
247
- || !Buffer.from(bytes).equals(Buffer.from(prepared.bytes))) {
248
- throw new Error('Stored deployment release identity or canonical bytes do not match.');
249
- }
250
- return Object.freeze({ release: prepared.release, canonical: prepared.canonical });
251
- }
252
- async function verifyStoredBundle(bundleDirectory, expectedIdentity) {
253
- let bundle;
254
- try {
255
- bundle = await verifyDeploymentBundle(bundleDirectory);
256
- }
257
- catch (error) {
258
- throw storeError('HQ_DEPLOYMENT_STORE_CORRUPT_STATE', `Stored deployment bundle is invalid: ${expectedIdentity}`, error);
259
- }
260
- if (bundle.identity !== expectedIdentity) {
261
- throw storeError('HQ_DEPLOYMENT_STORE_CORRUPT_STATE', `Stored deployment bundle identity is inconsistent: ${expectedIdentity}`);
262
- }
263
- return bundle;
264
- }
265
- async function ensureStoredBundle(root, bundlesDirectory, bundle) {
266
- const destination = path.join(bundlesDirectory, bundle.identity);
267
- if (await pathExists(destination)) {
268
- await verifyStoredBundle(destination, bundle.identity);
269
- return;
270
- }
271
- await withStagingDirectory(root, '.bundle-staging-', async (staging) => {
272
- await copyBundleToStaging(bundle, staging);
273
- await publishDirectory(staging, destination, bundlesDirectory, async () => { await verifyStoredBundle(destination, bundle.identity); });
274
- });
275
- }
276
- async function publishDirectory(staging, destination, parent, verifyExisting) {
277
- if (await pathExists(destination)) {
278
- await verifyExisting();
279
- return false;
280
- }
281
- try {
282
- await rename(staging, destination);
283
- }
284
- catch (error) {
285
- if (!await pathExists(destination))
286
- throw error;
287
- await verifyExisting();
288
- return false;
289
- }
290
- await syncDirectory(parent);
291
- return true;
292
- }
293
- async function withStagingDirectory(root, prefix, action) {
294
- const staging = await mkdtemp(path.join(root, prefix));
295
- let result;
296
- let failure;
297
- try {
298
- result = await action(staging);
299
- }
300
- catch (error) {
301
- failure = { error };
302
- }
303
- try {
304
- await rm(staging, { force: true, recursive: true });
305
- }
306
- catch (error) {
307
- throw storeError('HQ_DEPLOYMENT_STORE_IO', `Could not clean deployment store staging directory: ${staging}`, failure ? new AggregateError([failure.error, error]) : error);
308
- }
309
- if (failure)
310
- throw failure.error;
311
- return result;
312
- }
313
- function prepareSubmission(submission) {
314
- let prepared;
315
- try {
316
- prepared = prepareProtocolDeploymentReleaseEnvelope(submission.release);
317
- }
318
- catch (error) {
319
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', 'Deployment release is invalid.', error);
320
- }
321
- if (prepared.identity !== submission.releaseIdentity
322
- || prepared.canonical !== submission.releaseCanonical
323
- || prepared.release.bundleIdentity !== submission.bundle.identity) {
324
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', 'Deployment submission identities or canonical release bytes are inconsistent.');
325
- }
326
- return prepared;
327
- }
328
- function requireIdentity(input) {
329
- if (!IDENTITY_PATTERN.test(input)) {
330
- throw storeError('HQ_DEPLOYMENT_STORE_INVALID_SUBMISSION', 'Deployment release identity must be 64 lowercase hexadecimal characters.');
331
- }
332
- return input;
333
- }
334
- export function createFileSystemDeploymentSubmissionStore(options) {
335
- if (typeof options.directory !== 'string' || options.directory.length < 1) {
336
- throw storeError('HQ_DEPLOYMENT_STORE_CONFIGURATION', 'Deployment store directory is required.');
337
- }
338
- const root = path.resolve(options.directory);
339
- if (root === path.parse(root).root) {
340
- throw storeError('HQ_DEPLOYMENT_STORE_CONFIGURATION', 'Deployment store directory cannot be a filesystem root.');
341
- }
342
- let directoriesPromise;
343
- async function storeDirectories() {
344
- const pending = directoriesPromise ??= ensureStoreDirectories(root);
345
- try {
346
- return await pending;
347
- }
348
- catch (error) {
349
- if (directoriesPromise === pending)
350
- directoriesPromise = undefined;
351
- throw error;
352
- }
353
- }
354
- async function read(releaseIdentity) {
355
- const identity = requireIdentity(releaseIdentity);
356
- let directories;
357
- try {
358
- directories = await storeDirectories();
359
- const releaseDirectory = path.join(directories.releases, identity);
360
- if (!await pathExists(releaseDirectory))
361
- return undefined;
362
- let storedRelease;
363
- try {
364
- storedRelease = await readStoredRelease(releaseDirectory, identity);
365
- }
366
- catch (error) {
367
- if (error instanceof FileSystemDeploymentStoreError)
368
- throw error;
369
- throw storeError('HQ_DEPLOYMENT_STORE_CORRUPT_STATE', `Stored deployment release is invalid: ${identity}`, error);
370
- }
371
- const bundle = await verifyStoredBundle(path.join(directories.bundles, storedRelease.release.bundleIdentity), storedRelease.release.bundleIdentity);
372
- return Object.freeze({
373
- releaseDirectory,
374
- release: storedRelease.release,
375
- releaseCanonical: storedRelease.canonical,
376
- releaseIdentity: identity,
377
- bundle,
378
- });
379
- }
380
- catch (error) {
381
- if (error instanceof FileSystemDeploymentStoreError)
382
- throw error;
383
- throw storeError('HQ_DEPLOYMENT_STORE_IO', `Could not read deployment release from ${root}.`, error);
384
- }
385
- }
386
- async function accept(submission) {
387
- const prepared = prepareSubmission(submission);
388
- try {
389
- const directories = await storeDirectories();
390
- await ensureStoredBundle(root, directories.bundles, submission.bundle);
391
- const releaseDestination = path.join(directories.releases, submission.releaseIdentity);
392
- const published = await withStagingDirectory(root, '.release-staging-', async (staging) => {
393
- await writeExclusiveFile(path.join(staging, RELEASE_FILE), prepared.bytes);
394
- await syncDirectory(staging);
395
- return publishDirectory(staging, releaseDestination, directories.releases, async () => {
396
- let existing;
397
- try {
398
- existing = await readStoredRelease(releaseDestination, submission.releaseIdentity);
399
- }
400
- catch (error) {
401
- throw storeError('HQ_DEPLOYMENT_STORE_CORRUPT_STATE', `Stored deployment release is invalid: ${submission.releaseIdentity}`, error);
402
- }
403
- if (existing.canonical !== prepared.canonical
404
- || existing.release.bundleIdentity !== submission.bundle.identity) {
405
- throw storeError('HQ_DEPLOYMENT_STORE_CORRUPT_STATE', `Stored deployment release conflicts with its identity: ${submission.releaseIdentity}`);
406
- }
407
- });
408
- });
409
- return published ? 'accepted' : 'already-exists';
410
- }
411
- catch (error) {
412
- if (error instanceof FileSystemDeploymentStoreError)
413
- throw error;
414
- throw storeError('HQ_DEPLOYMENT_STORE_IO', `Could not persist deployment submission in ${root}.`, error);
415
- }
416
- }
417
- return Object.freeze({ accept, read });
418
- }
@@ -1,16 +0,0 @@
1
- import type { DeploymentRuntimeFactory } from './runtime-supervisor.js';
2
- export type NodeDeploymentRuntimeErrorCode = 'HQ_NODE_RUNTIME_CONFIGURATION' | 'HQ_NODE_RUNTIME_UNSUPPORTED_ARTIFACT' | 'HQ_NODE_RUNTIME_INVALID_ARTIFACT' | 'HQ_NODE_RUNTIME_START_FAILED' | 'HQ_NODE_RUNTIME_WORKER_EXITED' | 'HQ_NODE_RUNTIME_INVOCATION_FAILED' | 'HQ_NODE_RUNTIME_ABORTED' | 'HQ_NODE_RUNTIME_CLOSED';
3
- export declare class NodeDeploymentRuntimeError extends Error {
4
- readonly code: NodeDeploymentRuntimeErrorCode;
5
- constructor(code: NodeDeploymentRuntimeErrorCode, message: string, options?: {
6
- readonly cause?: unknown;
7
- });
8
- }
9
- export interface NodeDeploymentRuntimeFactoryOptions {
10
- readonly temporaryDirectory?: string;
11
- /** Worker import deadline from 1 through 300,000 milliseconds. */
12
- readonly startupTimeoutMs?: number;
13
- readonly onCleanupError?: (error: unknown, directory: string) => void;
14
- }
15
- export declare function createNodeWorkerDeploymentRuntimeFactory(options?: NodeDeploymentRuntimeFactoryOptions): DeploymentRuntimeFactory;
16
- //# sourceMappingURL=node-runtime-factory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"node-runtime-factory.d.ts","sourceRoot":"","sources":["../src/node-runtime-factory.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,wBAAwB,EAGzB,MAAM,yBAAyB,CAAC;AA+CjC,MAAM,MAAM,8BAA8B,GACtC,+BAA+B,GAC/B,sCAAsC,GACtC,kCAAkC,GAClC,8BAA8B,GAC9B,+BAA+B,GAC/B,mCAAmC,GACnC,yBAAyB,GACzB,wBAAwB,CAAC;AAE7B,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAE,8BAA8B,CAAC;gBAG5C,IAAI,EAAE,8BAA8B,EACpC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAM7C;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACvE;AAoSD,wBAAgB,wCAAwC,CACtD,OAAO,GAAE,mCAAwC,GAChD,wBAAwB,CAuB1B"}