@astrale-os/kernel-protocol 1.0.0-beta.1 → 1.0.0-beta.11
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/errors/retry.d.ts +3 -1
- package/dist/errors/retry.js +3 -1
- package/dist/invocation/index.d.ts +2 -1
- package/dist/invocation/index.js +1 -0
- package/dist/invocation/maintenance.d.ts +14 -0
- package/dist/invocation/maintenance.js +18 -0
- package/dist/invocation/request.d.ts +3 -0
- package/dist/invocation/request.js +7 -0
- package/dist/invocation/response.d.ts +11 -1
- package/dist/invocation/response.js +29 -15
- package/dist/schema/installation/codec.d.ts +9 -4
- package/dist/schema/installation/codec.js +38 -26
- package/dist/schema/installation/index.d.ts +2 -2
- package/dist/schema/installation/index.js +1 -1
- package/dist/schema/installation/introspection/codec.js +18 -4
- package/dist/schema/installation/introspection/result.d.ts +18 -6
- package/dist/schema/installation/result.d.ts +8 -2
- package/package.json +3 -3
package/dist/errors/retry.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const RETRY_REASONS: Readonly<{
|
|
|
9
9
|
readonly CAPACITY_EXHAUSTED: 'CAPACITY_EXHAUSTED';
|
|
10
10
|
/** The server stopped admitting work before it shuts down. */
|
|
11
11
|
readonly SERVER_DRAINING: 'SERVER_DRAINING';
|
|
12
|
+
/** The Instance is under a maintenance session that admits only its root operator. */
|
|
13
|
+
readonly INSTANCE_MAINTENANCE: 'INSTANCE_MAINTENANCE';
|
|
12
14
|
}>;
|
|
13
15
|
export type RetryReasonCode = (typeof RETRY_REASONS)[keyof typeof RETRY_REASONS];
|
|
14
16
|
export type RetryReason<Code extends RetryReasonCode = RetryReasonCode> = ErrorReason<Code, {
|
|
@@ -17,7 +19,7 @@ export type RetryReason<Code extends RetryReasonCode = RetryReasonCode> = ErrorR
|
|
|
17
19
|
/** One hour bounds a hint that only schedules the next attempt. */
|
|
18
20
|
export declare const MAXIMUM_RETRY_AFTER_MS = 3600000;
|
|
19
21
|
export declare function retryReason<Code extends RetryReasonCode>(code: Code, retryAfterMs: number): RetryReason<Code>;
|
|
20
|
-
/** Return the retry delay of a back-pressure or
|
|
22
|
+
/** Return the retry delay of a back-pressure, draining or maintenance refusal, and nothing else. */
|
|
21
23
|
export declare function retryAfterOf(error: ErrorPayload): number | undefined;
|
|
22
24
|
/** Present a retry delay as an HTTP `Retry-After` delta in whole seconds, rounded up. */
|
|
23
25
|
export declare function retryAfterField(retryAfterMs: number): string;
|
package/dist/errors/retry.js
CHANGED
|
@@ -9,6 +9,8 @@ export const RETRY_REASONS = Object.freeze({
|
|
|
9
9
|
CAPACITY_EXHAUSTED: 'CAPACITY_EXHAUSTED',
|
|
10
10
|
/** The server stopped admitting work before it shuts down. */
|
|
11
11
|
SERVER_DRAINING: 'SERVER_DRAINING',
|
|
12
|
+
/** The Instance is under a maintenance session that admits only its root operator. */
|
|
13
|
+
INSTANCE_MAINTENANCE: 'INSTANCE_MAINTENANCE',
|
|
12
14
|
});
|
|
13
15
|
/** One hour bounds a hint that only schedules the next attempt. */
|
|
14
16
|
export const MAXIMUM_RETRY_AFTER_MS = 3_600_000;
|
|
@@ -20,7 +22,7 @@ export function retryReason(code, retryAfterMs) {
|
|
|
20
22
|
throw new TypeError('Retry reason retryAfterMs is invalid.');
|
|
21
23
|
return Object.freeze({ code, details: Object.freeze({ retryAfterMs }) });
|
|
22
24
|
}
|
|
23
|
-
/** Return the retry delay of a back-pressure or
|
|
25
|
+
/** Return the retry delay of a back-pressure, draining or maintenance refusal, and nothing else. */
|
|
24
26
|
export function retryAfterOf(error) {
|
|
25
27
|
if (error.code !== CODES.BACKEND_UNAVAILABLE)
|
|
26
28
|
return undefined;
|
|
@@ -5,13 +5,14 @@ export { acceptIdempotencyKey, HTTP_IDEMPOTENCY_KEY, MAXIMUM_IDEMPOTENCY_KEY_LEN
|
|
|
5
5
|
export { acceptInvocationId, InvocationError, MAXIMUM_INVOCATION_ID_LENGTH, sameInvocationId, type InvocationId, } from './identity.js';
|
|
6
6
|
export * as http from './http/index.js';
|
|
7
7
|
export { acceptDelegate, type Delegate } from './delegate.js';
|
|
8
|
+
export { acceptMaintenance, MAXIMUM_MAINTENANCE_SESSION_LENGTH, type Maintenance, type MaintenanceSession, } from './maintenance.js';
|
|
8
9
|
export { decodeMiss, miss, type Miss } from './miss.js';
|
|
9
10
|
export { acceptRoute, acceptRedirection, decodeRedirectResponse, remoteRoute, type RemoteBindingInput, type Redirection, type RedirectResponse, type Route, } from './redirect.js';
|
|
10
11
|
export { decodeRequest, type Request } from './request.js';
|
|
11
12
|
export { admitTrace, MAXIMUM_TRACEPARENT_BYTES, MAXIMUM_TRACESTATE_BYTES, MAXIMUM_TRACESTATE_MEMBERS, type Trace, } from './trace.js';
|
|
12
13
|
export { acceptRouting, routedDigest, routedRevision, type Refresh, type Routing, type Via, } from './routing.js';
|
|
13
14
|
export { acceptSchemaTarget, type SchemaTarget } from './schema.js';
|
|
14
|
-
export { decodeResponse, type ErrorResponse, type Response, type ResponseContext, type SuccessResponse, } from './response.js';
|
|
15
|
+
export { decodeResponse, type ErrorResponse, type ExecutionResponse, type Response, type ResponseContext, type SuccessResponse, } from './response.js';
|
|
15
16
|
export { CALLABLE_FINGERPRINT_MISMATCH, DEPLOYMENT_RETIRED, SERVICE_CANDIDATE_INVALID, SERVICE_TRUST_REQUIRED, SERVICE_UNAVAILABLE, serviceErrorOf, type CallableFingerprintMismatchDetails, type CallableFingerprintMismatchReason, type DeploymentRetiredDetails, type DeploymentRetiredReason, type ServiceCandidateInvalidDetails, type ServiceCandidateInvalidReason, type ServiceErrorPayload, type ServiceReason, type ServiceTrustRequiredDetails, type ServiceTrustRequiredReason, type ServiceUnavailableDetails, type ServiceUnavailableReason, } from './service-reason.js';
|
|
16
17
|
export { decodeStale, stale, type Stale } from './stale-route.js';
|
|
17
18
|
export { decodeStreamFrame, type StreamEndFrame, type StreamErrorFrame, type StreamFrame, type StreamItemFrame, type StreamStartFrame, } from './stream.js';
|
package/dist/invocation/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { acceptIdempotencyKey, HTTP_IDEMPOTENCY_KEY, MAXIMUM_IDEMPOTENCY_KEY_LEN
|
|
|
5
5
|
export { acceptInvocationId, InvocationError, MAXIMUM_INVOCATION_ID_LENGTH, sameInvocationId, } from './identity.js';
|
|
6
6
|
export * as http from './http/index.js';
|
|
7
7
|
export { acceptDelegate } from './delegate.js';
|
|
8
|
+
export { acceptMaintenance, MAXIMUM_MAINTENANCE_SESSION_LENGTH, } from './maintenance.js';
|
|
8
9
|
export { decodeMiss, miss } from './miss.js';
|
|
9
10
|
export { acceptRoute, acceptRedirection, decodeRedirectResponse, remoteRoute, } from './redirect.js';
|
|
10
11
|
export { decodeRequest } from './request.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const MAINTENANCE_SESSION: unique symbol;
|
|
2
|
+
/** Identity of the install Execution that owns one maintenance session; it grants no authority. */
|
|
3
|
+
export type MaintenanceSession = string & {
|
|
4
|
+
readonly [MAINTENANCE_SESSION]: true;
|
|
5
|
+
};
|
|
6
|
+
/** Source-Kernel guard naming the maintenance session a root request expects to act within. */
|
|
7
|
+
export interface Maintenance {
|
|
8
|
+
readonly session: MaintenanceSession;
|
|
9
|
+
}
|
|
10
|
+
/** An Execution identifier: 1-255 ASCII characters drawn from `[A-Za-z0-9._-]`. */
|
|
11
|
+
export declare const MAXIMUM_MAINTENANCE_SESSION_LENGTH = 255;
|
|
12
|
+
/** Admit one exact maintenance guard without deciding whether its session is active. */
|
|
13
|
+
export declare function acceptMaintenance(input: unknown): Maintenance;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isNodeId } from '@astrale-os/kernel-core/graph/node';
|
|
2
|
+
import { exactFields, invalid, portableObject, required } from './admission.js';
|
|
3
|
+
/** An Execution identifier: 1-255 ASCII characters drawn from `[A-Za-z0-9._-]`. */
|
|
4
|
+
export const MAXIMUM_MAINTENANCE_SESSION_LENGTH = 255;
|
|
5
|
+
const FIELDS = new Set(['session']);
|
|
6
|
+
/** Admit one exact maintenance guard without deciding whether its session is active. */
|
|
7
|
+
export function acceptMaintenance(input) {
|
|
8
|
+
const object = portableObject(input, 'maintenance');
|
|
9
|
+
exactFields(object, FIELDS, 'maintenance');
|
|
10
|
+
required(object, ['session'], 'maintenance');
|
|
11
|
+
// An Execution identifier shares the Core NodeId grammar; it names no graph Node.
|
|
12
|
+
const session = object.session;
|
|
13
|
+
if (!isNodeId(session)) {
|
|
14
|
+
invalid('maintenance.session must contain 1-255 ASCII characters drawn from [A-Za-z0-9._-]');
|
|
15
|
+
}
|
|
16
|
+
const admitted = session;
|
|
17
|
+
return Object.freeze({ session: admitted });
|
|
18
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RequestId } from './correlation.js';
|
|
2
2
|
import type { Delegate } from './delegate.js';
|
|
3
3
|
import type { IdempotencyKey } from './idempotency.js';
|
|
4
|
+
import type { Maintenance } from './maintenance.js';
|
|
4
5
|
import type { Routing } from './routing.js';
|
|
5
6
|
import type { SchemaTarget } from './schema.js';
|
|
6
7
|
import type { Call } from './target.js';
|
|
@@ -15,6 +16,8 @@ export interface Request {
|
|
|
15
16
|
readonly routing?: Routing;
|
|
16
17
|
/** Source-Kernel precondition; forbidden on an already-routed destination request. */
|
|
17
18
|
readonly schema?: SchemaTarget;
|
|
19
|
+
/** Source-Kernel maintenance session guard; forbidden on an already-routed destination request. */
|
|
20
|
+
readonly maintenance?: Maintenance;
|
|
18
21
|
readonly idempotencyKey?: IdempotencyKey;
|
|
19
22
|
readonly trace?: Trace;
|
|
20
23
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { exactFields, invalid, portableObject, requestId, required, string } from './admission.js';
|
|
2
2
|
import { acceptDelegate } from './delegate.js';
|
|
3
3
|
import { acceptIdempotencyKey } from './idempotency.js';
|
|
4
|
+
import { acceptMaintenance } from './maintenance.js';
|
|
4
5
|
import { acceptRouting } from './routing.js';
|
|
5
6
|
import { acceptSchemaTarget } from './schema.js';
|
|
6
7
|
import { callFromSnapshot } from './target.js';
|
|
@@ -13,6 +14,7 @@ const REQUEST_FIELDS = new Set([
|
|
|
13
14
|
'delegate',
|
|
14
15
|
'routing',
|
|
15
16
|
'schema',
|
|
17
|
+
'maintenance',
|
|
16
18
|
'idempotencyKey',
|
|
17
19
|
'trace',
|
|
18
20
|
]);
|
|
@@ -26,12 +28,16 @@ export function decodeRequest(input) {
|
|
|
26
28
|
const delegate = object.delegate === undefined ? undefined : acceptDelegate(object.delegate);
|
|
27
29
|
const routing = object.routing === undefined ? undefined : acceptRouting(object.routing);
|
|
28
30
|
const schema = object.schema === undefined ? undefined : acceptSchemaTarget(object.schema);
|
|
31
|
+
const maintenance = object.maintenance === undefined ? undefined : acceptMaintenance(object.maintenance);
|
|
29
32
|
const trace = object.trace === undefined ? undefined : admitTrace(object.trace);
|
|
30
33
|
if (routing?.kind === 'via') {
|
|
31
34
|
if (delegate !== undefined)
|
|
32
35
|
invalid('destination requests with via routing forbid delegate');
|
|
33
36
|
if (schema !== undefined)
|
|
34
37
|
invalid('destination requests with via routing forbid schema target');
|
|
38
|
+
if (maintenance !== undefined) {
|
|
39
|
+
invalid('destination requests with via routing forbid maintenance');
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
else if ((credential === undefined) !== (delegate === undefined)) {
|
|
37
43
|
invalid('source requests require credential and delegate together');
|
|
@@ -44,6 +50,7 @@ export function decodeRequest(input) {
|
|
|
44
50
|
...(delegate === undefined ? {} : { delegate }),
|
|
45
51
|
...(routing === undefined ? {} : { routing }),
|
|
46
52
|
...(schema === undefined ? {} : { schema }),
|
|
53
|
+
...(maintenance === undefined ? {} : { maintenance }),
|
|
47
54
|
...(object.idempotencyKey === undefined
|
|
48
55
|
? {}
|
|
49
56
|
: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Value } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import { type NodeId } from '@astrale-os/kernel-core/graph/node';
|
|
2
3
|
import type { ErrorPayload } from '../errors/index.js';
|
|
3
4
|
import type { RequestId } from './correlation.js';
|
|
4
5
|
import type { InvocationId } from './identity.js';
|
|
@@ -15,5 +16,14 @@ export interface SuccessResponse<Result extends Value = Value> extends ResponseC
|
|
|
15
16
|
export interface ErrorResponse extends ResponseContext {
|
|
16
17
|
readonly error: ErrorPayload;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
/** A durable call's answer: the Execution it started or recovered, never a handler result. */
|
|
20
|
+
export interface ExecutionResponse extends ResponseContext {
|
|
21
|
+
readonly execution: NodeId;
|
|
22
|
+
/**
|
|
23
|
+
* False when this call created the Execution, true when its key already named it; absent when
|
|
24
|
+
* the Host does not say.
|
|
25
|
+
*/
|
|
26
|
+
readonly replayed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export type Response<Result extends Value = Value> = SuccessResponse<Result> | ErrorResponse | ExecutionResponse | RedirectResponse | Stale | Miss;
|
|
19
29
|
export declare function decodeResponse(input: unknown): Response;
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import { isNodeId } from '@astrale-os/kernel-core/graph/node';
|
|
1
2
|
import { errorPayload, exactFields, invalid, invocation, portableObject, requestId, required, } from './admission.js';
|
|
2
3
|
import { decodeMiss } from './miss.js';
|
|
3
4
|
import { decodeRedirectResponse } from './redirect.js';
|
|
4
5
|
import { decodeStale } from './stale-route.js';
|
|
5
6
|
const SUCCESS_FIELDS = new Set(['result', 'requestId', 'invocation']);
|
|
6
7
|
const ERROR_FIELDS = new Set(['error', 'requestId', 'invocation']);
|
|
8
|
+
const EXECUTION_FIELDS = new Set(['execution', 'replayed', 'requestId', 'invocation']);
|
|
7
9
|
export function decodeResponse(input) {
|
|
8
10
|
const object = portableObject(input, 'response');
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (hasResult) {
|
|
11
|
+
const outcomes = ['result', 'error', 'execution'].filter((field) => Object.hasOwn(object, field));
|
|
12
|
+
if (outcomes.length > 1)
|
|
13
|
+
invalid('response must contain exactly one of result, error or execution');
|
|
14
|
+
switch (outcomes[0]) {
|
|
15
|
+
case 'result':
|
|
15
16
|
exactFields(object, SUCCESS_FIELDS, 'response');
|
|
16
17
|
required(object, ['requestId', 'invocation'], 'response');
|
|
17
18
|
return Object.freeze({
|
|
@@ -19,14 +20,27 @@ export function decodeResponse(input) {
|
|
|
19
20
|
requestId: requestId(object.requestId, 'response.requestId'),
|
|
20
21
|
invocation: invocation(object.invocation, 'response.invocation'),
|
|
21
22
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
case 'error':
|
|
24
|
+
exactFields(object, ERROR_FIELDS, 'response');
|
|
25
|
+
required(object, ['requestId', 'error', 'invocation'], 'response');
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
error: errorPayload(object.error, 'response.error'),
|
|
28
|
+
requestId: requestId(object.requestId, 'response.requestId'),
|
|
29
|
+
invocation: invocation(object.invocation, 'response.invocation'),
|
|
30
|
+
});
|
|
31
|
+
case 'execution':
|
|
32
|
+
exactFields(object, EXECUTION_FIELDS, 'response');
|
|
33
|
+
required(object, ['requestId', 'execution', 'invocation'], 'response');
|
|
34
|
+
if (!isNodeId(object.execution))
|
|
35
|
+
invalid('response.execution must be a NodeId');
|
|
36
|
+
if (Object.hasOwn(object, 'replayed') && typeof object.replayed !== 'boolean')
|
|
37
|
+
invalid('response.replayed must be a boolean');
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
execution: object.execution,
|
|
40
|
+
...(typeof object.replayed === 'boolean' ? { replayed: object.replayed } : {}),
|
|
41
|
+
requestId: requestId(object.requestId, 'response.requestId'),
|
|
42
|
+
invocation: invocation(object.invocation, 'response.invocation'),
|
|
43
|
+
});
|
|
30
44
|
}
|
|
31
45
|
if (object.kind === 'redirect')
|
|
32
46
|
return decodeRedirectResponse(object);
|
|
@@ -34,5 +48,5 @@ export function decodeResponse(input) {
|
|
|
34
48
|
return decodeStale(object);
|
|
35
49
|
if (object.kind === 'miss')
|
|
36
50
|
return decodeMiss(object);
|
|
37
|
-
return invalid('response must contain a result, error, redirect, stale, or miss control');
|
|
51
|
+
return invalid('response must contain a result, error, execution, redirect, stale, or miss control');
|
|
38
52
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { InstallResult } from './result.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { InstalledGeneration, InstallResult } from './result.js';
|
|
2
|
+
/**
|
|
3
|
+
* Admit one install result, the value a completed install execution ends with: strict, detached
|
|
4
|
+
* and frozen. Throws a TypeError when it is not exactly a changed receipt or the unchanged
|
|
5
|
+
* generations.
|
|
6
|
+
*/
|
|
7
|
+
export declare function acceptResult(input: unknown): InstallResult;
|
|
8
|
+
/** One exact installed generation: its origin, Schema revision and Installation Target digest. */
|
|
9
|
+
export declare function acceptGeneration(input: unknown): InstalledGeneration;
|
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import { installReceipt } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export async function decode(bytes, maximumBytes) {
|
|
10
|
-
if (!(bytes instanceof Uint8Array) ||
|
|
11
|
-
!Number.isSafeInteger(maximumBytes) ||
|
|
12
|
-
maximumBytes < 1 ||
|
|
13
|
-
bytes.byteLength > maximumBytes) {
|
|
14
|
-
throw new TypeError('Schema installation evidence is invalid or exceeds its bound.');
|
|
15
|
-
}
|
|
16
|
-
let value;
|
|
17
|
-
try {
|
|
18
|
-
value = json.decode(bytes, { limits: { maxBytes: maximumBytes } });
|
|
19
|
-
}
|
|
20
|
-
catch (cause) {
|
|
21
|
-
throw new TypeError('Schema installation evidence is malformed.', { cause });
|
|
22
|
-
}
|
|
23
|
-
return accept(value);
|
|
24
|
-
}
|
|
25
|
-
function accept(input) {
|
|
2
|
+
import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
/**
|
|
4
|
+
* Admit one install result, the value a completed install execution ends with: strict, detached
|
|
5
|
+
* and frozen. Throws a TypeError when it is not exactly a changed receipt or the unchanged
|
|
6
|
+
* generations.
|
|
7
|
+
*/
|
|
8
|
+
export function acceptResult(input) {
|
|
26
9
|
const value = record(input);
|
|
27
10
|
if (value.changed === true) {
|
|
28
11
|
exact(value, ['changed', 'receipt']);
|
|
29
|
-
const
|
|
12
|
+
const parsed = installReceipt.safeParse(value.receipt);
|
|
13
|
+
if (!parsed.success)
|
|
14
|
+
throw new TypeError('Schema installation receipt is invalid.');
|
|
15
|
+
const receipt = deepFreeze(parsed.data);
|
|
30
16
|
if (receipt.transitions.some((transition) => transition.intent.operation !== receipt.operation)) {
|
|
31
17
|
throw new TypeError('Schema installation Transition belongs to another operation.');
|
|
32
18
|
}
|
|
@@ -41,7 +27,7 @@ function accept(input) {
|
|
|
41
27
|
if (!Array.isArray(value.domains) || value.domains.length === 0) {
|
|
42
28
|
throw new TypeError('Schema installation unchanged Domains are empty.');
|
|
43
29
|
}
|
|
44
|
-
const domains = value.domains.map(
|
|
30
|
+
const domains = value.domains.map(acceptGeneration);
|
|
45
31
|
uniqueOrigins(domains.map((domain) => domain.origin), 'Schema installation unchanged Domains');
|
|
46
32
|
return Object.freeze({
|
|
47
33
|
changed: false,
|
|
@@ -50,6 +36,32 @@ function accept(input) {
|
|
|
50
36
|
}
|
|
51
37
|
throw new TypeError('Schema installation result kind is invalid.');
|
|
52
38
|
}
|
|
39
|
+
/** One exact installed generation: its origin, Schema revision and Installation Target digest. */
|
|
40
|
+
export function acceptGeneration(input) {
|
|
41
|
+
const value = record(input);
|
|
42
|
+
exact(value, ['generation', 'origin', 'revision']);
|
|
43
|
+
if (typeof value.origin !== 'string' ||
|
|
44
|
+
!patterns.origin.test(value.origin) ||
|
|
45
|
+
typeof value.revision !== 'string' ||
|
|
46
|
+
!patterns.revision.test(value.revision) ||
|
|
47
|
+
typeof value.generation !== 'string' ||
|
|
48
|
+
!/^sha256:[0-9a-f]{64}$/u.test(value.generation)) {
|
|
49
|
+
throw new TypeError('Schema installation unchanged Domain generation is invalid.');
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
origin: value.origin,
|
|
53
|
+
revision: value.revision,
|
|
54
|
+
generation: value.generation,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function deepFreeze(input) {
|
|
58
|
+
if (input !== null && typeof input === 'object' && !Object.isFrozen(input)) {
|
|
59
|
+
for (const value of Object.values(input))
|
|
60
|
+
deepFreeze(value);
|
|
61
|
+
Object.freeze(input);
|
|
62
|
+
}
|
|
63
|
+
return input;
|
|
64
|
+
}
|
|
53
65
|
function uniqueOrigins(origins, label) {
|
|
54
66
|
if (new Set(origins).size !== origins.length) {
|
|
55
67
|
throw new TypeError(`${label} contain duplicate origins.`);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { acceptGeneration, acceptResult } from './codec.js';
|
|
2
2
|
export * as introspection from './introspection/index.js';
|
|
3
|
-
export type { InstallResult } from './result.js';
|
|
3
|
+
export type { InstalledGeneration, InstallResult } from './result.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { acceptGeneration, acceptResult } from './codec.js';
|
|
2
2
|
export * as introspection from './introspection/index.js';
|
|
@@ -127,7 +127,10 @@ function acceptDependencyBinding(input) {
|
|
|
127
127
|
binding: expected,
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Admit the installed listing: origin-ordered, one installation per origin, each pin of one kind,
|
|
132
|
+
* with a deployment URL exactly for a release pin.
|
|
133
|
+
*/
|
|
131
134
|
function acceptInstalledReleases(input) {
|
|
132
135
|
if (!Array.isArray(input))
|
|
133
136
|
invalid('Installed releases are invalid.');
|
|
@@ -142,7 +145,11 @@ function acceptInstalledReleases(input) {
|
|
|
142
145
|
}
|
|
143
146
|
function acceptInstalledRelease(input) {
|
|
144
147
|
const value = record(input, 'Installed release');
|
|
145
|
-
|
|
148
|
+
const pin = installedPin(value.pin);
|
|
149
|
+
// Only a release pin names a deployment: an artifact pin carries no URL.
|
|
150
|
+
exact(value, pin.kind === 'release'
|
|
151
|
+
? ['inFlight', 'issuer', 'origin', 'pin', 'revision', 'url']
|
|
152
|
+
: ['inFlight', 'issuer', 'origin', 'pin', 'revision'], 'Installed release');
|
|
146
153
|
if (typeof value.origin !== 'string' || !patterns.origin.test(value.origin)) {
|
|
147
154
|
invalid('Installed release origin is invalid.');
|
|
148
155
|
}
|
|
@@ -154,8 +161,8 @@ function acceptInstalledRelease(input) {
|
|
|
154
161
|
origin: value.origin,
|
|
155
162
|
revision: value.revision,
|
|
156
163
|
issuer,
|
|
157
|
-
url: deploymentUrl(value.url),
|
|
158
|
-
pin
|
|
164
|
+
...(pin.kind === 'release' ? { url: deploymentUrl(value.url) } : {}),
|
|
165
|
+
pin,
|
|
159
166
|
inFlight: inFlightIssuers(value.inFlight, issuer),
|
|
160
167
|
});
|
|
161
168
|
}
|
|
@@ -169,6 +176,13 @@ function installedPin(input) {
|
|
|
169
176
|
build: digest(value.build, 'Installed build'),
|
|
170
177
|
});
|
|
171
178
|
}
|
|
179
|
+
if (value.kind === 'artifact') {
|
|
180
|
+
exact(value, ['artifact', 'kind'], 'Installed artifact pin');
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
kind: 'artifact',
|
|
183
|
+
artifact: digest(value.artifact, 'Installed artifact'),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
172
186
|
return invalid('Installed pin kind is invalid.');
|
|
173
187
|
}
|
|
174
188
|
function inFlightIssuers(input, current) {
|
|
@@ -38,12 +38,18 @@ export interface DomainDependencies {
|
|
|
38
38
|
readonly domain: DomainInfo;
|
|
39
39
|
readonly dependencies: readonly DependencyBinding[];
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* What one installation pins: a DomainRelease v4 pin, its release digest beside the build digest
|
|
43
|
+
* it runs, or, for a local install, the digest of the stored artifact its source named.
|
|
44
|
+
*/
|
|
45
|
+
export type InstalledPin = {
|
|
43
46
|
readonly kind: 'release';
|
|
44
47
|
readonly release: ReleaseDigest;
|
|
45
48
|
readonly build: BuildDigest;
|
|
46
|
-
}
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: 'artifact';
|
|
51
|
+
readonly artifact: Digest;
|
|
52
|
+
};
|
|
47
53
|
/**
|
|
48
54
|
* The issuer of a release one installation replaced, as the Kernel records it: accepted while its
|
|
49
55
|
* work drains, and listed until a later consented install re-pins or prunes it, drained or not.
|
|
@@ -55,14 +61,20 @@ export interface InFlightIssuer {
|
|
|
55
61
|
/** UTC millisecond ISO 8601 instant of the activation commit that replaced it. */
|
|
56
62
|
readonly replacedAt: string;
|
|
57
63
|
}
|
|
58
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* One ready installation the Kernel pins to a deployment or to a stored artifact, as its caller can
|
|
66
|
+
* read it.
|
|
67
|
+
*/
|
|
59
68
|
export interface InstalledRelease {
|
|
60
69
|
readonly origin: Key.Origin;
|
|
61
70
|
readonly revision: Revision;
|
|
62
71
|
/** Issuer of the installation's Domain principal. */
|
|
63
72
|
readonly issuer: IssuerId;
|
|
64
|
-
/**
|
|
65
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The deployment URL, an HTTP(S) origin, the Kernel discovered the pinned document at: present
|
|
75
|
+
* exactly for a `release` pin, since an `artifact` pin names no deployment.
|
|
76
|
+
*/
|
|
77
|
+
readonly url?: string;
|
|
66
78
|
readonly pin: InstalledPin;
|
|
67
79
|
/** Issuer-ordered, distinct, never the current issuer. */
|
|
68
80
|
readonly inFlight: readonly InFlightIssuer[];
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { InstallReceipt } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Key, Revision } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
+
/** One exact installed Domain generation: what an install that changes nothing reports. */
|
|
4
|
+
export interface InstalledGeneration {
|
|
5
|
+
readonly origin: Key.Origin;
|
|
6
|
+
readonly revision: Revision;
|
|
7
|
+
readonly generation: `sha256:${string}`;
|
|
8
|
+
}
|
|
3
9
|
export type InstallResult = {
|
|
4
10
|
readonly changed: true;
|
|
5
11
|
readonly receipt: InstallReceipt;
|
|
6
12
|
} | {
|
|
7
13
|
readonly changed: false;
|
|
8
|
-
readonly domains: readonly [
|
|
14
|
+
readonly domains: readonly [InstalledGeneration, ...InstalledGeneration[]];
|
|
9
15
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-protocol",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.11",
|
|
4
4
|
"description": "Portable Astrale invocation, Release, installation, routing, and issuer protocols",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"access": "public"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@astrale-os/kernel-core": "1.0.0-beta.
|
|
93
|
-
"@astrale-os/kernel-dsl": "1.0.0-beta.
|
|
92
|
+
"@astrale-os/kernel-core": "1.0.0-beta.11",
|
|
93
|
+
"@astrale-os/kernel-dsl": "1.0.0-beta.5",
|
|
94
94
|
"@noble/hashes": "2.3.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|