@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,611 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { constants } from 'node:fs';
3
- import { lstat, mkdir, mkdtemp, open, readdir, rename, rm, } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { prepareProtocolDeploymentReleaseEnvelope, validateProtocolDeploymentReleaseTarget, } from '@hypequery/protocol';
6
- const ACTIVATION_FILE = 'activation.json';
7
- const CLAIMS_DIRECTORY = 'claims';
8
- // Each predecessor has one filesystem slot for its successor. Renaming a
9
- // complete record into that slot is the cross-process compare-and-swap.
10
- const INITIAL_CLAIM = 'initial';
11
- const TARGET_FILE = 'target.json';
12
- const MAX_ACTIVATION_BYTES = 4096;
13
- const MAX_TARGET_BYTES = 1024;
14
- const IDENTITY_PATTERN = /^[0-9a-f]{64}$/;
15
- const ACTIVATION_IDENTITY_DOMAIN = 'hypequery:deployment-activation:v1\0';
16
- const TARGET_IDENTITY_DOMAIN = 'hypequery:deployment-target:v1\0';
17
- const textDecoder = new TextDecoder('utf-8', { fatal: true });
18
- export class DeploymentActivationError extends Error {
19
- code;
20
- constructor(code, message, options = {}) {
21
- super(message, options.cause === undefined ? undefined : { cause: options.cause });
22
- this.name = 'DeploymentActivationError';
23
- this.code = code;
24
- }
25
- }
26
- function activationError(code, message, cause) {
27
- return new DeploymentActivationError(code, message, { cause });
28
- }
29
- function errorCode(error) {
30
- return typeof error === 'object' && error !== null && 'code' in error
31
- ? String(error.code)
32
- : undefined;
33
- }
34
- function isOperationalFileSystemError(error) {
35
- const code = errorCode(error);
36
- return code !== undefined && !['EISDIR', 'ELOOP', 'ENOENT', 'ENOTDIR'].includes(code);
37
- }
38
- function sha256(domain, value) {
39
- return createHash('sha256').update(domain).update(value).digest('hex');
40
- }
41
- function targetCanonical(target) {
42
- return JSON.stringify({ project: target.project, environment: target.environment });
43
- }
44
- function targetsEqual(left, right) {
45
- return left.project === right.project && left.environment === right.environment;
46
- }
47
- function validateTarget(input, code) {
48
- try {
49
- return validateProtocolDeploymentReleaseTarget(input);
50
- }
51
- catch (error) {
52
- throw activationError(code, 'Deployment activation target is invalid.', error);
53
- }
54
- }
55
- function requireIdentity(input, description, code) {
56
- if (typeof input !== 'string' || !IDENTITY_PATTERN.test(input)) {
57
- throw activationError(code, `${description} must be 64 lowercase hexadecimal characters.`);
58
- }
59
- return input;
60
- }
61
- function requireNullableIdentity(input, description, code) {
62
- return input === null ? null : requireIdentity(input, description, code);
63
- }
64
- function activationPayloadCanonical(input) {
65
- // Every value has already been reduced to a closed primitive shape, so this
66
- // fixed construction order is also the canonical byte order persisted below.
67
- return JSON.stringify({
68
- kind: 'hypequery-deployment-activation',
69
- version: 1,
70
- target: {
71
- project: input.target.project,
72
- environment: input.target.environment,
73
- },
74
- releaseIdentity: input.releaseIdentity,
75
- previousRevision: input.previousRevision,
76
- previousReleaseIdentity: input.previousReleaseIdentity,
77
- activatedAt: input.activatedAt,
78
- });
79
- }
80
- function activationCanonical(record) {
81
- return JSON.stringify({
82
- kind: record.kind,
83
- version: record.version,
84
- revision: record.revision,
85
- target: {
86
- project: record.target.project,
87
- environment: record.target.environment,
88
- },
89
- releaseIdentity: record.releaseIdentity,
90
- previousRevision: record.previousRevision,
91
- previousReleaseIdentity: record.previousReleaseIdentity,
92
- activatedAt: record.activatedAt,
93
- });
94
- }
95
- function prepareActivation(input) {
96
- const payload = activationPayloadCanonical(input);
97
- const revision = sha256(ACTIVATION_IDENTITY_DOMAIN, payload);
98
- const record = Object.freeze({
99
- kind: 'hypequery-deployment-activation',
100
- version: 1,
101
- revision,
102
- target: input.target,
103
- releaseIdentity: input.releaseIdentity,
104
- previousRevision: input.previousRevision,
105
- previousReleaseIdentity: input.previousReleaseIdentity,
106
- activatedAt: input.activatedAt,
107
- });
108
- const canonical = activationCanonical(record);
109
- return Object.freeze({ record, canonical, bytes: Buffer.from(canonical) });
110
- }
111
- function requireRecord(input) {
112
- if (typeof input !== 'object' || input === null || Array.isArray(input)) {
113
- throw new Error('Activation record must be an object.');
114
- }
115
- const value = input;
116
- const expected = [
117
- 'activatedAt',
118
- 'kind',
119
- 'previousReleaseIdentity',
120
- 'previousRevision',
121
- 'releaseIdentity',
122
- 'revision',
123
- 'target',
124
- 'version',
125
- ];
126
- if (Object.keys(value).sort().join('\0') !== expected.join('\0')) {
127
- throw new Error('Activation record fields are inconsistent.');
128
- }
129
- return value;
130
- }
131
- function prepareValidatedActivation(input) {
132
- const value = requireRecord(input);
133
- if (value.kind !== 'hypequery-deployment-activation' || value.version !== 1) {
134
- throw new Error('Activation record kind or version is invalid.');
135
- }
136
- const target = validateTarget(value.target, 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
137
- const releaseIdentity = requireIdentity(value.releaseIdentity, 'Stored release identity', 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
138
- const previousRevision = requireNullableIdentity(value.previousRevision, 'Stored previous revision', 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
139
- const previousReleaseIdentity = requireNullableIdentity(value.previousReleaseIdentity, 'Stored previous release identity', 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
140
- if ((previousRevision === null) !== (previousReleaseIdentity === null)) {
141
- throw new Error('Activation record predecessor is inconsistent.');
142
- }
143
- if (typeof value.activatedAt !== 'string') {
144
- throw new Error('Activation timestamp is invalid.');
145
- }
146
- const timestamp = new Date(value.activatedAt);
147
- if (!Number.isFinite(timestamp.valueOf()) || timestamp.toISOString() !== value.activatedAt) {
148
- throw new Error('Activation timestamp is not a canonical ISO timestamp.');
149
- }
150
- const prepared = prepareActivation({
151
- target,
152
- releaseIdentity,
153
- previousRevision,
154
- previousReleaseIdentity,
155
- activatedAt: value.activatedAt,
156
- });
157
- if (value.revision !== prepared.record.revision) {
158
- throw new Error('Activation revision does not match its contents.');
159
- }
160
- return prepared;
161
- }
162
- /** Validate and recompute an immutable deployment activation record. */
163
- export function validateDeploymentActivationRecord(input) {
164
- return prepareValidatedActivation(input).record;
165
- }
166
- function validateStoredActivation(input, expectedTarget, expectedPreviousRevision, expectedPreviousReleaseIdentity) {
167
- const prepared = prepareValidatedActivation(input);
168
- if (!targetsEqual(prepared.record.target, expectedTarget)) {
169
- throw new Error('Activation record target is inconsistent.');
170
- }
171
- if (prepared.record.previousRevision !== expectedPreviousRevision
172
- || prepared.record.previousReleaseIdentity !== expectedPreviousReleaseIdentity) {
173
- throw new Error('Activation record predecessor is inconsistent.');
174
- }
175
- return prepared;
176
- }
177
- async function pathExists(filePath) {
178
- try {
179
- await lstat(filePath);
180
- return true;
181
- }
182
- catch (error) {
183
- if (errorCode(error) === 'ENOENT')
184
- return false;
185
- throw error;
186
- }
187
- }
188
- async function requireRegularDirectory(directory, description) {
189
- const stat = await lstat(directory);
190
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
191
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE', `${description} must be a regular directory: ${directory}`);
192
- }
193
- }
194
- async function ensureRegularDirectory(directory, description) {
195
- let created = false;
196
- try {
197
- created = await mkdir(directory, { recursive: true, mode: 0o700 }) !== undefined;
198
- }
199
- catch (error) {
200
- if (errorCode(error) !== 'EEXIST')
201
- throw error;
202
- }
203
- await requireRegularDirectory(directory, description);
204
- return created;
205
- }
206
- async function syncDirectory(directory) {
207
- if (process.platform === 'win32')
208
- return;
209
- const handle = await open(directory, constants.O_RDONLY);
210
- try {
211
- await handle.sync();
212
- }
213
- finally {
214
- await handle.close();
215
- }
216
- }
217
- async function writeExclusiveFile(filePath, bytes) {
218
- const handle = await open(filePath, 'wx', 0o600);
219
- try {
220
- let offset = 0;
221
- while (offset < bytes.byteLength) {
222
- const result = await handle.write(bytes, offset, bytes.byteLength - offset);
223
- if (result.bytesWritten < 1)
224
- throw new Error('Filesystem write made no progress.');
225
- offset += result.bytesWritten;
226
- }
227
- await handle.sync();
228
- }
229
- finally {
230
- await handle.close();
231
- }
232
- }
233
- async function readRegularFile(filePath, maximumBytes) {
234
- const initial = await lstat(filePath);
235
- if (initial.isSymbolicLink() || !initial.isFile()
236
- || initial.size < 1 || initial.size > maximumBytes) {
237
- throw new Error(`Stored entry is not a bounded regular file: ${filePath}`);
238
- }
239
- const handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
240
- try {
241
- const stat = await handle.stat();
242
- if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes) {
243
- throw new Error(`Stored entry is not a bounded regular file: ${filePath}`);
244
- }
245
- return await handle.readFile();
246
- }
247
- finally {
248
- await handle.close();
249
- }
250
- }
251
- async function withStagingDirectory(root, prefix, action) {
252
- const staging = await mkdtemp(path.join(root, prefix));
253
- let result;
254
- let failure;
255
- try {
256
- result = await action(staging);
257
- }
258
- catch (error) {
259
- failure = { error };
260
- }
261
- try {
262
- await rm(staging, { force: true, recursive: true });
263
- }
264
- catch (error) {
265
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', `Could not clean deployment activation staging directory: ${staging}`, failure ? new AggregateError([failure.error, error]) : error);
266
- }
267
- if (failure)
268
- throw failure.error;
269
- return result;
270
- }
271
- async function readCanonicalTarget(targetDirectory, expectedTarget) {
272
- try {
273
- await requireRegularDirectory(targetDirectory, 'Deployment activation target');
274
- const entries = (await readdir(targetDirectory)).sort();
275
- if (entries.join('\0') !== `${CLAIMS_DIRECTORY}\0${TARGET_FILE}`) {
276
- throw new Error('Deployment activation target contains undeclared or missing entries.');
277
- }
278
- await requireRegularDirectory(path.join(targetDirectory, CLAIMS_DIRECTORY), 'Deployment activation claims');
279
- const bytes = await readRegularFile(path.join(targetDirectory, TARGET_FILE), MAX_TARGET_BYTES);
280
- let input;
281
- try {
282
- input = JSON.parse(textDecoder.decode(bytes));
283
- }
284
- catch (error) {
285
- throw new Error('Stored deployment activation target is invalid JSON.', { cause: error });
286
- }
287
- const target = validateTarget(input, 'HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE');
288
- if (!targetsEqual(target, expectedTarget)
289
- || !Buffer.from(bytes).equals(Buffer.from(targetCanonical(target)))) {
290
- throw new Error('Stored deployment activation target is inconsistent.');
291
- }
292
- }
293
- catch (error) {
294
- if (error instanceof DeploymentActivationError)
295
- throw error;
296
- if (isOperationalFileSystemError(error))
297
- throw error;
298
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE', 'Stored deployment activation target is invalid.', error);
299
- }
300
- }
301
- async function ensureTargetDirectory(activationsRoot, target) {
302
- const canonical = targetCanonical(target);
303
- const targetKey = sha256(TARGET_IDENTITY_DOMAIN, canonical);
304
- const destination = path.join(activationsRoot, targetKey);
305
- if (await pathExists(destination)) {
306
- await readCanonicalTarget(destination, target);
307
- return destination;
308
- }
309
- await withStagingDirectory(activationsRoot, '.target-staging-', async (staging) => {
310
- await mkdir(path.join(staging, CLAIMS_DIRECTORY), { mode: 0o700 });
311
- await writeExclusiveFile(path.join(staging, TARGET_FILE), Buffer.from(canonical));
312
- await syncDirectory(path.join(staging, CLAIMS_DIRECTORY));
313
- await syncDirectory(staging);
314
- try {
315
- await rename(staging, destination);
316
- }
317
- catch (error) {
318
- if (!await pathExists(destination))
319
- throw error;
320
- await readCanonicalTarget(destination, target);
321
- return;
322
- }
323
- await syncDirectory(activationsRoot);
324
- });
325
- await readCanonicalTarget(destination, target);
326
- return destination;
327
- }
328
- async function readStoredActivation(claimDirectory, target, previousRevision, previousReleaseIdentity) {
329
- await requireRegularDirectory(claimDirectory, 'Deployment activation claim');
330
- const entries = await readdir(claimDirectory);
331
- if (entries.length !== 1 || entries[0] !== ACTIVATION_FILE) {
332
- throw new Error('Deployment activation claim contains undeclared or missing entries.');
333
- }
334
- const bytes = await readRegularFile(path.join(claimDirectory, ACTIVATION_FILE), MAX_ACTIVATION_BYTES);
335
- let input;
336
- try {
337
- input = JSON.parse(textDecoder.decode(bytes));
338
- }
339
- catch (error) {
340
- throw new Error('Stored deployment activation is invalid JSON.', { cause: error });
341
- }
342
- const prepared = validateStoredActivation(input, target, previousRevision, previousReleaseIdentity);
343
- if (!Buffer.from(bytes).equals(Buffer.from(prepared.bytes))) {
344
- throw new Error('Stored deployment activation is not canonical JSON.');
345
- }
346
- return prepared;
347
- }
348
- async function visitHistory(targetDirectory, target, visitor) {
349
- try {
350
- await readCanonicalTarget(targetDirectory, target);
351
- const claimsDirectory = path.join(targetDirectory, CLAIMS_DIRECTORY);
352
- const claimNames = (await readdir(claimsDirectory)).sort();
353
- for (const name of claimNames) {
354
- if (name !== INITIAL_CLAIM && !IDENTITY_PATTERN.test(name)) {
355
- throw new Error(`Deployment activation claim name is invalid: ${name}`);
356
- }
357
- }
358
- const remaining = new Set(claimNames);
359
- const revisions = new Set();
360
- let previousRevision = null;
361
- let previousReleaseIdentity = null;
362
- // Following predecessor-named claims derives the head without trusting a
363
- // mutable pointer. The remaining-set check rejects orphaned branches.
364
- for (;;) {
365
- const claimName = previousRevision ?? INITIAL_CLAIM;
366
- if (!remaining.delete(claimName))
367
- break;
368
- const prepared = await readStoredActivation(path.join(claimsDirectory, claimName), target, previousRevision, previousReleaseIdentity);
369
- if (revisions.has(prepared.record.revision)) {
370
- throw new Error('Deployment activation history contains a cycle.');
371
- }
372
- revisions.add(prepared.record.revision);
373
- visitor(prepared.record);
374
- previousRevision = prepared.record.revision;
375
- previousReleaseIdentity = prepared.record.releaseIdentity;
376
- }
377
- if (remaining.size > 0) {
378
- throw new Error('Deployment activation history contains unreachable claims.');
379
- }
380
- }
381
- catch (error) {
382
- if (error instanceof DeploymentActivationError)
383
- throw error;
384
- if (isOperationalFileSystemError(error))
385
- throw error;
386
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CORRUPT_STATE', 'Stored deployment activation history is invalid.', error);
387
- }
388
- }
389
- async function resolveHistory(targetDirectory, target) {
390
- const history = [];
391
- await visitHistory(targetDirectory, target, record => { history.push(record); });
392
- return Object.freeze(history);
393
- }
394
- async function resolveCurrent(targetDirectory, target) {
395
- let current;
396
- await visitHistory(targetDirectory, target, record => { current = record; });
397
- return current;
398
- }
399
- function validateHistoryQuery(input) {
400
- if (!Number.isSafeInteger(input?.limit) || input.limit < 1
401
- || (input.before !== undefined && !IDENTITY_PATTERN.test(input.before))) {
402
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST', 'Deployment activation history query is invalid.');
403
- }
404
- return Object.freeze({
405
- limit: input.limit,
406
- ...(input.before === undefined ? {} : { before: input.before }),
407
- });
408
- }
409
- async function resolveHistoryPage(targetDirectory, target, query) {
410
- const retained = [];
411
- let beforeFound = query.before === undefined;
412
- let beforeReached = false;
413
- await visitHistory(targetDirectory, target, record => {
414
- if (record.revision === query.before) {
415
- beforeFound = true;
416
- beforeReached = true;
417
- return;
418
- }
419
- if (beforeReached)
420
- return;
421
- retained.push(record);
422
- if (retained.length > query.limit + 1)
423
- retained.shift();
424
- });
425
- if (!beforeFound) {
426
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST', 'Deployment activation history cursor was not found.');
427
- }
428
- const hasOlder = retained.length > query.limit;
429
- if (hasOlder)
430
- retained.shift();
431
- const activations = Object.freeze(retained);
432
- return Object.freeze({
433
- activations,
434
- nextBefore: hasOlder ? activations[0].revision : null,
435
- });
436
- }
437
- export function createFileSystemDeploymentActivationRegistry(options) {
438
- if (typeof options.directory !== 'string' || options.directory.length < 1) {
439
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CONFIGURATION', 'Deployment activation directory is required.');
440
- }
441
- if (typeof options.releases?.read !== 'function') {
442
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CONFIGURATION', 'A deployment release reader is required.');
443
- }
444
- const root = path.resolve(options.directory);
445
- if (root === path.parse(root).root) {
446
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CONFIGURATION', 'Deployment activation directory cannot be a filesystem root.');
447
- }
448
- const clock = options.clock ?? (() => new Date());
449
- const activations = path.join(root, 'activations');
450
- let initializationPromise;
451
- async function initializeActivationRoot() {
452
- const rootCreated = await ensureRegularDirectory(root, 'Deployment store root');
453
- const activationsCreated = await ensureRegularDirectory(activations, 'Deployment activation store');
454
- if (activationsCreated)
455
- await syncDirectory(root);
456
- if (rootCreated)
457
- await syncDirectory(path.dirname(root));
458
- }
459
- async function activationRoot() {
460
- const pending = initializationPromise ??= initializeActivationRoot();
461
- try {
462
- await pending;
463
- }
464
- catch (error) {
465
- if (initializationPromise === pending)
466
- initializationPromise = undefined;
467
- throw error;
468
- }
469
- await requireRegularDirectory(root, 'Deployment store root');
470
- await requireRegularDirectory(activations, 'Deployment activation store');
471
- return activations;
472
- }
473
- async function history(targetInput) {
474
- const target = validateTarget(targetInput, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
475
- try {
476
- const activations = await activationRoot();
477
- const key = sha256(TARGET_IDENTITY_DOMAIN, targetCanonical(target));
478
- const targetDirectory = path.join(activations, key);
479
- if (!await pathExists(targetDirectory))
480
- return Object.freeze([]);
481
- return await resolveHistory(targetDirectory, target);
482
- }
483
- catch (error) {
484
- if (error instanceof DeploymentActivationError)
485
- throw error;
486
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not read deployment activation history.', error);
487
- }
488
- }
489
- async function current(targetInput) {
490
- const target = validateTarget(targetInput, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
491
- try {
492
- const key = sha256(TARGET_IDENTITY_DOMAIN, targetCanonical(target));
493
- const targetDirectory = path.join(await activationRoot(), key);
494
- if (!await pathExists(targetDirectory))
495
- return undefined;
496
- return await resolveCurrent(targetDirectory, target);
497
- }
498
- catch (error) {
499
- if (error instanceof DeploymentActivationError)
500
- throw error;
501
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not read the current deployment activation.', error);
502
- }
503
- }
504
- async function historyPage(targetInput, queryInput) {
505
- const target = validateTarget(targetInput, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
506
- const query = validateHistoryQuery(queryInput);
507
- try {
508
- const key = sha256(TARGET_IDENTITY_DOMAIN, targetCanonical(target));
509
- const targetDirectory = path.join(await activationRoot(), key);
510
- if (!await pathExists(targetDirectory)) {
511
- return Object.freeze({ activations: Object.freeze([]), nextBefore: null });
512
- }
513
- return await resolveHistoryPage(targetDirectory, target, query);
514
- }
515
- catch (error) {
516
- if (error instanceof DeploymentActivationError)
517
- throw error;
518
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not read deployment activation history.', error);
519
- }
520
- }
521
- async function activate(request) {
522
- const target = validateTarget(request.target, 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
523
- const releaseIdentity = requireIdentity(request.releaseIdentity, 'Deployment release identity', 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
524
- const expectedRevision = requireNullableIdentity(request.expectedRevision, 'Expected deployment activation revision', 'HQ_DEPLOYMENT_ACTIVATION_INVALID_REQUEST');
525
- let storedRelease;
526
- try {
527
- storedRelease = await options.releases.read(releaseIdentity);
528
- }
529
- catch (error) {
530
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_RELEASE_UNAVAILABLE', `Could not verify deployment release before activation: ${releaseIdentity}`, error);
531
- }
532
- if (!storedRelease) {
533
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_RELEASE_NOT_FOUND', `Deployment release is not stored: ${releaseIdentity}`);
534
- }
535
- let verifiedRelease;
536
- try {
537
- verifiedRelease = prepareProtocolDeploymentReleaseEnvelope(storedRelease.release);
538
- }
539
- catch (error) {
540
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_RELEASE_UNAVAILABLE', `Stored deployment release is invalid: ${releaseIdentity}`, error);
541
- }
542
- if (storedRelease.releaseIdentity !== releaseIdentity
543
- || verifiedRelease.identity !== releaseIdentity) {
544
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_RELEASE_UNAVAILABLE', `Stored deployment release identity is inconsistent: ${releaseIdentity}`);
545
- }
546
- if (!targetsEqual(verifiedRelease.release.target, target)) {
547
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_TARGET_MISMATCH', 'Deployment release target does not match the activation target.');
548
- }
549
- try {
550
- const activations = await activationRoot();
551
- const targetDirectory = await ensureTargetDirectory(activations, target);
552
- const existing = await resolveCurrent(targetDirectory, target);
553
- if (existing?.releaseIdentity === releaseIdentity) {
554
- return Object.freeze({ status: 'already-active', activation: existing });
555
- }
556
- if ((existing?.revision ?? null) !== expectedRevision) {
557
- return Object.freeze({ status: 'conflict', current: existing ?? null });
558
- }
559
- let now;
560
- try {
561
- now = clock();
562
- if (!(now instanceof Date) || !Number.isFinite(now.valueOf()))
563
- throw new Error('Invalid date.');
564
- }
565
- catch (error) {
566
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_CONFIGURATION', 'Deployment activation clock returned an invalid value.', error);
567
- }
568
- const prepared = prepareActivation({
569
- target,
570
- releaseIdentity,
571
- previousRevision: existing?.revision ?? null,
572
- previousReleaseIdentity: existing?.releaseIdentity ?? null,
573
- activatedAt: now.toISOString(),
574
- });
575
- const claimsDirectory = path.join(targetDirectory, CLAIMS_DIRECTORY);
576
- const claimName = existing?.revision ?? INITIAL_CLAIM;
577
- const destination = path.join(claimsDirectory, claimName);
578
- const published = await withStagingDirectory(
579
- // Staging lives outside the exact target directory, so interrupted
580
- // writers are invisible to history readers until the atomic rename.
581
- activations, '.activation-staging-', async (staging) => {
582
- await writeExclusiveFile(path.join(staging, ACTIVATION_FILE), prepared.bytes);
583
- await syncDirectory(staging);
584
- try {
585
- await rename(staging, destination);
586
- }
587
- catch (error) {
588
- if (!await pathExists(destination))
589
- throw error;
590
- return false;
591
- }
592
- await syncDirectory(claimsDirectory);
593
- return true;
594
- });
595
- if (published) {
596
- return Object.freeze({ status: 'activated', activation: prepared.record });
597
- }
598
- const resolved = await resolveCurrent(targetDirectory, target);
599
- if (resolved?.releaseIdentity === releaseIdentity) {
600
- return Object.freeze({ status: 'already-active', activation: resolved });
601
- }
602
- return Object.freeze({ status: 'conflict', current: resolved ?? null });
603
- }
604
- catch (error) {
605
- if (error instanceof DeploymentActivationError)
606
- throw error;
607
- throw activationError('HQ_DEPLOYMENT_ACTIVATION_IO', 'Could not update deployment activation state.', error);
608
- }
609
- }
610
- return Object.freeze({ activate, current, history, historyPage });
611
- }
@@ -1,7 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import type { DeploymentControlPlane } from './control-plane.js';
3
- export type DeploymentControlPlaneFetchHandler = (request: Request) => Promise<Response>;
4
- export type DeploymentControlPlaneNodeHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
5
- export declare function createDeploymentControlPlaneFetchHandler(controlPlane: DeploymentControlPlane): DeploymentControlPlaneFetchHandler;
6
- export declare function createDeploymentControlPlaneNodeHandler(controlPlane: DeploymentControlPlane): DeploymentControlPlaneNodeHandler;
7
- //# sourceMappingURL=control-plane-adapters.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"control-plane-adapters.d.ts","sourceRoot":"","sources":["../src/control-plane-adapters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EACV,sBAAsB,EAGvB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,kCAAkC,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AACzF,MAAM,MAAM,iCAAiC,GAAG,CAC9C,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,KACrB,OAAO,CAAC,IAAI,CAAC,CAAC;AAkHnB,wBAAgB,wCAAwC,CACtD,YAAY,EAAE,sBAAsB,GACnC,kCAAkC,CAkBpC;AAED,wBAAgB,uCAAuC,CACrD,YAAY,EAAE,sBAAsB,GACnC,iCAAiC,CAgCnC"}