@astrale-os/kernel-protocol 0.5.0-beta.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact/index.d.ts +1 -1
- package/dist/artifact/index.js +1 -1
- package/dist/artifact/ref.d.ts +2 -0
- package/dist/artifact/ref.js +4 -3
- package/dist/codecs/index.d.ts +1 -2
- package/dist/codecs/index.js +1 -2
- package/dist/codecs/media-types.d.ts +0 -1
- package/dist/codecs/media-types.js +0 -1
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/errors/retry.d.ts +25 -0
- package/dist/errors/retry.js +46 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/invocation/admission.d.ts +2 -2
- package/dist/invocation/admission.js +5 -5
- package/dist/invocation/binary.d.ts +1 -0
- package/dist/invocation/binary.js +21 -4
- package/dist/invocation/binding-codecs.d.ts +3 -2
- package/dist/invocation/binding-codecs.js +1 -6
- package/dist/invocation/delegate.d.ts +0 -20
- package/dist/invocation/delegate.js +2 -34
- package/dist/invocation/http/binary.d.ts +3 -1
- package/dist/invocation/http/binary.js +6 -1
- package/dist/invocation/http/index.d.ts +2 -1
- package/dist/invocation/http/index.js +2 -1
- package/dist/invocation/http/negotiation.js +3 -10
- package/dist/invocation/http/request.d.ts +11 -0
- package/dist/invocation/http/request.js +69 -0
- package/dist/invocation/index.d.ts +5 -4
- package/dist/invocation/index.js +4 -3
- package/dist/invocation/maintenance.d.ts +14 -0
- package/dist/invocation/maintenance.js +18 -0
- package/dist/invocation/redirect.d.ts +10 -12
- package/dist/invocation/redirect.js +10 -22
- package/dist/invocation/request.d.ts +3 -0
- package/dist/invocation/request.js +9 -2
- package/dist/invocation/response.d.ts +6 -1
- package/dist/invocation/response.js +26 -15
- package/dist/invocation/routing.d.ts +14 -4
- package/dist/invocation/routing.js +35 -20
- package/dist/invocation/schema.d.ts +6 -4
- package/dist/invocation/schema.js +5 -11
- package/dist/invocation/service-reason.d.ts +20 -15
- package/dist/invocation/service-reason.js +27 -19
- package/dist/invocation/stale-route.d.ts +3 -3
- package/dist/invocation/target.d.ts +5 -2
- package/dist/invocation/target.js +25 -3
- package/dist/invocation/websocket/binary.js +1 -0
- package/dist/invocation/websocket/index.d.ts +1 -1
- package/dist/invocation/websocket/index.js +1 -1
- package/dist/invocation/websocket/subprotocol.d.ts +1 -2
- package/dist/invocation/websocket/subprotocol.js +3 -4
- package/dist/issuer/claims.d.ts +7 -0
- package/dist/issuer/claims.js +7 -0
- package/dist/issuer/index.d.ts +2 -0
- package/dist/issuer/index.js +2 -0
- package/dist/issuer/paths.d.ts +2 -0
- package/dist/issuer/paths.js +1 -0
- package/dist/issuer/workflow-engine.d.ts +18 -0
- package/dist/issuer/workflow-engine.js +59 -0
- package/dist/{publication → release}/bindings/callable.js +6 -6
- package/dist/release/bindings/invocation.js +13 -0
- package/dist/release/bindings/view.d.ts +11 -0
- package/dist/release/bindings/view.js +54 -0
- package/dist/release/build.d.ts +6 -0
- package/dist/release/build.js +7 -0
- package/dist/release/bundle-admission.d.ts +21 -0
- package/dist/{publication → release}/bundle-admission.js +15 -20
- package/dist/{publication → release}/bundle-descriptor.js +6 -6
- package/dist/release/bundle-evidence.d.ts +3 -0
- package/dist/release/bundle-evidence.js +13 -0
- package/dist/{publication → release}/bundle.d.ts +5 -2
- package/dist/{publication → release}/bundle.js +13 -7
- package/dist/{publication → release}/delivery/attestation.d.ts +1 -0
- package/dist/{publication → release}/delivery/claims.d.ts +7 -2
- package/dist/{publication → release}/delivery/claims.js +20 -13
- package/dist/release/delivery/delivery.d.ts +11 -0
- package/dist/{publication → release}/delivery/delivery.js +7 -12
- package/dist/{publication → release}/delivery/error.d.ts +2 -0
- package/dist/{publication → release}/delivery/error.js +1 -0
- package/dist/{publication → release}/delivery/index.d.ts +5 -1
- package/dist/{publication → release}/delivery/index.js +4 -0
- package/dist/{publication → release}/delivery/limits.d.ts +2 -1
- package/dist/release/delivery/limits.js +5 -0
- package/dist/{publication → release}/delivery/request.d.ts +10 -9
- package/dist/{publication → release}/delivery/request.js +9 -10
- package/dist/{publication → release}/delivery-declaration.js +4 -4
- package/dist/release/delivery-endpoint.js +22 -0
- package/dist/release/digest.d.ts +13 -0
- package/dist/{publication/etag.js → release/digest.js} +1 -6
- package/dist/release/error.d.ts +7 -0
- package/dist/release/error.js +11 -0
- package/dist/release/index.d.ts +17 -0
- package/dist/release/index.js +10 -0
- package/dist/release/issuer.d.ts +5 -0
- package/dist/release/issuer.js +34 -0
- package/dist/release/manifest.d.ts +6 -0
- package/dist/release/manifest.js +22 -0
- package/dist/release/reference.d.ts +26 -0
- package/dist/release/reference.js +70 -0
- package/dist/release/release.d.ts +60 -0
- package/dist/release/release.js +125 -0
- package/dist/release/requirements.d.ts +41 -0
- package/dist/release/requirements.js +306 -0
- package/dist/{publication → release}/route.js +24 -24
- package/dist/{publication → release}/schema.js +4 -4
- package/dist/release/served.d.ts +27 -0
- package/dist/release/served.js +56 -0
- package/dist/release/shape.d.ts +8 -0
- package/dist/routes/admission.d.ts +1 -1
- package/dist/routes/admission.js +7 -85
- package/dist/routes/binding.d.ts +3 -3
- package/dist/routes/binding.js +11 -21
- package/dist/routes/compiler.d.ts +2 -2
- package/dist/routes/compiler.js +7 -3
- package/dist/routes/credential.d.ts +12 -2
- package/dist/routes/credential.js +9 -4
- package/dist/routes/decode.d.ts +5 -2
- package/dist/routes/decode.js +24 -17
- package/dist/routes/deployment-retired.d.ts +30 -0
- package/dist/routes/deployment-retired.js +96 -0
- package/dist/routes/encode.js +12 -5
- package/dist/routes/entries.d.ts +23 -0
- package/dist/routes/entries.js +150 -0
- package/dist/routes/evidence.d.ts +3 -3
- package/dist/routes/evidence.js +7 -33
- package/dist/routes/header.d.ts +9 -0
- package/dist/routes/header.js +34 -0
- package/dist/routes/index.d.ts +2 -2
- package/dist/routes/index.js +2 -2
- package/dist/routes/mapping.d.ts +1 -1
- package/dist/routes/mapping.js +4 -4
- package/dist/routes/match.d.ts +1 -1
- package/dist/routes/placement.d.ts +2 -2
- package/dist/routes/plan.d.ts +1 -1
- package/dist/routes/request.d.ts +3 -1
- package/dist/routes/reservation.d.ts +4 -2
- package/dist/routes/reservation.js +14 -8
- package/dist/routes/routing.d.ts +18 -6
- package/dist/routes/routing.js +28 -49
- package/dist/routes/select.d.ts +1 -1
- package/dist/routes/table.d.ts +3 -2
- package/dist/routes/target.d.ts +5 -0
- package/dist/routes/target.js +16 -0
- package/dist/routes/template.d.ts +0 -1
- package/dist/routes/template.js +0 -3
- package/dist/runtime/artifact.d.ts +5 -4
- package/dist/runtime/artifact.js +7 -4
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/schema/installation/codec.d.ts +9 -0
- package/dist/schema/installation/codec.js +82 -0
- package/dist/schema/installation/index.d.ts +2 -0
- package/dist/schema/installation/index.js +1 -0
- package/dist/schema/installation/introspection/codec.d.ts +5 -2
- package/dist/schema/installation/introspection/codec.js +319 -193
- package/dist/schema/installation/introspection/index.d.ts +2 -2
- package/dist/schema/installation/introspection/index.js +1 -1
- package/dist/schema/installation/introspection/result.d.ts +84 -25
- package/dist/schema/installation/result.d.ts +15 -0
- package/dist/schema/installation/result.js +1 -0
- package/package.json +21 -75
- package/dist/codecs/msgpack.d.ts +0 -4
- package/dist/codecs/msgpack.js +0 -50
- package/dist/publication/bindings/invocation.js +0 -13
- package/dist/publication/bindings/view.d.ts +0 -7
- package/dist/publication/bindings/view.js +0 -28
- package/dist/publication/bundle-admission.d.ts +0 -4
- package/dist/publication/bundle-evidence.d.ts +0 -3
- package/dist/publication/bundle-evidence.js +0 -13
- package/dist/publication/content.d.ts +0 -10
- package/dist/publication/delivery/delivery.d.ts +0 -9
- package/dist/publication/delivery/limits.js +0 -4
- package/dist/publication/delivery-endpoint.js +0 -22
- package/dist/publication/error.d.ts +0 -7
- package/dist/publication/error.js +0 -11
- package/dist/publication/etag.d.ts +0 -6
- package/dist/publication/index.d.ts +0 -17
- package/dist/publication/index.js +0 -10
- package/dist/publication/manifest.d.ts +0 -6
- package/dist/publication/manifest.js +0 -30
- package/dist/publication/publication.d.ts +0 -36
- package/dist/publication/publication.js +0 -133
- package/dist/publication/reference.d.ts +0 -17
- package/dist/publication/reference.js +0 -68
- package/dist/publication/requirements.d.ts +0 -26
- package/dist/publication/requirements.js +0 -180
- package/dist/routes/release-unavailable.d.ts +0 -8
- package/dist/routes/release-unavailable.js +0 -32
- /package/dist/{publication → release}/bindings/callable.d.ts +0 -0
- /package/dist/{publication → release}/bindings/index.d.ts +0 -0
- /package/dist/{publication → release}/bindings/index.js +0 -0
- /package/dist/{publication → release}/bindings/invocation.d.ts +0 -0
- /package/dist/{publication → release}/bundle-descriptor.d.ts +0 -0
- /package/dist/{publication → release}/declaration.d.ts +0 -0
- /package/dist/{publication → release}/declaration.js +0 -0
- /package/dist/{publication → release}/delivery/attestation.js +0 -0
- /package/dist/{publication → release}/delivery-declaration.d.ts +0 -0
- /package/dist/{publication → release}/delivery-endpoint.d.ts +0 -0
- /package/dist/{publication → release}/route.d.ts +0 -0
- /package/dist/{publication → release}/schema.d.ts +0 -0
- /package/dist/{publication/content.js → release/shape.js} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as portable from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import { admitBuild } from './build.js';
|
|
3
|
+
import { admitDigest, sha256Hex } from './digest.js';
|
|
4
|
+
import { fail } from './error.js';
|
|
5
|
+
import { servedUrl } from './issuer.js';
|
|
6
|
+
import { assertAuthoredManifest } from './manifest.js';
|
|
7
|
+
import { admitServedContent } from './served.js';
|
|
8
|
+
import { deepFreeze, exact, record } from './shape.js';
|
|
9
|
+
/** Path of the release a deployment serves beneath its serving issuer. */
|
|
10
|
+
export const PATH = '/.well-known/astrale/release.json';
|
|
11
|
+
/** Media type of `DomainRelease` v4. */
|
|
12
|
+
export const MEDIA_TYPE = 'application/vnd.astrale.domain-release+json;v=4';
|
|
13
|
+
/** Resolve the canonical release URL beneath one exact HTTP(S) serving issuer, keeping its path. */
|
|
14
|
+
export function url(issuer) {
|
|
15
|
+
return servedUrl(issuer, PATH);
|
|
16
|
+
}
|
|
17
|
+
const CONTENT_FIELDS = [
|
|
18
|
+
'format',
|
|
19
|
+
'version',
|
|
20
|
+
'origin',
|
|
21
|
+
'identity',
|
|
22
|
+
'build',
|
|
23
|
+
'schema',
|
|
24
|
+
'requirements',
|
|
25
|
+
'manifest',
|
|
26
|
+
'bindings',
|
|
27
|
+
'routes',
|
|
28
|
+
'delivery',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Release digest of canonical release content: SHA-256 of its canonical JSON. The content must be
|
|
32
|
+
* exactly what `seal` produces, a sealed release without its digest; content that `seal` would
|
|
33
|
+
* canonicalize or refuse throws `ReleaseError` instead of yielding a digest no decoder accepts.
|
|
34
|
+
* Authored content gets its digest from `seal(content).digest`.
|
|
35
|
+
*/
|
|
36
|
+
export function digestOf(input) {
|
|
37
|
+
const value = record(input, 'RELEASE_INVALID', 'Expected release content.');
|
|
38
|
+
exact(value, CONTENT_FIELDS, 'RELEASE_INVALID');
|
|
39
|
+
return hash(admitCanonical(value));
|
|
40
|
+
}
|
|
41
|
+
/** Validate release content, canonicalize its View bindings, and set its release digest. */
|
|
42
|
+
export function seal(input) {
|
|
43
|
+
const authored = record(input, 'RELEASE_INVALID', 'Expected release content.');
|
|
44
|
+
exact(authored, CONTENT_FIELDS, 'RELEASE_INVALID');
|
|
45
|
+
assertAuthoredManifest(authored.manifest);
|
|
46
|
+
const canonical = admitContent(admitContent(authored, 'canonicalize'), 'admit');
|
|
47
|
+
return release(canonical, hash(canonical));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Admit one wire release: exact fields at every level, every value in the canonical form `seal`
|
|
51
|
+
* produces, and a digest matching its content. One release therefore has exactly one wire
|
|
52
|
+
* document: `encode(accept(input))` is the canonical JSON of `input`.
|
|
53
|
+
*/
|
|
54
|
+
export function accept(input) {
|
|
55
|
+
const value = record(input, 'RELEASE_INVALID', 'Expected a DomainRelease object.');
|
|
56
|
+
exact(value, [...CONTENT_FIELDS, 'digest'], 'RELEASE_INVALID');
|
|
57
|
+
const { digest: received, ...wire } = value;
|
|
58
|
+
const content = admitCanonical(wire);
|
|
59
|
+
const digest = admitDigest(received);
|
|
60
|
+
if (digest !== hash(content)) {
|
|
61
|
+
fail('RELEASE_DIGEST_MISMATCH', 'Release digest does not match its canonical content.');
|
|
62
|
+
}
|
|
63
|
+
return release(content, digest);
|
|
64
|
+
}
|
|
65
|
+
/** Encode one release as its canonical JSON bytes. */
|
|
66
|
+
export function encode(input) {
|
|
67
|
+
return portable.json.encode(input);
|
|
68
|
+
}
|
|
69
|
+
function admitContent(value, views) {
|
|
70
|
+
if (value.format !== 'astrale.domain.release' || value.version !== 4) {
|
|
71
|
+
fail('RELEASE_INVALID', 'Unsupported release format or version.');
|
|
72
|
+
}
|
|
73
|
+
const served = admitServedContent(value, { views });
|
|
74
|
+
const build = admitBuild(value.build);
|
|
75
|
+
return deepFreeze({
|
|
76
|
+
format: 'astrale.domain.release',
|
|
77
|
+
version: 4,
|
|
78
|
+
origin: served.origin,
|
|
79
|
+
identity: served.identity,
|
|
80
|
+
build,
|
|
81
|
+
schema: served.schema,
|
|
82
|
+
requirements: served.requirements,
|
|
83
|
+
...(served.manifest === undefined ? {} : { manifest: served.manifest }),
|
|
84
|
+
bindings: served.bindings,
|
|
85
|
+
routes: served.routes,
|
|
86
|
+
...(served.delivery === undefined ? {} : { delivery: served.delivery }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Shared validators normalize legacy values; releases require their exact canonical spelling. */
|
|
90
|
+
function admitCanonical(value) {
|
|
91
|
+
const content = admitContent(value, 'admit');
|
|
92
|
+
const canonical = portable.json.serialize(content);
|
|
93
|
+
let received;
|
|
94
|
+
try {
|
|
95
|
+
received = portable.json.serialize(value);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
if (cause instanceof portable.BoundaryError) {
|
|
99
|
+
return fail('RELEASE_INVALID', 'Release content is not in canonical form.', cause);
|
|
100
|
+
}
|
|
101
|
+
throw cause;
|
|
102
|
+
}
|
|
103
|
+
if (received !== canonical)
|
|
104
|
+
fail('RELEASE_INVALID', 'Release content is not in canonical form.');
|
|
105
|
+
return content;
|
|
106
|
+
}
|
|
107
|
+
function hash(content) {
|
|
108
|
+
return `sha256:${sha256Hex(portable.json.encode(content))}`;
|
|
109
|
+
}
|
|
110
|
+
function release(content, digest) {
|
|
111
|
+
return deepFreeze({
|
|
112
|
+
format: content.format,
|
|
113
|
+
version: content.version,
|
|
114
|
+
origin: content.origin,
|
|
115
|
+
identity: content.identity,
|
|
116
|
+
digest,
|
|
117
|
+
build: content.build,
|
|
118
|
+
schema: content.schema,
|
|
119
|
+
requirements: content.requirements,
|
|
120
|
+
...(content.manifest === undefined ? {} : { manifest: content.manifest }),
|
|
121
|
+
bindings: content.bindings,
|
|
122
|
+
routes: content.routes,
|
|
123
|
+
...(content.delivery === undefined ? {} : { delivery: content.delivery }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AuthorityOperation } from '@astrale-os/kernel-core/schema/authority/operation';
|
|
2
|
+
import type { schema as schemaLanguage } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
+
import type { ClassRef, CoreRef, Key as SemanticKey } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
4
|
+
export type NonEmpty<Value> = readonly [Value, ...Value[]];
|
|
5
|
+
export declare const ClassOperationOrder: readonly ["create", "read", "traverse", "update", "delete"];
|
|
6
|
+
export type ClassOperation = (typeof ClassOperationOrder)[number];
|
|
7
|
+
export interface ClassCapabilityRequirement {
|
|
8
|
+
readonly class: ClassRef;
|
|
9
|
+
readonly operations: NonEmpty<ClassOperation>;
|
|
10
|
+
}
|
|
11
|
+
export interface CapabilityAuthorityRequirements {
|
|
12
|
+
readonly grant?: NonEmpty<AuthorityOperation>;
|
|
13
|
+
readonly revoke?: NonEmpty<AuthorityOperation>;
|
|
14
|
+
}
|
|
15
|
+
export interface CapabilityRequirements {
|
|
16
|
+
readonly functions?: NonEmpty<SemanticKey.Callable>;
|
|
17
|
+
readonly classes?: NonEmpty<ClassCapabilityRequirement>;
|
|
18
|
+
readonly authority?: CapabilityAuthorityRequirements;
|
|
19
|
+
readonly core?: NonEmpty<CoreCapabilityRequirement>;
|
|
20
|
+
}
|
|
21
|
+
/** Installation-owned authority materialized onto one local Core Identity. */
|
|
22
|
+
export interface CoreCapabilityRequirement {
|
|
23
|
+
readonly subject: CoreRef;
|
|
24
|
+
readonly functions?: NonEmpty<SemanticKey.Callable>;
|
|
25
|
+
readonly classes?: NonEmpty<ClassCapabilityRequirement>;
|
|
26
|
+
readonly authority?: CapabilityAuthorityRequirements;
|
|
27
|
+
}
|
|
28
|
+
export interface Requirements {
|
|
29
|
+
readonly capabilities: CapabilityRequirements;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Admit the exact closed canonical Release requirements object.
|
|
33
|
+
*/
|
|
34
|
+
export declare function admitRequirements(input: unknown): Requirements;
|
|
35
|
+
/** Bind explicit capability targets to exact functional dependencies in the bundle closure. */
|
|
36
|
+
export declare function validateRequirements(root: schemaLanguage.DomainSchema, closure: readonly schemaLanguage.DomainSchema[], requirements: Requirements): void;
|
|
37
|
+
/**
|
|
38
|
+
* Distinct Function, Method and Class Keys, including Core-scoped targets, in code-unit order.
|
|
39
|
+
* Core subjects and authority operations contribute no Keys. Results may span multiple origins.
|
|
40
|
+
*/
|
|
41
|
+
export declare function capabilityKeys(capabilities: CapabilityRequirements): readonly SemanticKey[];
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { AuthorityOperationOrder } from '@astrale-os/kernel-core/schema/authority/operation';
|
|
2
|
+
import { acceptDefinitionRef, callableAddress, Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
import { isMethodExecutable } from '@astrale-os/kernel-dsl/v1/language';
|
|
4
|
+
import { revision } from '@astrale-os/kernel-dsl/v1/schema/revision';
|
|
5
|
+
import { fail } from './error.js';
|
|
6
|
+
import { array, deepFreeze, exact, record, unique } from './shape.js';
|
|
7
|
+
const KernelIdentityClass = Object.freeze({
|
|
8
|
+
origin: 'kernel.astrale.ai',
|
|
9
|
+
kind: 'class',
|
|
10
|
+
name: 'Identity',
|
|
11
|
+
});
|
|
12
|
+
export const ClassOperationOrder = Object.freeze([
|
|
13
|
+
'create',
|
|
14
|
+
'read',
|
|
15
|
+
'traverse',
|
|
16
|
+
'update',
|
|
17
|
+
'delete',
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Admit the exact closed canonical Release requirements object.
|
|
21
|
+
*/
|
|
22
|
+
export function admitRequirements(input) {
|
|
23
|
+
const requirements = record(input, 'RELEASE_INVALID', 'Release requirements are invalid.');
|
|
24
|
+
exact(requirements, ['capabilities'], 'RELEASE_INVALID');
|
|
25
|
+
const capabilities = record(requirements.capabilities, 'RELEASE_INVALID', 'Release capability requirements are invalid.');
|
|
26
|
+
exact(capabilities, ['authority', 'classes', 'functions', 'core'], 'RELEASE_INVALID');
|
|
27
|
+
const functions = capabilities.functions === undefined
|
|
28
|
+
? undefined
|
|
29
|
+
: nonEmpty(capabilities.functions, 'Function capability requirements').map(admitCallable);
|
|
30
|
+
if (functions !== undefined) {
|
|
31
|
+
unique(functions, 'RELEASE_INVALID', 'Function capability requirement');
|
|
32
|
+
canonical(functions, compare, 'Function capability requirements');
|
|
33
|
+
}
|
|
34
|
+
const classes = capabilities.classes === undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: nonEmpty(capabilities.classes, 'Class capability requirements').map(admitClassRequirement);
|
|
37
|
+
if (classes !== undefined) {
|
|
38
|
+
unique(classes.map((entry) => classKey(entry.class)), 'RELEASE_INVALID', 'Class capability requirement');
|
|
39
|
+
canonical(classes, (left, right) => compare(classKey(left.class), classKey(right.class)), 'Class capability requirements');
|
|
40
|
+
}
|
|
41
|
+
const authority = capabilities.authority === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: admitAuthorityRequirements(capabilities.authority);
|
|
44
|
+
const core = capabilities.core === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: nonEmpty(capabilities.core, 'Core capability requirements').map(admitCoreCapabilityRequirement);
|
|
47
|
+
if (core !== undefined) {
|
|
48
|
+
unique(core.map(({ subject }) => Key.of(subject)), 'RELEASE_INVALID', 'Core capability subject');
|
|
49
|
+
canonical(core, (left, right) => compare(Key.of(left.subject), Key.of(right.subject)), 'Core capability requirements');
|
|
50
|
+
}
|
|
51
|
+
return deepFreeze({
|
|
52
|
+
capabilities: {
|
|
53
|
+
...(functions === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { functions: Object.freeze(functions) }),
|
|
56
|
+
...(classes === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { classes: Object.freeze(classes) }),
|
|
59
|
+
...(authority === undefined ? {} : { authority }),
|
|
60
|
+
...(core === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { core: Object.freeze(core) }),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function admitCoreCapabilityRequirement(input) {
|
|
67
|
+
const requirement = record(input, 'RELEASE_INVALID', 'Core capability requirement is invalid.');
|
|
68
|
+
exact(requirement, ['authority', 'classes', 'functions', 'subject'], 'RELEASE_INVALID');
|
|
69
|
+
const subject = admitCoreRef(requirement.subject);
|
|
70
|
+
const functions = requirement.functions === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: nonEmpty(requirement.functions, 'Core capability Function requirements').map(admitCallable);
|
|
73
|
+
if (functions !== undefined) {
|
|
74
|
+
unique(functions, 'RELEASE_INVALID', 'Core capability Function requirement');
|
|
75
|
+
canonical(functions, compare, 'Core capability Function requirements');
|
|
76
|
+
}
|
|
77
|
+
const classes = requirement.classes === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: nonEmpty(requirement.classes, 'Core capability Class requirements').map(admitClassRequirement);
|
|
80
|
+
if (classes !== undefined) {
|
|
81
|
+
unique(classes.map((entry) => classKey(entry.class)), 'RELEASE_INVALID', 'Core capability Class requirement');
|
|
82
|
+
canonical(classes, (left, right) => compare(classKey(left.class), classKey(right.class)), 'Core capability Class requirements');
|
|
83
|
+
}
|
|
84
|
+
const authority = requirement.authority === undefined
|
|
85
|
+
? undefined
|
|
86
|
+
: admitAuthorityRequirements(requirement.authority);
|
|
87
|
+
if (functions === undefined && classes === undefined && authority === undefined) {
|
|
88
|
+
fail('RELEASE_INVALID', 'Core capability requirements cannot be empty.');
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
subject,
|
|
92
|
+
...(functions === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { functions: Object.freeze(functions) }),
|
|
95
|
+
...(classes === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: { classes: Object.freeze(classes) }),
|
|
98
|
+
...(authority === undefined ? {} : { authority }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Bind explicit capability targets to exact functional dependencies in the bundle closure. */
|
|
102
|
+
export function validateRequirements(root, closure, requirements) {
|
|
103
|
+
const dependencies = exactDependencyClosure(root, closure);
|
|
104
|
+
for (const callable of requirements.capabilities.functions ?? []) {
|
|
105
|
+
const dependency = capabilityOwner(root, dependencies, callable, 'Function');
|
|
106
|
+
if (!hasCallable(dependency, callable)) {
|
|
107
|
+
fail('RELEASE_INVALID', 'Function capability requirement names an unknown dependency callable.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const requirement of requirements.capabilities.classes ?? []) {
|
|
111
|
+
const key = Key.of(requirement.class);
|
|
112
|
+
const dependency = capabilityOwner(root, dependencies, key, 'Class');
|
|
113
|
+
if (dependency.classes[requirement.class.name] === undefined) {
|
|
114
|
+
fail('RELEASE_INVALID', 'Class capability requirement names an unknown dependency Class.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const requirement of requirements.capabilities.core ?? []) {
|
|
118
|
+
const subject = root.core.nodes[requirement.subject.name];
|
|
119
|
+
if (requirement.subject.origin !== root.origin ||
|
|
120
|
+
subject === undefined ||
|
|
121
|
+
!classSatisfies(root, dependencies, subject.class, KernelIdentityClass)) {
|
|
122
|
+
fail('RELEASE_INVALID', 'Core capability subject must name one local Core Identity.');
|
|
123
|
+
}
|
|
124
|
+
for (const callable of requirement.functions ?? []) {
|
|
125
|
+
const owner = Key.origin(callable) === root.origin
|
|
126
|
+
? root
|
|
127
|
+
: capabilityOwner(root, dependencies, callable, 'Function');
|
|
128
|
+
if (!hasCallable(owner, callable)) {
|
|
129
|
+
fail('RELEASE_INVALID', 'Core capability Function requirement names an unknown or non-executable callable.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const classRequirement of requirement.classes ?? []) {
|
|
133
|
+
const key = Key.of(classRequirement.class);
|
|
134
|
+
const owner = classRequirement.class.origin === root.origin
|
|
135
|
+
? root
|
|
136
|
+
: capabilityOwner(root, dependencies, key, 'Class');
|
|
137
|
+
if (owner.classes[classRequirement.class.name] === undefined) {
|
|
138
|
+
fail('RELEASE_INVALID', 'Core capability Class requirement names an unknown dependency Class.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Distinct Function, Method and Class Keys, including Core-scoped targets, in code-unit order.
|
|
145
|
+
* Core subjects and authority operations contribute no Keys. Results may span multiple origins.
|
|
146
|
+
*/
|
|
147
|
+
export function capabilityKeys(capabilities) {
|
|
148
|
+
const keys = new Set(capabilities.functions);
|
|
149
|
+
for (const requirement of capabilities.classes ?? [])
|
|
150
|
+
keys.add(Key.of(requirement.class));
|
|
151
|
+
for (const requirement of capabilities.core ?? []) {
|
|
152
|
+
for (const callable of requirement.functions ?? [])
|
|
153
|
+
keys.add(callable);
|
|
154
|
+
for (const classRequirement of requirement.classes ?? []) {
|
|
155
|
+
keys.add(Key.of(classRequirement.class));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Object.freeze([...keys].sort(compare));
|
|
159
|
+
}
|
|
160
|
+
function classSatisfies(root, dependencies, candidate, expected) {
|
|
161
|
+
const expectedKey = Key.of(expected);
|
|
162
|
+
const visited = new Set();
|
|
163
|
+
const visit = (reference) => {
|
|
164
|
+
const key = Key.of(reference);
|
|
165
|
+
if (key === expectedKey)
|
|
166
|
+
return true;
|
|
167
|
+
if (visited.has(key))
|
|
168
|
+
return false;
|
|
169
|
+
visited.add(key);
|
|
170
|
+
const owner = reference.origin === root.origin ? root : dependencies.get(reference.origin);
|
|
171
|
+
const definition = owner?.classes[reference.name];
|
|
172
|
+
return definition?.kind === 'node' && definition.extends.some(visit);
|
|
173
|
+
};
|
|
174
|
+
return visit(candidate);
|
|
175
|
+
}
|
|
176
|
+
function exactDependencyClosure(root, closure) {
|
|
177
|
+
const supplied = new Map(closure.map((dependency) => [dependency.origin, dependency]));
|
|
178
|
+
const reachable = new Map();
|
|
179
|
+
const visit = (owner) => {
|
|
180
|
+
for (const coordinate of Object.values(owner.dependencies)) {
|
|
181
|
+
const dependency = supplied.get(coordinate.origin);
|
|
182
|
+
if (dependency === undefined || revision(dependency) !== coordinate.revision) {
|
|
183
|
+
fail('RELEASE_INVALID', 'Release bundle omits an exact dependency.');
|
|
184
|
+
}
|
|
185
|
+
if (reachable.has(dependency.origin))
|
|
186
|
+
continue;
|
|
187
|
+
reachable.set(dependency.origin, dependency);
|
|
188
|
+
visit(dependency);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
visit(root);
|
|
192
|
+
return reachable;
|
|
193
|
+
}
|
|
194
|
+
function admitCallable(input) {
|
|
195
|
+
if (typeof input !== 'string' || !Key.is(input)) {
|
|
196
|
+
return fail('RELEASE_INVALID', 'Function capability reference is invalid.');
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
callableAddress(input);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return fail('RELEASE_INVALID', 'Function capability reference must name a Function or Method.');
|
|
203
|
+
}
|
|
204
|
+
return input;
|
|
205
|
+
}
|
|
206
|
+
function admitClassRequirement(input) {
|
|
207
|
+
const requirement = record(input, 'RELEASE_INVALID', 'Class capability requirement is invalid.');
|
|
208
|
+
exact(requirement, ['class', 'operations'], 'RELEASE_INVALID');
|
|
209
|
+
const operations = nonEmpty(requirement.operations, 'Class capability operations').map((entry) => admitOperation(entry, ClassOperationOrder, 'Class capability operation'));
|
|
210
|
+
unique(operations, 'RELEASE_INVALID', 'Class capability operation');
|
|
211
|
+
canonical(operations, ordered(ClassOperationOrder), 'Class capability operations');
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
class: admitClassRef(requirement.class),
|
|
214
|
+
operations: Object.freeze(operations),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function admitAuthorityRequirements(input) {
|
|
218
|
+
const authority = record(input, 'RELEASE_INVALID', 'Authority capability requirements are invalid.');
|
|
219
|
+
exact(authority, ['grant', 'revoke'], 'RELEASE_INVALID');
|
|
220
|
+
if (authority.grant === undefined && authority.revoke === undefined) {
|
|
221
|
+
fail('RELEASE_INVALID', 'Authority capability requirements cannot be empty.');
|
|
222
|
+
}
|
|
223
|
+
const grant = admitAuthorityOperations(authority.grant, 'grant');
|
|
224
|
+
const revoke = admitAuthorityOperations(authority.revoke, 'revoke');
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
...(grant === undefined ? {} : { grant }),
|
|
227
|
+
...(revoke === undefined ? {} : { revoke }),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function admitAuthorityOperations(input, kind) {
|
|
231
|
+
if (input === undefined)
|
|
232
|
+
return undefined;
|
|
233
|
+
const values = nonEmpty(input, `Authority ${kind} operations`).map((entry) => admitOperation(entry, AuthorityOperationOrder, `Authority ${kind} operation`));
|
|
234
|
+
unique(values, 'RELEASE_INVALID', `authority ${kind} operation`);
|
|
235
|
+
canonical(values, ordered(AuthorityOperationOrder), `Authority ${kind} operations`);
|
|
236
|
+
return Object.freeze(values);
|
|
237
|
+
}
|
|
238
|
+
function admitClassRef(input) {
|
|
239
|
+
let value;
|
|
240
|
+
try {
|
|
241
|
+
value = acceptDefinitionRef(input);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
fail('RELEASE_INVALID', 'Class capability reference is invalid.');
|
|
245
|
+
}
|
|
246
|
+
if (value.kind !== 'class')
|
|
247
|
+
fail('RELEASE_INVALID', 'Class capability reference is invalid.');
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
function admitCoreRef(input) {
|
|
251
|
+
let value;
|
|
252
|
+
try {
|
|
253
|
+
value = acceptDefinitionRef(input);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
fail('RELEASE_INVALID', 'Core capability subject is invalid.');
|
|
257
|
+
}
|
|
258
|
+
if (value.kind !== 'core') {
|
|
259
|
+
fail('RELEASE_INVALID', 'Core capability subject must be one local Core reference.');
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
function nonEmpty(input, label) {
|
|
264
|
+
const values = [...array(input, 'RELEASE_INVALID')];
|
|
265
|
+
if (values.length === 0)
|
|
266
|
+
fail('RELEASE_INVALID', `${label} cannot be empty.`);
|
|
267
|
+
return values;
|
|
268
|
+
}
|
|
269
|
+
function admitOperation(input, vocabulary, label) {
|
|
270
|
+
if (typeof input !== 'string' || !vocabulary.includes(input)) {
|
|
271
|
+
return fail('RELEASE_INVALID', `${label} is unknown.`);
|
|
272
|
+
}
|
|
273
|
+
return input;
|
|
274
|
+
}
|
|
275
|
+
function canonical(values, comparator, label) {
|
|
276
|
+
if (values.some((entry, index) => index > 0 && comparator(values[index - 1], entry) >= 0)) {
|
|
277
|
+
fail('RELEASE_INVALID', `${label} must be sorted canonically.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function ordered(order) {
|
|
281
|
+
return (left, right) => order.indexOf(left) - order.indexOf(right);
|
|
282
|
+
}
|
|
283
|
+
function compare(left, right) {
|
|
284
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
285
|
+
}
|
|
286
|
+
function classKey(input) {
|
|
287
|
+
return String(Key.of(input));
|
|
288
|
+
}
|
|
289
|
+
function capabilityOwner(root, dependencies, key, label) {
|
|
290
|
+
const origin = Key.origin(key);
|
|
291
|
+
if (origin === root.origin)
|
|
292
|
+
fail('RELEASE_INVALID', `${label} capability target cannot be local.`);
|
|
293
|
+
const dependency = dependencies.get(origin);
|
|
294
|
+
if (dependency === undefined) {
|
|
295
|
+
fail('RELEASE_INVALID', `${label} capability target must belong to the exact dependency closure.`);
|
|
296
|
+
}
|
|
297
|
+
return dependency;
|
|
298
|
+
}
|
|
299
|
+
function hasCallable(dependency, callable) {
|
|
300
|
+
const coordinate = callableAddress(callable);
|
|
301
|
+
if (coordinate.kind === 'function')
|
|
302
|
+
return dependency.functions[coordinate.name] !== undefined;
|
|
303
|
+
const declaration = dependency.classes[coordinate.owner.name];
|
|
304
|
+
const method = declaration?.kind === 'node' ? declaration.methods[coordinate.name] : undefined;
|
|
305
|
+
return method !== undefined && isMethodExecutable(declaration, method);
|
|
306
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
2
|
import { admitCallableKey } from './bindings/callable.js';
|
|
3
|
-
import { absoluteHttpUrl, deepFreeze, exact, record } from './content.js';
|
|
4
3
|
import { fail } from './error.js';
|
|
4
|
+
import { absoluteHttpUrl, deepFreeze, exact, record } from './shape.js';
|
|
5
5
|
const ROUTE_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u;
|
|
6
6
|
export function admitRoute(input, origin) {
|
|
7
|
-
const value = record(input, '
|
|
7
|
+
const value = record(input, 'RELEASE_ROUTE_INVALID', 'Release HTTP route is invalid.');
|
|
8
8
|
const common = admitRouteCommon(value, origin, 'href');
|
|
9
9
|
return deepFreeze({
|
|
10
10
|
...common,
|
|
11
|
-
href: absoluteHttpUrl(value.href, false, '
|
|
11
|
+
href: absoluteHttpUrl(value.href, false, 'RELEASE_ROUTE_INVALID', true),
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
export function admitRouteDeclaration(input, origin) {
|
|
15
|
-
const value = record(input, '
|
|
15
|
+
const value = record(input, 'RELEASE_ROUTE_INVALID', 'HTTP route declaration is invalid.');
|
|
16
16
|
const common = admitRouteCommon(value, origin, 'path');
|
|
17
17
|
if (typeof value.path !== 'string' || !value.path.startsWith('/')) {
|
|
18
|
-
fail('
|
|
18
|
+
fail('RELEASE_ROUTE_INVALID', 'Route declaration path must be relative to one issuer.');
|
|
19
19
|
}
|
|
20
20
|
let parsed;
|
|
21
21
|
try {
|
|
22
22
|
parsed = new URL(value.path, 'https://placement.invalid');
|
|
23
23
|
}
|
|
24
24
|
catch (cause) {
|
|
25
|
-
return fail('
|
|
25
|
+
return fail('RELEASE_ROUTE_INVALID', 'Route declaration path is invalid.', cause);
|
|
26
26
|
}
|
|
27
27
|
if (parsed.origin !== 'https://placement.invalid' || parsed.search !== '' || parsed.hash !== '') {
|
|
28
|
-
fail('
|
|
28
|
+
fail('RELEASE_ROUTE_INVALID', 'Route declaration path cannot select an origin, query, or fragment.');
|
|
29
29
|
}
|
|
30
30
|
return deepFreeze({ ...common, path: value.path });
|
|
31
31
|
}
|
|
@@ -42,24 +42,24 @@ function admitRouteCommon(value, origin, location) {
|
|
|
42
42
|
'headers',
|
|
43
43
|
'body',
|
|
44
44
|
'credential',
|
|
45
|
-
], '
|
|
45
|
+
], 'RELEASE_ROUTE_INVALID');
|
|
46
46
|
if (typeof value.name !== 'string' || !ROUTE_NAME.test(value.name)) {
|
|
47
|
-
fail('
|
|
47
|
+
fail('RELEASE_ROUTE_INVALID', 'Route name is invalid.');
|
|
48
48
|
}
|
|
49
49
|
const callable = admitCallableKey(value.callable, origin);
|
|
50
50
|
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*'].includes(value.method)) {
|
|
51
|
-
fail('
|
|
51
|
+
fail('RELEASE_ROUTE_INVALID', 'Route method is invalid.');
|
|
52
52
|
}
|
|
53
53
|
if (value.default !== undefined && value.default !== true) {
|
|
54
|
-
fail('
|
|
54
|
+
fail('RELEASE_ROUTE_INVALID', 'Route default may only be true.');
|
|
55
55
|
}
|
|
56
56
|
if (value.requestMethod !== undefined &&
|
|
57
57
|
(typeof value.requestMethod !== 'string' || !patterns.memberName.test(value.requestMethod))) {
|
|
58
|
-
fail('
|
|
58
|
+
fail('RELEASE_ROUTE_INVALID', 'Route request-method mapping is invalid.');
|
|
59
59
|
}
|
|
60
60
|
if (value.receiver !== undefined &&
|
|
61
61
|
(typeof value.receiver !== 'string' || !patterns.memberName.test(value.receiver))) {
|
|
62
|
-
fail('
|
|
62
|
+
fail('RELEASE_ROUTE_INVALID', 'Route receiver is invalid.');
|
|
63
63
|
}
|
|
64
64
|
const query = stringRecord(value.query);
|
|
65
65
|
const headers = headerRecord(value.headers);
|
|
@@ -83,13 +83,13 @@ function admitCredential(input) {
|
|
|
83
83
|
return undefined;
|
|
84
84
|
const values = Array.isArray(input) ? input : [input];
|
|
85
85
|
if (values.length === 0)
|
|
86
|
-
fail('
|
|
86
|
+
fail('RELEASE_ROUTE_INVALID', 'Credential source list is empty.');
|
|
87
87
|
const admitted = values.map((entry) => {
|
|
88
|
-
const value = record(entry, '
|
|
89
|
-
exact(value, ['in', 'name'], '
|
|
88
|
+
const value = record(entry, 'RELEASE_ROUTE_INVALID', 'Credential source is invalid.');
|
|
89
|
+
exact(value, ['in', 'name'], 'RELEASE_ROUTE_INVALID');
|
|
90
90
|
if (value.in === 'header') {
|
|
91
91
|
if (value.name !== undefined && (typeof value.name !== 'string' || value.name.length === 0)) {
|
|
92
|
-
fail('
|
|
92
|
+
fail('RELEASE_ROUTE_INVALID', 'Credential header name is invalid.');
|
|
93
93
|
}
|
|
94
94
|
return Object.freeze({
|
|
95
95
|
in: 'header',
|
|
@@ -101,14 +101,14 @@ function admitCredential(input) {
|
|
|
101
101
|
value.name.length > 0) {
|
|
102
102
|
return Object.freeze({ in: value.in, name: value.name });
|
|
103
103
|
}
|
|
104
|
-
return fail('
|
|
104
|
+
return fail('RELEASE_ROUTE_INVALID', 'Credential source is invalid.');
|
|
105
105
|
});
|
|
106
106
|
return Array.isArray(input) ? Object.freeze(admitted) : admitted[0];
|
|
107
107
|
}
|
|
108
108
|
function admitBody(input) {
|
|
109
109
|
if (input === undefined || input === '*')
|
|
110
110
|
return input;
|
|
111
|
-
const value = record(input, '
|
|
111
|
+
const value = record(input, 'RELEASE_ROUTE_INVALID', 'Route body mapping is invalid.');
|
|
112
112
|
const output = {};
|
|
113
113
|
for (const [field, pick] of Object.entries(value)) {
|
|
114
114
|
if (typeof pick === 'string' && pick.length > 0)
|
|
@@ -121,7 +121,7 @@ function admitBody(input) {
|
|
|
121
121
|
output[field] = { as: 'text' };
|
|
122
122
|
}
|
|
123
123
|
else {
|
|
124
|
-
fail('
|
|
124
|
+
fail('RELEASE_ROUTE_INVALID', 'Route body pick is invalid.');
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
return Object.freeze(output);
|
|
@@ -129,11 +129,11 @@ function admitBody(input) {
|
|
|
129
129
|
function headerRecord(input) {
|
|
130
130
|
if (input === undefined)
|
|
131
131
|
return undefined;
|
|
132
|
-
const value = record(input, '
|
|
132
|
+
const value = record(input, 'RELEASE_ROUTE_INVALID', 'Route header mapping is invalid.');
|
|
133
133
|
const output = {};
|
|
134
134
|
for (const [field, pick] of Object.entries(value)) {
|
|
135
135
|
if (typeof pick !== 'string' || pick.length === 0) {
|
|
136
|
-
fail('
|
|
136
|
+
fail('RELEASE_ROUTE_INVALID', 'Route header pick is invalid.');
|
|
137
137
|
}
|
|
138
138
|
output[field] = pick;
|
|
139
139
|
}
|
|
@@ -142,11 +142,11 @@ function headerRecord(input) {
|
|
|
142
142
|
function stringRecord(input) {
|
|
143
143
|
if (input === undefined)
|
|
144
144
|
return undefined;
|
|
145
|
-
const value = record(input, '
|
|
145
|
+
const value = record(input, 'RELEASE_ROUTE_INVALID', 'Route mapping is invalid.');
|
|
146
146
|
const output = {};
|
|
147
147
|
for (const [field, wire] of Object.entries(value)) {
|
|
148
148
|
if (typeof wire !== 'string' || wire.length === 0) {
|
|
149
|
-
fail('
|
|
149
|
+
fail('RELEASE_ROUTE_INVALID', 'Route wire name is invalid.');
|
|
150
150
|
}
|
|
151
151
|
output[field] = wire;
|
|
152
152
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
2
|
import { admitBundleDescriptor } from './bundle-descriptor.js';
|
|
3
|
-
import { exact, record } from './content.js';
|
|
4
3
|
import { fail } from './error.js';
|
|
4
|
+
import { exact, record } from './shape.js';
|
|
5
5
|
export function admitSchema(input) {
|
|
6
|
-
const value = record(input, '
|
|
7
|
-
exact(value, ['revision', 'bundle'], '
|
|
6
|
+
const value = record(input, 'RELEASE_INVALID', 'Release schema is invalid.');
|
|
7
|
+
exact(value, ['revision', 'bundle'], 'RELEASE_INVALID');
|
|
8
8
|
if (typeof value.revision !== 'string' || !patterns.revision.test(value.revision)) {
|
|
9
|
-
fail('
|
|
9
|
+
fail('RELEASE_INVALID', 'Release schema revision is invalid.');
|
|
10
10
|
}
|
|
11
11
|
return Object.freeze({
|
|
12
12
|
revision: value.revision,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
|
+
import type { ServedRelease } from './bundle-admission.js';
|
|
3
|
+
import type { Endpoint as DeliveryEndpoint } from './delivery-endpoint.js';
|
|
4
|
+
import type { Requirements } from './requirements.js';
|
|
5
|
+
/**
|
|
6
|
+
* Everything a served release document carries besides its format, version, digest and build:
|
|
7
|
+
* the content a bundle must match, the serving identity, and the optional Delivery endpoint.
|
|
8
|
+
*/
|
|
9
|
+
export interface ServedContent extends ServedRelease {
|
|
10
|
+
readonly identity: {
|
|
11
|
+
readonly issuer: IssuerId;
|
|
12
|
+
readonly subject: string;
|
|
13
|
+
};
|
|
14
|
+
readonly delivery?: DeliveryEndpoint;
|
|
15
|
+
}
|
|
16
|
+
interface ServedOptions {
|
|
17
|
+
/** `canonicalize` normalizes authored View bindings before sealing; `admit` requires canonical ones. */
|
|
18
|
+
readonly views: 'canonicalize' | 'admit';
|
|
19
|
+
/** Reads the requirements coordinate of the document version; canonical `core` by default. */
|
|
20
|
+
readonly requirements?: (input: unknown) => Requirements;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Admit the version-neutral content of one served release document. Every document version runs
|
|
24
|
+
* these checks in this order, so a document of any version admits exactly the same served content.
|
|
25
|
+
*/
|
|
26
|
+
export declare function admitServedContent(value: Readonly<Record<string, unknown>>, options: ServedOptions): ServedContent;
|
|
27
|
+
export {};
|