@astrale-os/sdk 0.6.0-beta.13 → 0.6.0-beta.14
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 +4 -0
- package/dist/deployment/adapter/adapter.d.ts +1 -6
- package/dist/deployment/adapter/legacy/adapter.d.ts +0 -10
- package/dist/deployment/adapter/legacy/deploy/context.d.ts +0 -2
- package/dist/deployment/adapter/legacy/deploy/result.d.ts +2 -3
- package/dist/deployment/adapter/legacy/index.d.ts +10 -8
- package/dist/deployment/adapter/legacy/prepare/context.d.ts +0 -2
- package/dist/deployment/adapter/release/context.d.ts +16 -2
- package/dist/deployment/address/index.d.ts +1 -1
- package/dist/deployment/address/index.js +1 -0
- package/dist/deployment/address/summary.d.ts +30 -0
- package/dist/deployment/address/summary.js +117 -1
- package/dist/deployment/build/index.d.ts +1 -1
- package/dist/deployment/build/index.js +1 -1
- package/dist/deployment/build/manifest.d.ts +5 -0
- package/dist/deployment/build/manifest.js +10 -0
- package/dist/deployment/index.d.ts +1 -1
- package/dist/deployment/release/legacy/index.d.ts +15 -10
- package/dist/deployment/release/legacy/index.js +15 -10
- package/dist/deployment/verify/readiness/deployment.js +3 -13
- package/dist/deployment/verify/readiness/fetch.d.ts +13 -0
- package/dist/deployment/verify/readiness/fetch.js +26 -0
- package/dist/deployment/verify/readiness/identity.d.ts +3 -6
- package/dist/deployment/verify/readiness/identity.js +3 -3
- package/dist/deployment/verify/readiness/index.d.ts +12 -8
- package/dist/deployment/verify/readiness/index.js +15 -4
- package/dist/deployment/verify/readiness/legacy/index.d.ts +2 -0
- package/dist/deployment/verify/readiness/legacy/index.js +2 -0
- package/dist/deployment/verify/readiness/legacy/publication.d.ts +12 -0
- package/dist/deployment/verify/readiness/legacy/publication.js +23 -0
- package/dist/deployment/verify/readiness/release.d.ts +8 -0
- package/dist/deployment/verify/readiness/release.js +22 -0
- package/dist/execution/identity/authentication.d.ts +7 -0
- package/dist/execution/identity/authentication.js +7 -1
- package/dist/execution/identity/delivery.d.ts +11 -4
- package/dist/execution/identity/delivery.js +47 -20
- package/dist/execution/identity/identity.d.ts +7 -3
- package/dist/execution/identity/identity.js +2 -2
- package/dist/execution/invocation/admission/authentication.d.ts +1 -2
- package/dist/execution/invocation/admission/authentication.js +1 -2
- package/dist/execution/invocation/admission/authority.d.ts +10 -4
- package/dist/execution/invocation/admission/authority.js +5 -1
- package/dist/execution/invocation/invocation.js +51 -14
- package/dist/execution/runtime/index.d.ts +1 -0
- package/dist/execution/runtime/index.js +1 -0
- package/dist/execution/runtime/load.d.ts +3 -3
- package/dist/execution/runtime/load.js +3 -2
- package/dist/execution/runtime/release.d.ts +22 -0
- package/dist/execution/runtime/release.js +14 -0
- package/dist/execution/serve.d.ts +2 -2
- package/dist/execution/service/create.js +9 -7
- package/dist/execution/service/release.d.ts +2 -2
- package/dist/execution/service/release.js +3 -2
- package/dist/execution/service/service.d.ts +9 -5
- package/dist/execution/service/serving/delivery.d.ts +6 -0
- package/dist/execution/service/{publication → serving}/delivery.js +5 -1
- package/dist/execution/service/{publication → serving}/index.d.ts +1 -1
- package/dist/execution/service/serving/installation.d.ts +4 -0
- package/dist/execution/service/serving/installation.js +9 -0
- package/dist/execution/service/serving/legacy/index.d.ts +15 -0
- package/dist/execution/service/serving/legacy/index.js +19 -0
- package/dist/execution/service/serving/source.d.ts +18 -0
- package/dist/execution/service/serving/source.js +19 -0
- package/dist/project/define.js +57 -28
- package/dist/project/install-command.d.ts +10 -0
- package/dist/project/install-command.js +13 -0
- package/dist/project/project.d.ts +4 -2
- package/dist/tooling/cli/arguments.d.ts +0 -2
- package/dist/tooling/cli/arguments.js +22 -15
- package/dist/tooling/cli/commit.d.ts +23 -0
- package/dist/tooling/cli/commit.js +216 -0
- package/dist/tooling/cli/deploy-mode.d.ts +19 -12
- package/dist/tooling/cli/deploy-mode.js +14 -32
- package/dist/tooling/cli/deploy-result.d.ts +10 -7
- package/dist/tooling/cli/deploy-result.js +4 -3
- package/dist/tooling/cli/development/candidate.d.ts +2 -3
- package/dist/tooling/cli/development/candidate.js +2 -9
- package/dist/tooling/cli/development/deployment.d.ts +2 -8
- package/dist/tooling/cli/development/deployment.js +2 -21
- package/dist/tooling/cli/development/develop.d.ts +4 -3
- package/dist/tooling/cli/development/develop.js +8 -112
- package/dist/tooling/cli/development/report.d.ts +1 -3
- package/dist/tooling/cli/development/report.js +1 -4
- package/dist/tooling/cli/help.js +28 -26
- package/dist/tooling/cli/immutable-deployment.d.ts +27 -0
- package/dist/tooling/cli/immutable-deployment.js +95 -0
- package/dist/tooling/cli/index.d.ts +2 -4
- package/dist/tooling/cli/index.js +2 -3
- package/dist/tooling/cli/legacy/deploy-result.d.ts +5 -3
- package/dist/tooling/cli/legacy/deploy-result.js +5 -3
- package/dist/tooling/cli/legacy/stable-target-adapter.d.ts +5 -3
- package/dist/tooling/cli/legacy/stable-target-deploy.d.ts +42 -0
- package/dist/tooling/cli/legacy/stable-target-deploy.js +104 -0
- package/dist/tooling/cli/orchestrate.js +46 -97
- package/dist/tooling/cli/secrets.d.ts +9 -0
- package/dist/tooling/cli/secrets.js +19 -0
- package/package.json +1 -1
- package/dist/deployment/verify/readiness/publication.d.ts +0 -11
- package/dist/deployment/verify/readiness/publication.js +0 -41
- package/dist/execution/service/publication/delivery.d.ts +0 -2
- package/dist/execution/service/publication/installation.d.ts +0 -3
- package/dist/execution/service/publication/installation.js +0 -8
- package/dist/execution/service/publication/source.d.ts +0 -3
- package/dist/execution/service/publication/source.js +0 -11
- package/dist/tooling/cli/astrale/authority.d.ts +0 -5
- package/dist/tooling/cli/astrale/authority.js +0 -11
- package/dist/tooling/cli/astrale/executable.d.ts +0 -2
- package/dist/tooling/cli/astrale/executable.js +0 -47
- package/dist/tooling/cli/astrale/failure.d.ts +0 -6
- package/dist/tooling/cli/astrale/failure.js +0 -48
- package/dist/tooling/cli/astrale/instance.d.ts +0 -11
- package/dist/tooling/cli/astrale/instance.js +0 -52
- package/dist/tooling/cli/astrale/process.d.ts +0 -13
- package/dist/tooling/cli/astrale/process.js +0 -94
- package/dist/tooling/cli/astrale/version.d.ts +0 -18
- package/dist/tooling/cli/astrale/version.js +0 -55
- package/dist/tooling/cli/development/release-evidence.d.ts +0 -4
- package/dist/tooling/cli/development/release-evidence.js +0 -10
- package/dist/tooling/cli/development/session-file.d.ts +0 -53
- package/dist/tooling/cli/development/session-file.js +0 -337
- package/dist/tooling/cli/development/target-lock.d.ts +0 -16
- package/dist/tooling/cli/development/target-lock.js +0 -68
- package/dist/tooling/cli/development/view.d.ts +0 -28
- package/dist/tooling/cli/development/view.js +0 -151
- package/dist/tooling/cli/environment-release.d.ts +0 -24
- package/dist/tooling/cli/environment-release.js +0 -46
- package/dist/tooling/cli/evidence.d.ts +0 -14
- package/dist/tooling/cli/evidence.js +0 -53
- package/dist/tooling/cli/installation/index.d.ts +0 -2
- package/dist/tooling/cli/installation/index.js +0 -1
- package/dist/tooling/cli/installation/observation.d.ts +0 -10
- package/dist/tooling/cli/installation/observation.js +0 -44
- package/dist/tooling/cli/installation/reconcile.d.ts +0 -32
- package/dist/tooling/cli/installation/reconcile.js +0 -122
- /package/dist/execution/service/{publication → serving}/index.js +0 -0
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
const OUTPUT_LIMIT = 256 * 1024;
|
|
3
|
-
const TERMINATION_GRACE_MS = 1_000;
|
|
4
|
-
/** Run one exact Kernel CLI executable with bounded output, time, and termination. */
|
|
5
|
-
export function runAstrale(input) {
|
|
6
|
-
if (input.signal.aborted)
|
|
7
|
-
return Promise.reject(abortError(input.signal));
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
let child;
|
|
10
|
-
try {
|
|
11
|
-
child = spawn(input.executable, [...input.args], {
|
|
12
|
-
cwd: input.projectDir,
|
|
13
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
14
|
-
shell: false,
|
|
15
|
-
// This owner already forwards cancellation and enforces a deadline. Keep
|
|
16
|
-
// terminal group signals from killing installation observation/View cleanup.
|
|
17
|
-
detached: process.platform !== 'win32',
|
|
18
|
-
});
|
|
19
|
-
child.stdin.end();
|
|
20
|
-
}
|
|
21
|
-
catch (cause) {
|
|
22
|
-
reject(new Error('Astrale CLI is unavailable.', { cause }));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
let stdout = '';
|
|
26
|
-
let stderr = '';
|
|
27
|
-
let stdoutBytes = 0;
|
|
28
|
-
let stderrBytes = 0;
|
|
29
|
-
let settled = false;
|
|
30
|
-
let termination;
|
|
31
|
-
let escalation;
|
|
32
|
-
const abort = () => terminate(input.signal.reason instanceof Error
|
|
33
|
-
? input.signal.reason
|
|
34
|
-
: new Error('Project operation interrupted.'));
|
|
35
|
-
input.signal.addEventListener('abort', abort, { once: true });
|
|
36
|
-
const cleanup = () => {
|
|
37
|
-
clearTimeout(timeout);
|
|
38
|
-
if (escalation !== undefined)
|
|
39
|
-
clearTimeout(escalation);
|
|
40
|
-
input.signal.removeEventListener('abort', abort);
|
|
41
|
-
};
|
|
42
|
-
const terminate = (cause) => {
|
|
43
|
-
if (settled || termination !== undefined)
|
|
44
|
-
return;
|
|
45
|
-
termination = cause;
|
|
46
|
-
child.stdin.destroy();
|
|
47
|
-
child.kill('SIGTERM');
|
|
48
|
-
escalation = setTimeout(() => {
|
|
49
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
50
|
-
child.kill('SIGKILL');
|
|
51
|
-
}, TERMINATION_GRACE_MS);
|
|
52
|
-
};
|
|
53
|
-
const timeout = setTimeout(() => terminate(new Error('Astrale CLI command timed out.')), input.timeoutMs);
|
|
54
|
-
child.stdout.on('data', (chunk) => {
|
|
55
|
-
stdoutBytes += chunk.byteLength;
|
|
56
|
-
if (stdoutBytes > OUTPUT_LIMIT) {
|
|
57
|
-
terminate(new Error('Astrale CLI stdout exceeded the supported limit.'));
|
|
58
|
-
}
|
|
59
|
-
else
|
|
60
|
-
stdout += chunk.toString('utf8');
|
|
61
|
-
});
|
|
62
|
-
child.stderr.on('data', (chunk) => {
|
|
63
|
-
stderrBytes += chunk.byteLength;
|
|
64
|
-
if (stderrBytes > OUTPUT_LIMIT) {
|
|
65
|
-
terminate(new Error('Astrale CLI stderr exceeded the supported limit.'));
|
|
66
|
-
}
|
|
67
|
-
else
|
|
68
|
-
stderr += chunk.toString('utf8');
|
|
69
|
-
});
|
|
70
|
-
child.once('error', (cause) => {
|
|
71
|
-
if (settled)
|
|
72
|
-
return;
|
|
73
|
-
settled = true;
|
|
74
|
-
cleanup();
|
|
75
|
-
reject(new Error('Astrale CLI could not be started.', { cause }));
|
|
76
|
-
});
|
|
77
|
-
child.once('close', (code) => {
|
|
78
|
-
if (settled)
|
|
79
|
-
return;
|
|
80
|
-
settled = true;
|
|
81
|
-
cleanup();
|
|
82
|
-
if (termination !== undefined) {
|
|
83
|
-
reject(termination);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
resolve(Object.freeze({ code: code ?? 1, stdout, stderr }));
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
function abortError(signal) {
|
|
91
|
-
return signal.reason instanceof Error
|
|
92
|
-
? signal.reason
|
|
93
|
-
: new Error('Project operation interrupted.');
|
|
94
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { AstraleProcessResult } from './process.js';
|
|
2
|
-
import { resolveAstraleExecutable } from './executable.js';
|
|
3
|
-
export declare const MINIMUM_KERNEL_CLI_VERSION = "1.0.0-beta.48";
|
|
4
|
-
export interface CompatibleAstraleDependencies {
|
|
5
|
-
readonly resolveExecutable: typeof resolveAstraleExecutable;
|
|
6
|
-
readonly run: (input: {
|
|
7
|
-
readonly executable: string;
|
|
8
|
-
readonly args: readonly string[];
|
|
9
|
-
readonly projectDir: string;
|
|
10
|
-
readonly signal: AbortSignal;
|
|
11
|
-
readonly timeoutMs: number;
|
|
12
|
-
}) => Promise<AstraleProcessResult>;
|
|
13
|
-
}
|
|
14
|
-
/** Pin and admit one compatible CLI before any configured Kernel installation effect. */
|
|
15
|
-
export declare function resolveCompatibleAstrale(input: {
|
|
16
|
-
readonly projectDir: string;
|
|
17
|
-
readonly signal: AbortSignal;
|
|
18
|
-
}, dependencies?: CompatibleAstraleDependencies): Promise<string>;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { resolveAstraleExecutable } from './executable.js';
|
|
2
|
-
import { astraleFailure } from './failure.js';
|
|
3
|
-
import { runAstrale } from './process.js';
|
|
4
|
-
export const MINIMUM_KERNEL_CLI_VERSION = '1.0.0-beta.48';
|
|
5
|
-
/** Pin and admit one compatible CLI before any configured Kernel installation effect. */
|
|
6
|
-
export async function resolveCompatibleAstrale(input, dependencies = {
|
|
7
|
-
resolveExecutable: resolveAstraleExecutable,
|
|
8
|
-
run: runAstrale,
|
|
9
|
-
}) {
|
|
10
|
-
const executable = await dependencies.resolveExecutable();
|
|
11
|
-
const result = await dependencies.run({
|
|
12
|
-
executable,
|
|
13
|
-
projectDir: input.projectDir,
|
|
14
|
-
args: ['--version'],
|
|
15
|
-
signal: input.signal,
|
|
16
|
-
timeoutMs: 10_000,
|
|
17
|
-
});
|
|
18
|
-
if (result.code !== 0)
|
|
19
|
-
throw astraleFailure('version', result);
|
|
20
|
-
const version = result.stdout.trim().replace(/^v/u, '');
|
|
21
|
-
if (!isCompatible(version)) {
|
|
22
|
-
const diagnostic = semver(version) === undefined ? '<invalid>' : version;
|
|
23
|
-
throw new TypeError(`Astrale CLI ${diagnostic} is incompatible; ${MINIMUM_KERNEL_CLI_VERSION} or newer is required.`);
|
|
24
|
-
}
|
|
25
|
-
return executable;
|
|
26
|
-
}
|
|
27
|
-
function isCompatible(input) {
|
|
28
|
-
const value = semver(input);
|
|
29
|
-
const minimum = semver(MINIMUM_KERNEL_CLI_VERSION);
|
|
30
|
-
if (value === undefined)
|
|
31
|
-
return false;
|
|
32
|
-
for (let index = 0; index < 3; index += 1) {
|
|
33
|
-
if (value.core[index] > minimum.core[index])
|
|
34
|
-
return true;
|
|
35
|
-
if (value.core[index] < minimum.core[index])
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
if (value.prerelease === undefined)
|
|
39
|
-
return true;
|
|
40
|
-
if (minimum.prerelease === undefined)
|
|
41
|
-
return false;
|
|
42
|
-
if (value.prerelease.name !== minimum.prerelease.name) {
|
|
43
|
-
return value.prerelease.name.localeCompare(minimum.prerelease.name) > 0;
|
|
44
|
-
}
|
|
45
|
-
return value.prerelease.number >= minimum.prerelease.number;
|
|
46
|
-
}
|
|
47
|
-
function semver(input) {
|
|
48
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/u.exec(input);
|
|
49
|
-
if (match === null)
|
|
50
|
-
return undefined;
|
|
51
|
-
return {
|
|
52
|
-
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
53
|
-
...(match[4] === undefined ? {} : { prerelease: { name: match[4], number: Number(match[5]) } }),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { InstallationEvidence } from '../installation/index.js';
|
|
2
|
-
import type { DevelopmentSessionReleaseEvidenceV1 } from './session-file.js';
|
|
3
|
-
/** Project installed evidence projected into the stable development-session representation. */
|
|
4
|
-
export declare function developmentReleaseEvidence(installation: InstallationEvidence): DevelopmentSessionReleaseEvidenceV1;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** Project installed evidence projected into the stable development-session representation. */
|
|
2
|
-
export function developmentReleaseEvidence(installation) {
|
|
3
|
-
return Object.freeze({
|
|
4
|
-
origin: installation.origin,
|
|
5
|
-
revision: installation.revision,
|
|
6
|
-
issuer: installation.issuer,
|
|
7
|
-
etag: installation.etag,
|
|
8
|
-
generation: installation.generation,
|
|
9
|
-
});
|
|
10
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export declare const DEVELOPMENT_SESSION_FILE_VERSION: 1;
|
|
2
|
-
export type DevelopmentSessionPhaseV1 = 'starting' | 'ready' | 'updating' | 'recovering' | 'stopping' | 'stopped' | 'failed';
|
|
3
|
-
export interface DevelopmentSessionProjectV1 {
|
|
4
|
-
readonly origin: string;
|
|
5
|
-
}
|
|
6
|
-
export interface DevelopmentSessionReleaseV1 {
|
|
7
|
-
readonly revision: string;
|
|
8
|
-
readonly issuer: string;
|
|
9
|
-
readonly etag: string;
|
|
10
|
-
readonly generation: string;
|
|
11
|
-
}
|
|
12
|
-
interface DevelopmentSessionFileBaseV1 {
|
|
13
|
-
readonly version: typeof DEVELOPMENT_SESSION_FILE_VERSION;
|
|
14
|
-
readonly sessionId: string;
|
|
15
|
-
readonly sequence: number;
|
|
16
|
-
readonly phase: DevelopmentSessionPhaseV1;
|
|
17
|
-
}
|
|
18
|
-
interface DevelopmentSessionStartingFileV1 extends DevelopmentSessionFileBaseV1 {
|
|
19
|
-
readonly phase: 'starting';
|
|
20
|
-
}
|
|
21
|
-
interface DevelopmentSessionActiveFileV1 extends DevelopmentSessionFileBaseV1 {
|
|
22
|
-
readonly phase: 'ready' | 'updating' | 'recovering';
|
|
23
|
-
readonly project: DevelopmentSessionProjectV1;
|
|
24
|
-
readonly release: DevelopmentSessionReleaseV1;
|
|
25
|
-
}
|
|
26
|
-
interface DevelopmentSessionTerminalFileBaseV1 extends DevelopmentSessionFileBaseV1 {
|
|
27
|
-
readonly phase: 'stopping' | 'stopped' | 'failed';
|
|
28
|
-
}
|
|
29
|
-
type DevelopmentSessionTerminalFileV1 = DevelopmentSessionTerminalFileBaseV1 & ({
|
|
30
|
-
readonly project: DevelopmentSessionProjectV1;
|
|
31
|
-
readonly release: DevelopmentSessionReleaseV1;
|
|
32
|
-
} | {
|
|
33
|
-
readonly project?: never;
|
|
34
|
-
readonly release?: never;
|
|
35
|
-
});
|
|
36
|
-
export type DevelopmentSessionFileV1 = DevelopmentSessionStartingFileV1 | DevelopmentSessionActiveFileV1 | DevelopmentSessionTerminalFileV1;
|
|
37
|
-
export interface DevelopmentSessionReleaseEvidenceV1 extends DevelopmentSessionProjectV1, DevelopmentSessionReleaseV1 {
|
|
38
|
-
}
|
|
39
|
-
export declare class DevelopmentSessionFileError extends Error {
|
|
40
|
-
readonly code: 'INVALID_SESSION_FILE_PATH' | 'SESSION_FILE_PATH_UNSAFE' | 'SESSION_FILE_EXISTS' | 'SESSION_FILE_TAMPERED' | 'SESSION_FILE_TRANSITION_INVALID' | 'SESSION_FILE_EVIDENCE_INCOHERENT';
|
|
41
|
-
readonly name = "DevelopmentSessionFileError";
|
|
42
|
-
constructor(code: 'INVALID_SESSION_FILE_PATH' | 'SESSION_FILE_PATH_UNSAFE' | 'SESSION_FILE_EXISTS' | 'SESSION_FILE_TAMPERED' | 'SESSION_FILE_TRANSITION_INVALID' | 'SESSION_FILE_EVIDENCE_INCOHERENT', message: string, options?: ErrorOptions);
|
|
43
|
-
}
|
|
44
|
-
export declare function acceptDevelopmentSessionFileV1(input: unknown): DevelopmentSessionFileV1;
|
|
45
|
-
/** SDK-private sole writer for one caller-selected machine-integration file. */
|
|
46
|
-
export declare class DevelopmentSessionFileWriter {
|
|
47
|
-
#private;
|
|
48
|
-
private constructor();
|
|
49
|
-
static create(path: string): Promise<DevelopmentSessionFileWriter>;
|
|
50
|
-
transition(phase: Exclude<DevelopmentSessionPhaseV1, 'starting'>, evidence?: DevelopmentSessionReleaseEvidenceV1): Promise<DevelopmentSessionFileV1>;
|
|
51
|
-
}
|
|
52
|
-
export declare function developmentSessionTransitionAllowed(previous: DevelopmentSessionPhaseV1, next: Exclude<DevelopmentSessionPhaseV1, 'starting'>): boolean;
|
|
53
|
-
export {};
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { link, lstat, open, readFile, realpath, rename, unlink } from 'node:fs/promises';
|
|
3
|
-
import { basename, dirname, isAbsolute, join, normalize, parse, sep } from 'node:path';
|
|
4
|
-
import { patterns } from '../../../platform/schema/index.js';
|
|
5
|
-
export const DEVELOPMENT_SESSION_FILE_VERSION = 1;
|
|
6
|
-
export class DevelopmentSessionFileError extends Error {
|
|
7
|
-
code;
|
|
8
|
-
name = 'DevelopmentSessionFileError';
|
|
9
|
-
constructor(code, message, options) {
|
|
10
|
-
super(message, options);
|
|
11
|
-
this.code = code;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export function acceptDevelopmentSessionFileV1(input) {
|
|
15
|
-
const value = record(input, 'Development session file');
|
|
16
|
-
if (value.metadata !== undefined ||
|
|
17
|
-
value.extensions !== undefined ||
|
|
18
|
-
value.vendor !== undefined) {
|
|
19
|
-
invalid('Development session file cannot contain a metadata, extensions, or vendor bag.');
|
|
20
|
-
}
|
|
21
|
-
if (value.version !== DEVELOPMENT_SESSION_FILE_VERSION) {
|
|
22
|
-
invalid(`Unsupported development session file version: ${String(value.version)}.`);
|
|
23
|
-
}
|
|
24
|
-
const sessionId = text(value.sessionId, 'Development session id');
|
|
25
|
-
const sequence = integer(value.sequence, 'Development session sequence');
|
|
26
|
-
const phase = admitPhase(value.phase);
|
|
27
|
-
const base = Object.freeze({ version: 1, sessionId, sequence, phase });
|
|
28
|
-
const hasProject = value.project !== undefined;
|
|
29
|
-
const hasRelease = value.release !== undefined;
|
|
30
|
-
if (phase === 'starting') {
|
|
31
|
-
if (hasProject || hasRelease) {
|
|
32
|
-
invalid('Development session starting phase cannot contain Release evidence.');
|
|
33
|
-
}
|
|
34
|
-
return Object.freeze({ ...base, phase: 'starting' });
|
|
35
|
-
}
|
|
36
|
-
if (phase === 'ready' || phase === 'updating' || phase === 'recovering') {
|
|
37
|
-
if (!hasProject || !hasRelease)
|
|
38
|
-
invalid(`Development session phase ${phase} requires a Release.`);
|
|
39
|
-
return Object.freeze({
|
|
40
|
-
...base,
|
|
41
|
-
phase,
|
|
42
|
-
project: project(value.project),
|
|
43
|
-
release: release(value.release),
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
if (hasProject !== hasRelease) {
|
|
47
|
-
invalid('Development session terminal project and Release must be present together.');
|
|
48
|
-
}
|
|
49
|
-
if (!hasProject)
|
|
50
|
-
return Object.freeze({ ...base, phase });
|
|
51
|
-
return Object.freeze({
|
|
52
|
-
...base,
|
|
53
|
-
phase,
|
|
54
|
-
project: project(value.project),
|
|
55
|
-
release: release(value.release),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
/** SDK-private sole writer for one caller-selected machine-integration file. */
|
|
59
|
-
export class DevelopmentSessionFileWriter {
|
|
60
|
-
#path;
|
|
61
|
-
#parent;
|
|
62
|
-
#sessionId;
|
|
63
|
-
#sequence = 0;
|
|
64
|
-
#phase = 'starting';
|
|
65
|
-
#last;
|
|
66
|
-
#tail = Promise.resolve();
|
|
67
|
-
constructor(path, parent, sessionId) {
|
|
68
|
-
this.#path = path;
|
|
69
|
-
this.#parent = parent;
|
|
70
|
-
this.#sessionId = sessionId;
|
|
71
|
-
}
|
|
72
|
-
static async create(path) {
|
|
73
|
-
const admitted = await admitSessionFilePath(path);
|
|
74
|
-
const writer = new DevelopmentSessionFileWriter(admitted.path, admitted.parent, randomUUID());
|
|
75
|
-
await writer.#create();
|
|
76
|
-
return writer;
|
|
77
|
-
}
|
|
78
|
-
transition(phase, evidence) {
|
|
79
|
-
const operation = this.#tail.then(async () => {
|
|
80
|
-
if (!developmentSessionTransitionAllowed(this.#phase, phase)) {
|
|
81
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_TRANSITION_INVALID', `Development session cannot transition from ${this.#phase} to ${phase}.`);
|
|
82
|
-
}
|
|
83
|
-
if (evidence !== undefined)
|
|
84
|
-
this.#last = admitEvidence(evidence);
|
|
85
|
-
if (['ready', 'updating', 'recovering'].includes(phase) && this.#last === undefined) {
|
|
86
|
-
throw new TypeError(`Development session phase ${phase} requires Release evidence.`);
|
|
87
|
-
}
|
|
88
|
-
const snapshot = this.#snapshot(phase, this.#sequence + 1);
|
|
89
|
-
await this.#replace(snapshot);
|
|
90
|
-
this.#sequence = snapshot.sequence;
|
|
91
|
-
this.#phase = snapshot.phase;
|
|
92
|
-
return snapshot;
|
|
93
|
-
});
|
|
94
|
-
this.#tail = operation.then(() => undefined, () => undefined);
|
|
95
|
-
return operation;
|
|
96
|
-
}
|
|
97
|
-
async #create() {
|
|
98
|
-
const snapshot = this.#snapshot('starting', 1);
|
|
99
|
-
const staging = await writeStaging(this.#parent, this.#path, this.#sessionId, snapshot);
|
|
100
|
-
try {
|
|
101
|
-
await link(staging, this.#path);
|
|
102
|
-
}
|
|
103
|
-
catch (cause) {
|
|
104
|
-
if (errorCode(cause) === 'EEXIST') {
|
|
105
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_EXISTS', `Development session file already exists: ${this.#path}`, { cause });
|
|
106
|
-
}
|
|
107
|
-
throw cause;
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
await unlink(staging).catch((cause) => {
|
|
111
|
-
if (errorCode(cause) !== 'ENOENT')
|
|
112
|
-
throw cause;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
await syncDirectory(this.#parent);
|
|
116
|
-
this.#sequence = snapshot.sequence;
|
|
117
|
-
}
|
|
118
|
-
#snapshot(phase, sequence) {
|
|
119
|
-
const base = {
|
|
120
|
-
version: DEVELOPMENT_SESSION_FILE_VERSION,
|
|
121
|
-
sessionId: this.#sessionId,
|
|
122
|
-
sequence,
|
|
123
|
-
};
|
|
124
|
-
if (phase === 'starting')
|
|
125
|
-
return Object.freeze({ ...base, phase: 'starting' });
|
|
126
|
-
if (this.#last === undefined) {
|
|
127
|
-
if (phase === 'ready' || phase === 'updating' || phase === 'recovering') {
|
|
128
|
-
throw new TypeError(`Development session phase ${phase} requires Release evidence.`);
|
|
129
|
-
}
|
|
130
|
-
return Object.freeze({ ...base, phase });
|
|
131
|
-
}
|
|
132
|
-
return Object.freeze({
|
|
133
|
-
...base,
|
|
134
|
-
phase,
|
|
135
|
-
project: Object.freeze({ origin: this.#last.origin }),
|
|
136
|
-
release: Object.freeze({
|
|
137
|
-
revision: this.#last.revision,
|
|
138
|
-
issuer: this.#last.issuer,
|
|
139
|
-
etag: this.#last.etag,
|
|
140
|
-
generation: this.#last.generation,
|
|
141
|
-
}),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
async #replace(snapshot) {
|
|
145
|
-
await assertParent(this.#parent);
|
|
146
|
-
const current = await readCurrent(this.#path);
|
|
147
|
-
if (current.sessionId !== this.#sessionId || current.sequence !== snapshot.sequence - 1) {
|
|
148
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_TAMPERED', `Development session file ownership changed: ${this.#path}`);
|
|
149
|
-
}
|
|
150
|
-
const staging = await writeStaging(this.#parent, this.#path, this.#sessionId, snapshot);
|
|
151
|
-
try {
|
|
152
|
-
await rename(staging, this.#path);
|
|
153
|
-
await syncDirectory(this.#parent);
|
|
154
|
-
}
|
|
155
|
-
finally {
|
|
156
|
-
await unlink(staging).catch((cause) => {
|
|
157
|
-
if (errorCode(cause) !== 'ENOENT')
|
|
158
|
-
throw cause;
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
function admitEvidence(input) {
|
|
164
|
-
const admittedProject = project({ origin: input.origin });
|
|
165
|
-
const admittedRelease = release(input);
|
|
166
|
-
return Object.freeze({ origin: admittedProject.origin, ...admittedRelease });
|
|
167
|
-
}
|
|
168
|
-
async function admitSessionFilePath(input) {
|
|
169
|
-
if (!isAbsolute(input) || normalize(input) !== input || basename(input).length === 0) {
|
|
170
|
-
throw new DevelopmentSessionFileError('INVALID_SESSION_FILE_PATH', '--session-file requires one normalized absolute file path.');
|
|
171
|
-
}
|
|
172
|
-
const parent = dirname(input);
|
|
173
|
-
await assertParent(parent);
|
|
174
|
-
try {
|
|
175
|
-
await lstat(input);
|
|
176
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_EXISTS', `Development session file path must not exist: ${input}`);
|
|
177
|
-
}
|
|
178
|
-
catch (cause) {
|
|
179
|
-
if (errorCode(cause) !== 'ENOENT')
|
|
180
|
-
throw cause;
|
|
181
|
-
}
|
|
182
|
-
return Object.freeze({ path: input, parent });
|
|
183
|
-
}
|
|
184
|
-
async function assertParent(parent) {
|
|
185
|
-
const root = parse(parent).root;
|
|
186
|
-
let cursor = root;
|
|
187
|
-
for (const segment of parent.slice(root.length).split(sep).filter(Boolean)) {
|
|
188
|
-
cursor = join(cursor, segment);
|
|
189
|
-
const entry = await lstat(cursor);
|
|
190
|
-
if (entry.isSymbolicLink())
|
|
191
|
-
unsafe(`Development session file path crosses a symlink: ${cursor}`);
|
|
192
|
-
}
|
|
193
|
-
const canonical = await realpath(parent);
|
|
194
|
-
if (canonical !== parent)
|
|
195
|
-
unsafe(`Development session file parent is not canonical: ${parent}`);
|
|
196
|
-
const entry = await lstat(parent);
|
|
197
|
-
if (!entry.isDirectory())
|
|
198
|
-
unsafe(`Development session file parent is not a directory: ${parent}`);
|
|
199
|
-
const uid = process.getuid?.();
|
|
200
|
-
if (uid !== undefined && entry.uid !== uid) {
|
|
201
|
-
unsafe(`Development session file parent is not owned by the current user: ${parent}`);
|
|
202
|
-
}
|
|
203
|
-
if ((entry.mode & 0o077) !== 0) {
|
|
204
|
-
unsafe(`Development session file parent must be owner-only: ${parent}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
async function writeStaging(parent, target, sessionId, snapshot) {
|
|
208
|
-
const path = join(parent, `.${basename(target)}.${sessionId}.${randomUUID()}.next`);
|
|
209
|
-
const file = await open(path, 'wx', 0o600);
|
|
210
|
-
try {
|
|
211
|
-
await file.writeFile(`${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
|
212
|
-
await file.sync();
|
|
213
|
-
}
|
|
214
|
-
catch (cause) {
|
|
215
|
-
await file.close();
|
|
216
|
-
await unlink(path).catch(() => undefined);
|
|
217
|
-
throw cause;
|
|
218
|
-
}
|
|
219
|
-
await file.close();
|
|
220
|
-
return path;
|
|
221
|
-
}
|
|
222
|
-
async function syncDirectory(path) {
|
|
223
|
-
const directory = await open(path, 'r');
|
|
224
|
-
try {
|
|
225
|
-
await directory.sync();
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
await directory.close();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async function readCurrent(path) {
|
|
232
|
-
const entry = await lstat(path);
|
|
233
|
-
if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o077) !== 0) {
|
|
234
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_TAMPERED', `Development session file is no longer one owner-only regular file: ${path}`);
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
return acceptDevelopmentSessionFileV1(JSON.parse(await readFile(path, 'utf8')));
|
|
238
|
-
}
|
|
239
|
-
catch (cause) {
|
|
240
|
-
if (cause instanceof DevelopmentSessionFileError)
|
|
241
|
-
throw cause;
|
|
242
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_TAMPERED', `Development session file is unreadable or invalid: ${path}`, { cause });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
function project(input) {
|
|
246
|
-
const value = record(input, 'Development session project');
|
|
247
|
-
rejectBags(value, 'Development session project');
|
|
248
|
-
const origin = text(value.origin, 'Development session project origin');
|
|
249
|
-
if (!patterns.origin.test(origin))
|
|
250
|
-
invalid('Development session project origin is invalid.');
|
|
251
|
-
return Object.freeze({ origin });
|
|
252
|
-
}
|
|
253
|
-
function release(input) {
|
|
254
|
-
const value = record(input, 'Development session Release');
|
|
255
|
-
rejectBags(value, 'Development session Release');
|
|
256
|
-
const revision = digest(value.revision, 'Development session Release revision');
|
|
257
|
-
const etag = digest(value.etag, 'Development session Release etag');
|
|
258
|
-
const generation = digest(value.generation, 'Development session Release generation');
|
|
259
|
-
const issuer = origin(value.issuer);
|
|
260
|
-
return Object.freeze({ revision, issuer, etag, generation });
|
|
261
|
-
}
|
|
262
|
-
function origin(input) {
|
|
263
|
-
const value = text(input, 'Development session Release issuer');
|
|
264
|
-
let url;
|
|
265
|
-
try {
|
|
266
|
-
url = new URL(value);
|
|
267
|
-
}
|
|
268
|
-
catch (cause) {
|
|
269
|
-
throw new TypeError('Development session Release issuer must be an HTTP(S) origin.', { cause });
|
|
270
|
-
}
|
|
271
|
-
if (!['http:', 'https:'].includes(url.protocol) || url.href !== `${url.origin}/`) {
|
|
272
|
-
invalid('Development session Release issuer must be an HTTP(S) origin.');
|
|
273
|
-
}
|
|
274
|
-
return url.origin;
|
|
275
|
-
}
|
|
276
|
-
function digest(input, label) {
|
|
277
|
-
const value = text(input, label);
|
|
278
|
-
if (!/^sha256:[0-9a-f]{64}$/u.test(value))
|
|
279
|
-
invalid(`${label} must be a sha256 digest.`);
|
|
280
|
-
return value;
|
|
281
|
-
}
|
|
282
|
-
function admitPhase(input) {
|
|
283
|
-
if (input !== 'starting' &&
|
|
284
|
-
input !== 'ready' &&
|
|
285
|
-
input !== 'updating' &&
|
|
286
|
-
input !== 'recovering' &&
|
|
287
|
-
input !== 'stopping' &&
|
|
288
|
-
input !== 'stopped' &&
|
|
289
|
-
input !== 'failed') {
|
|
290
|
-
invalid(`Unknown development session phase: ${String(input)}.`);
|
|
291
|
-
}
|
|
292
|
-
return input;
|
|
293
|
-
}
|
|
294
|
-
export function developmentSessionTransitionAllowed(previous, next) {
|
|
295
|
-
if (previous === 'stopped' || previous === 'failed')
|
|
296
|
-
return false;
|
|
297
|
-
if (previous === 'stopping')
|
|
298
|
-
return next === 'stopped' || next === 'failed';
|
|
299
|
-
if (next === 'stopping' || next === 'failed')
|
|
300
|
-
return true;
|
|
301
|
-
if (previous === 'starting')
|
|
302
|
-
return next === 'ready';
|
|
303
|
-
return next === 'ready' || next === 'updating' || next === 'recovering';
|
|
304
|
-
}
|
|
305
|
-
function rejectBags(value, label) {
|
|
306
|
-
if (value.metadata !== undefined ||
|
|
307
|
-
value.extensions !== undefined ||
|
|
308
|
-
value.vendor !== undefined) {
|
|
309
|
-
invalid(`${label} cannot contain a metadata, extensions, or vendor bag.`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function record(input, label) {
|
|
313
|
-
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
314
|
-
invalid(`${label} must be an object.`);
|
|
315
|
-
return input;
|
|
316
|
-
}
|
|
317
|
-
function text(input, label) {
|
|
318
|
-
if (typeof input !== 'string' || input.length === 0)
|
|
319
|
-
invalid(`${label} must be text.`);
|
|
320
|
-
return input;
|
|
321
|
-
}
|
|
322
|
-
function integer(input, label) {
|
|
323
|
-
if (!Number.isSafeInteger(input) || Number(input) <= 0)
|
|
324
|
-
invalid(`${label} must be a positive integer.`);
|
|
325
|
-
return Number(input);
|
|
326
|
-
}
|
|
327
|
-
function invalid(message) {
|
|
328
|
-
throw new TypeError(message);
|
|
329
|
-
}
|
|
330
|
-
function unsafe(message) {
|
|
331
|
-
throw new DevelopmentSessionFileError('SESSION_FILE_PATH_UNSAFE', message);
|
|
332
|
-
}
|
|
333
|
-
function errorCode(input) {
|
|
334
|
-
return input !== null && typeof input === 'object' && 'code' in input
|
|
335
|
-
? String(Reflect.get(input, 'code'))
|
|
336
|
-
: undefined;
|
|
337
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { DevelopmentLease } from './lease.js';
|
|
2
|
-
export declare class DevelopmentTargetLockError extends Error {
|
|
3
|
-
readonly code: 'DEVELOPMENT_TARGET_LOCKED' | 'DEVELOPMENT_TARGET_LOCK_LOST';
|
|
4
|
-
readonly name = "DevelopmentTargetLockError";
|
|
5
|
-
constructor(code: 'DEVELOPMENT_TARGET_LOCKED' | 'DEVELOPMENT_TARGET_LOCK_LOST', message: string, options?: ErrorOptions);
|
|
6
|
-
}
|
|
7
|
-
/** Exclude local sessions targeting one installed Domain across separate checkouts. */
|
|
8
|
-
export declare function acquireDevelopmentTargetLock(input: {
|
|
9
|
-
readonly issuer: string;
|
|
10
|
-
readonly origin: string;
|
|
11
|
-
}, dependencies?: {
|
|
12
|
-
readonly root?: string;
|
|
13
|
-
readonly pid?: number;
|
|
14
|
-
readonly sessionId?: string;
|
|
15
|
-
readonly processAlive?: (pid: number) => boolean;
|
|
16
|
-
}): Promise<DevelopmentLease>;
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdir, rmdir } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { acquireDevelopmentLease, DevelopmentLeaseError } from './lease.js';
|
|
6
|
-
export class DevelopmentTargetLockError extends Error {
|
|
7
|
-
code;
|
|
8
|
-
name = 'DevelopmentTargetLockError';
|
|
9
|
-
constructor(code, message, options) {
|
|
10
|
-
super(message, options);
|
|
11
|
-
this.code = code;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
/** Exclude local sessions targeting one installed Domain across separate checkouts. */
|
|
15
|
-
export async function acquireDevelopmentTargetLock(input, dependencies = {}) {
|
|
16
|
-
const coordinate = `${input.issuer}#${input.origin}`;
|
|
17
|
-
const root = dependencies.root ?? join(tmpdir(), 'astrale-domain-development-targets');
|
|
18
|
-
const key = createHash('sha256')
|
|
19
|
-
.update(input.issuer)
|
|
20
|
-
.update('\0')
|
|
21
|
-
.update(input.origin)
|
|
22
|
-
.digest('hex');
|
|
23
|
-
const directory = join(root, key);
|
|
24
|
-
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
25
|
-
let lease;
|
|
26
|
-
try {
|
|
27
|
-
lease = await acquireDevelopmentLease({
|
|
28
|
-
directory,
|
|
29
|
-
coordinate,
|
|
30
|
-
...(dependencies.pid === undefined ? {} : { pid: dependencies.pid }),
|
|
31
|
-
...(dependencies.sessionId === undefined ? {} : { sessionId: dependencies.sessionId }),
|
|
32
|
-
...(dependencies.processAlive === undefined
|
|
33
|
-
? {}
|
|
34
|
-
: { processAlive: dependencies.processAlive }),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
catch (cause) {
|
|
38
|
-
if (cause instanceof DevelopmentLeaseError) {
|
|
39
|
-
throw new DevelopmentTargetLockError('DEVELOPMENT_TARGET_LOCKED', cause.message, { cause });
|
|
40
|
-
}
|
|
41
|
-
throw cause;
|
|
42
|
-
}
|
|
43
|
-
return Object.freeze({
|
|
44
|
-
...lease,
|
|
45
|
-
async release() {
|
|
46
|
-
try {
|
|
47
|
-
await lease.release();
|
|
48
|
-
}
|
|
49
|
-
catch (cause) {
|
|
50
|
-
if (cause instanceof DevelopmentLeaseError) {
|
|
51
|
-
throw new DevelopmentTargetLockError('DEVELOPMENT_TARGET_LOCK_LOST', cause.message, {
|
|
52
|
-
cause,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
throw cause;
|
|
56
|
-
}
|
|
57
|
-
await rmdir(directory).catch(ignoreNonEmpty);
|
|
58
|
-
await rmdir(root).catch(ignoreNonEmpty);
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function ignoreNonEmpty(cause) {
|
|
63
|
-
const code = cause !== null && typeof cause === 'object' && 'code' in cause
|
|
64
|
-
? String(Reflect.get(cause, 'code'))
|
|
65
|
-
: undefined;
|
|
66
|
-
if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(code ?? ''))
|
|
67
|
-
throw cause;
|
|
68
|
-
}
|