@astrale-os/kernel-protocol 0.5.0-beta.10
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/LICENSE +202 -0
- package/dist/artifact/index.d.ts +1 -0
- package/dist/artifact/index.js +1 -0
- package/dist/artifact/ref.d.ts +10 -0
- package/dist/artifact/ref.js +70 -0
- package/dist/codecs/codec.d.ts +13 -0
- package/dist/codecs/codec.js +6 -0
- package/dist/codecs/index.d.ts +4 -0
- package/dist/codecs/index.js +4 -0
- package/dist/codecs/json.d.ts +2 -0
- package/dist/codecs/json.js +20 -0
- package/dist/codecs/media-types.d.ts +2 -0
- package/dist/codecs/media-types.js +2 -0
- package/dist/codecs/msgpack.d.ts +4 -0
- package/dist/codecs/msgpack.js +50 -0
- package/dist/errors/codes.d.ts +23 -0
- package/dist/errors/codes.js +22 -0
- package/dist/errors/http-status.d.ts +2 -0
- package/dist/errors/http-status.js +25 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/payload.d.ts +8 -0
- package/dist/errors/payload.js +37 -0
- package/dist/errors/protocol-error.d.ts +6 -0
- package/dist/errors/protocol-error.js +11 -0
- package/dist/errors/reason.d.ts +7 -0
- package/dist/errors/reason.js +43 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/invocation/admission.d.ts +23 -0
- package/dist/invocation/admission.js +177 -0
- package/dist/invocation/binary.d.ts +11 -0
- package/dist/invocation/binary.js +24 -0
- package/dist/invocation/binding-codecs.d.ts +8 -0
- package/dist/invocation/binding-codecs.js +27 -0
- package/dist/invocation/cancellation.d.ts +8 -0
- package/dist/invocation/cancellation.js +16 -0
- package/dist/invocation/correlation.d.ts +2 -0
- package/dist/invocation/correlation.js +7 -0
- package/dist/invocation/delegate.d.ts +24 -0
- package/dist/invocation/delegate.js +43 -0
- package/dist/invocation/http/binary.d.ts +12 -0
- package/dist/invocation/http/binary.js +132 -0
- package/dist/invocation/http/index.d.ts +4 -0
- package/dist/invocation/http/index.js +4 -0
- package/dist/invocation/http/negotiation.d.ts +3 -0
- package/dist/invocation/http/negotiation.js +207 -0
- package/dist/invocation/http/stream.d.ts +5 -0
- package/dist/invocation/http/stream.js +15 -0
- package/dist/invocation/http/trace.d.ts +6 -0
- package/dist/invocation/http/trace.js +24 -0
- package/dist/invocation/idempotency.d.ts +10 -0
- package/dist/invocation/idempotency.js +13 -0
- package/dist/invocation/identity.d.ts +16 -0
- package/dist/invocation/identity.js +55 -0
- package/dist/invocation/index.d.ts +20 -0
- package/dist/invocation/index.js +20 -0
- package/dist/invocation/miss.d.ts +9 -0
- package/dist/invocation/miss.js +17 -0
- package/dist/invocation/redirect.d.ts +40 -0
- package/dist/invocation/redirect.js +81 -0
- package/dist/invocation/request.d.ts +21 -0
- package/dist/invocation/request.js +54 -0
- package/dist/invocation/response.d.ts +19 -0
- package/dist/invocation/response.js +38 -0
- package/dist/invocation/routing.d.ts +14 -0
- package/dist/invocation/routing.js +34 -0
- package/dist/invocation/schema.d.ts +9 -0
- package/dist/invocation/schema.js +19 -0
- package/dist/invocation/service-reason.d.ts +48 -0
- package/dist/invocation/service-reason.js +85 -0
- package/dist/invocation/stale-route.d.ts +12 -0
- package/dist/invocation/stale-route.js +19 -0
- package/dist/invocation/stream.d.ts +27 -0
- package/dist/invocation/stream.js +44 -0
- package/dist/invocation/target.d.ts +3 -0
- package/dist/invocation/target.js +11 -0
- package/dist/invocation/trace.d.ts +9 -0
- package/dist/invocation/trace.js +86 -0
- package/dist/invocation/version.d.ts +2 -0
- package/dist/invocation/version.js +1 -0
- package/dist/invocation/websocket/binary.d.ts +3 -0
- package/dist/invocation/websocket/binary.js +71 -0
- package/dist/invocation/websocket/index.d.ts +2 -0
- package/dist/invocation/websocket/index.js +2 -0
- package/dist/invocation/websocket/subprotocol.d.ts +10 -0
- package/dist/invocation/websocket/subprotocol.js +29 -0
- package/dist/issuer/configuration.d.ts +14 -0
- package/dist/issuer/configuration.js +86 -0
- package/dist/issuer/error.d.ts +7 -0
- package/dist/issuer/error.js +11 -0
- package/dist/issuer/exchange.d.ts +25 -0
- package/dist/issuer/exchange.js +129 -0
- package/dist/issuer/hosted.d.ts +2 -0
- package/dist/issuer/hosted.js +7 -0
- package/dist/issuer/index.d.ts +6 -0
- package/dist/issuer/index.js +6 -0
- package/dist/issuer/key-set.d.ts +6 -0
- package/dist/issuer/key-set.js +48 -0
- package/dist/issuer/paths.d.ts +6 -0
- package/dist/issuer/paths.js +10 -0
- package/dist/issuer/url.d.ts +2 -0
- package/dist/issuer/url.js +34 -0
- package/dist/publication/bindings/callable.d.ts +8 -0
- package/dist/publication/bindings/callable.js +34 -0
- package/dist/publication/bindings/index.d.ts +3 -0
- package/dist/publication/bindings/index.js +3 -0
- package/dist/publication/bindings/invocation.d.ts +5 -0
- package/dist/publication/bindings/invocation.js +13 -0
- package/dist/publication/bindings/view.d.ts +7 -0
- package/dist/publication/bindings/view.js +28 -0
- package/dist/publication/bundle-admission.d.ts +4 -0
- package/dist/publication/bundle-admission.js +65 -0
- package/dist/publication/bundle-descriptor.d.ts +9 -0
- package/dist/publication/bundle-descriptor.js +19 -0
- package/dist/publication/bundle-evidence.d.ts +3 -0
- package/dist/publication/bundle-evidence.js +13 -0
- package/dist/publication/bundle.d.ts +9 -0
- package/dist/publication/bundle.js +44 -0
- package/dist/publication/content.d.ts +10 -0
- package/dist/publication/content.js +55 -0
- package/dist/publication/declaration.d.ts +11 -0
- package/dist/publication/declaration.js +1 -0
- package/dist/publication/delivery/attestation.d.ts +6 -0
- package/dist/publication/delivery/attestation.js +10 -0
- package/dist/publication/delivery/claims.d.ts +16 -0
- package/dist/publication/delivery/claims.js +61 -0
- package/dist/publication/delivery/delivery.d.ts +9 -0
- package/dist/publication/delivery/delivery.js +29 -0
- package/dist/publication/delivery/error.d.ts +7 -0
- package/dist/publication/delivery/error.js +11 -0
- package/dist/publication/delivery/index.d.ts +6 -0
- package/dist/publication/delivery/index.js +6 -0
- package/dist/publication/delivery/limits.d.ts +4 -0
- package/dist/publication/delivery/limits.js +4 -0
- package/dist/publication/delivery/request.d.ts +15 -0
- package/dist/publication/delivery/request.js +52 -0
- package/dist/publication/delivery-declaration.d.ts +4 -0
- package/dist/publication/delivery-declaration.js +25 -0
- package/dist/publication/delivery-endpoint.d.ts +11 -0
- package/dist/publication/delivery-endpoint.js +22 -0
- package/dist/publication/error.d.ts +7 -0
- package/dist/publication/error.js +11 -0
- package/dist/publication/etag.d.ts +6 -0
- package/dist/publication/etag.js +79 -0
- package/dist/publication/index.d.ts +17 -0
- package/dist/publication/index.js +10 -0
- package/dist/publication/manifest.d.ts +6 -0
- package/dist/publication/manifest.js +30 -0
- package/dist/publication/publication.d.ts +36 -0
- package/dist/publication/publication.js +133 -0
- package/dist/publication/reference.d.ts +17 -0
- package/dist/publication/reference.js +68 -0
- package/dist/publication/requirements.d.ts +26 -0
- package/dist/publication/requirements.js +180 -0
- package/dist/publication/route.d.ts +37 -0
- package/dist/publication/route.js +154 -0
- package/dist/publication/schema.d.ts +7 -0
- package/dist/publication/schema.js +15 -0
- package/dist/routes/admission.d.ts +3 -0
- package/dist/routes/admission.js +89 -0
- package/dist/routes/binding.d.ts +6 -0
- package/dist/routes/binding.js +27 -0
- package/dist/routes/collision.d.ts +2 -0
- package/dist/routes/collision.js +16 -0
- package/dist/routes/compiler.d.ts +8 -0
- package/dist/routes/compiler.js +50 -0
- package/dist/routes/credential.d.ts +8 -0
- package/dist/routes/credential.js +118 -0
- package/dist/routes/decode.d.ts +5 -0
- package/dist/routes/decode.js +147 -0
- package/dist/routes/encode.d.ts +4 -0
- package/dist/routes/encode.js +145 -0
- package/dist/routes/error-response.d.ts +5 -0
- package/dist/routes/error-response.js +31 -0
- package/dist/routes/error.d.ts +8 -0
- package/dist/routes/error.js +14 -0
- package/dist/routes/evidence.d.ts +5 -0
- package/dist/routes/evidence.js +38 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.js +16 -0
- package/dist/routes/mapping.d.ts +5 -0
- package/dist/routes/mapping.js +126 -0
- package/dist/routes/match.d.ts +4 -0
- package/dist/routes/match.js +7 -0
- package/dist/routes/placement.d.ts +5 -0
- package/dist/routes/placement.js +18 -0
- package/dist/routes/plan.d.ts +24 -0
- package/dist/routes/plan.js +1 -0
- package/dist/routes/release-unavailable.d.ts +8 -0
- package/dist/routes/release-unavailable.js +32 -0
- package/dist/routes/request.d.ts +26 -0
- package/dist/routes/request.js +53 -0
- package/dist/routes/reservation.d.ts +3 -0
- package/dist/routes/reservation.js +37 -0
- package/dist/routes/routing.d.ts +22 -0
- package/dist/routes/routing.js +87 -0
- package/dist/routes/scalar.d.ts +7 -0
- package/dist/routes/scalar.js +61 -0
- package/dist/routes/select.d.ts +3 -0
- package/dist/routes/select.js +16 -0
- package/dist/routes/stream.d.ts +14 -0
- package/dist/routes/stream.js +50 -0
- package/dist/routes/table.d.ts +9 -0
- package/dist/routes/table.js +1 -0
- package/dist/routes/target.d.ts +13 -0
- package/dist/routes/target.js +3 -0
- package/dist/routes/template.d.ts +8 -0
- package/dist/routes/template.js +227 -0
- package/dist/runtime/artifact.d.ts +47 -0
- package/dist/runtime/artifact.js +176 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/installation/index.d.ts +1 -0
- package/dist/schema/installation/index.js +1 -0
- package/dist/schema/installation/introspection/codec.d.ts +5 -0
- package/dist/schema/installation/introspection/codec.js +272 -0
- package/dist/schema/installation/introspection/index.d.ts +2 -0
- package/dist/schema/installation/introspection/index.js +1 -0
- package/dist/schema/installation/introspection/result.d.ts +33 -0
- package/dist/schema/installation/introspection/result.js +1 -0
- package/package.json +163 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
|
+
import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
import type { CallableBinding, ViewBinding } from './bindings/index.js';
|
|
4
|
+
import type { Content } from './content.js';
|
|
5
|
+
import type { Endpoint as DeliveryEndpoint } from './delivery-endpoint.js';
|
|
6
|
+
import type { Etag } from './etag.js';
|
|
7
|
+
import type { Manifest } from './manifest.js';
|
|
8
|
+
import type { PublicationRequirements } from './requirements.js';
|
|
9
|
+
import type { HttpRoute } from './route.js';
|
|
10
|
+
import type { Schema } from './schema.js';
|
|
11
|
+
export declare const PATH: '/.well-known/astrale/domain.json';
|
|
12
|
+
export declare const MEDIA_TYPE: 'application/vnd.astrale.domain-publication+json;v=2';
|
|
13
|
+
/** Resolve the canonical Publication URL beneath one exact HTTP(S) serving issuer. */
|
|
14
|
+
export declare function url(input: string): string;
|
|
15
|
+
export interface Publication {
|
|
16
|
+
readonly format: 'astrale.domain.publication';
|
|
17
|
+
readonly version: 2;
|
|
18
|
+
readonly origin: Key.Origin;
|
|
19
|
+
readonly identity: {
|
|
20
|
+
readonly issuer: IssuerId;
|
|
21
|
+
readonly subject: string;
|
|
22
|
+
};
|
|
23
|
+
readonly etag: Etag;
|
|
24
|
+
readonly schema: Schema;
|
|
25
|
+
readonly requirements: PublicationRequirements;
|
|
26
|
+
readonly manifest?: Manifest;
|
|
27
|
+
readonly bindings: {
|
|
28
|
+
readonly callables: readonly CallableBinding[];
|
|
29
|
+
readonly views: readonly ViewBinding[];
|
|
30
|
+
};
|
|
31
|
+
readonly routes: readonly HttpRoute[];
|
|
32
|
+
readonly delivery?: DeliveryEndpoint;
|
|
33
|
+
}
|
|
34
|
+
export declare function seal(input: Content): Publication;
|
|
35
|
+
export declare function accept(input: unknown): Publication;
|
|
36
|
+
export declare function encode(input: Publication): Uint8Array;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { AuthValueError } from '@astrale-os/kernel-core/auth/error';
|
|
2
|
+
import { accept as acceptIssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
3
|
+
import { Key, patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
4
|
+
import * as portable from '@astrale-os/kernel-dsl/value';
|
|
5
|
+
import { admitCallableBinding, admitViewBinding } from './bindings/index.js';
|
|
6
|
+
import { array, deepFreeze, exact, record, unique } from './content.js';
|
|
7
|
+
import { admitEndpoint as admitDeliveryEndpoint } from './delivery-endpoint.js';
|
|
8
|
+
import { fail } from './error.js';
|
|
9
|
+
import { admitDigest, computeEtag } from './etag.js';
|
|
10
|
+
import { admitManifest } from './manifest.js';
|
|
11
|
+
import { admitRequirements } from './requirements.js';
|
|
12
|
+
import { admitRoute } from './route.js';
|
|
13
|
+
import { admitSchema } from './schema.js';
|
|
14
|
+
export const PATH = '/.well-known/astrale/domain.json';
|
|
15
|
+
export const MEDIA_TYPE = 'application/vnd.astrale.domain-publication+json;v=2';
|
|
16
|
+
/** Resolve the canonical Publication URL beneath one exact HTTP(S) serving issuer. */
|
|
17
|
+
export function url(input) {
|
|
18
|
+
const admitted = admitIssuer(input);
|
|
19
|
+
let value;
|
|
20
|
+
try {
|
|
21
|
+
value = new URL(admitted);
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
return fail('PUBLICATION_INVALID', 'Publication issuer is not an HTTP(S) URL.', cause);
|
|
25
|
+
}
|
|
26
|
+
if ((value.protocol !== 'https:' && value.protocol !== 'http:') ||
|
|
27
|
+
value.username !== '' ||
|
|
28
|
+
value.password !== '' ||
|
|
29
|
+
value.search !== '' ||
|
|
30
|
+
value.hash !== '') {
|
|
31
|
+
fail('PUBLICATION_INVALID', 'Publication issuer is not an HTTP(S) URL.');
|
|
32
|
+
}
|
|
33
|
+
return `${admitted.replace(/\/+$/u, '')}${PATH}`;
|
|
34
|
+
}
|
|
35
|
+
export function seal(input) {
|
|
36
|
+
return acceptPublication({ ...input, etag: computeEtag(input) }, false);
|
|
37
|
+
}
|
|
38
|
+
export function accept(input) {
|
|
39
|
+
const publication = acceptPublication(input, true);
|
|
40
|
+
if (publication.etag !== computeEtag(withoutEtag(publication))) {
|
|
41
|
+
fail('PUBLICATION_ETAG_MISMATCH', 'Publication ETag does not match its canonical content.');
|
|
42
|
+
}
|
|
43
|
+
return publication;
|
|
44
|
+
}
|
|
45
|
+
export function encode(input) {
|
|
46
|
+
return portable.json.encode(input);
|
|
47
|
+
}
|
|
48
|
+
function acceptPublication(input, requireEtag) {
|
|
49
|
+
const value = record(input, 'PUBLICATION_INVALID', 'Expected a Publication object.');
|
|
50
|
+
exact(value, [
|
|
51
|
+
'format',
|
|
52
|
+
'version',
|
|
53
|
+
'origin',
|
|
54
|
+
'identity',
|
|
55
|
+
'etag',
|
|
56
|
+
'schema',
|
|
57
|
+
'requirements',
|
|
58
|
+
'manifest',
|
|
59
|
+
'bindings',
|
|
60
|
+
'routes',
|
|
61
|
+
'delivery',
|
|
62
|
+
], 'PUBLICATION_INVALID');
|
|
63
|
+
if (value.format !== 'astrale.domain.publication' || value.version !== 2) {
|
|
64
|
+
fail('PUBLICATION_INVALID', 'Unsupported Publication format or version.');
|
|
65
|
+
}
|
|
66
|
+
if (typeof value.origin !== 'string' || !patterns.origin.test(value.origin)) {
|
|
67
|
+
fail('PUBLICATION_INVALID', 'Publication origin is invalid.');
|
|
68
|
+
}
|
|
69
|
+
const origin = value.origin;
|
|
70
|
+
const identity = record(value.identity, 'PUBLICATION_INVALID', 'Publication identity is invalid.');
|
|
71
|
+
exact(identity, ['issuer', 'subject'], 'PUBLICATION_INVALID');
|
|
72
|
+
const issuer = admitIssuer(identity.issuer);
|
|
73
|
+
if (identity.subject !== origin) {
|
|
74
|
+
fail('PUBLICATION_INVALID', 'Publication identity subject must equal its root origin.');
|
|
75
|
+
}
|
|
76
|
+
const publishedSchema = admitSchema(value.schema);
|
|
77
|
+
const requirements = admitRequirements(value.requirements);
|
|
78
|
+
const manifest = admitManifest(value.manifest);
|
|
79
|
+
const bindingSet = record(value.bindings, 'PUBLICATION_BINDING_INVALID', 'Publication bindings are invalid.');
|
|
80
|
+
exact(bindingSet, ['callables', 'views'], 'PUBLICATION_BINDING_INVALID');
|
|
81
|
+
const callables = array(bindingSet.callables, 'PUBLICATION_BINDING_INVALID').map((entry) => admitCallableBinding(entry, origin));
|
|
82
|
+
unique(callables.map(({ callable }) => callable), 'PUBLICATION_BINDING_INVALID', 'callable binding');
|
|
83
|
+
const views = array(bindingSet.views, 'PUBLICATION_VIEW_INVALID').map((entry) => admitViewBinding(entry, origin));
|
|
84
|
+
unique(views.map(({ view }) => view), 'PUBLICATION_VIEW_INVALID', 'View binding');
|
|
85
|
+
const routes = array(value.routes, 'PUBLICATION_ROUTE_INVALID').map((entry) => admitRoute(entry, origin));
|
|
86
|
+
unique(routes.map(({ name }) => name), 'PUBLICATION_ROUTE_INVALID', 'route name');
|
|
87
|
+
for (const callable of new Set(routes.map(({ callable }) => callable))) {
|
|
88
|
+
if (routes.filter((route) => route.callable === callable && route.default === true).length > 1) {
|
|
89
|
+
fail('PUBLICATION_ROUTE_INVALID', `Callable ${callable} has more than one default route.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (manifest?.entrypoint !== undefined) {
|
|
93
|
+
if (manifest.entrypoint.origin !== origin) {
|
|
94
|
+
fail('PUBLICATION_VIEW_INVALID', 'Manifest entrypoint must belong to the root origin.');
|
|
95
|
+
}
|
|
96
|
+
const key = Key.of(manifest.entrypoint);
|
|
97
|
+
if (!views.some(({ view }) => view === key)) {
|
|
98
|
+
fail('PUBLICATION_VIEW_INVALID', 'Manifest entrypoint must have a published View binding.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const delivery = admitDeliveryEndpoint(value.delivery, publishedSchema.bundle.href);
|
|
102
|
+
const admittedEtag = requireEtag ? admitDigest(value.etag) : value.etag;
|
|
103
|
+
if (typeof admittedEtag !== 'string')
|
|
104
|
+
fail('PUBLICATION_INVALID', 'Publication ETag is missing.');
|
|
105
|
+
return deepFreeze({
|
|
106
|
+
format: 'astrale.domain.publication',
|
|
107
|
+
version: 2,
|
|
108
|
+
origin,
|
|
109
|
+
identity: { issuer, subject: origin },
|
|
110
|
+
etag: admittedEtag,
|
|
111
|
+
schema: publishedSchema,
|
|
112
|
+
requirements,
|
|
113
|
+
...(manifest === undefined ? {} : { manifest }),
|
|
114
|
+
bindings: { callables, views },
|
|
115
|
+
routes,
|
|
116
|
+
...(delivery === undefined ? {} : { delivery }),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function admitIssuer(input) {
|
|
120
|
+
try {
|
|
121
|
+
return acceptIssuerId(String(input));
|
|
122
|
+
}
|
|
123
|
+
catch (cause) {
|
|
124
|
+
if (cause instanceof AuthValueError) {
|
|
125
|
+
return fail('PUBLICATION_INVALID', 'Publication issuer is invalid.', cause);
|
|
126
|
+
}
|
|
127
|
+
throw cause;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function withoutEtag(input) {
|
|
131
|
+
const { etag: _etag, ...content } = input;
|
|
132
|
+
return content;
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Revision } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { type Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
import type { Etag } from './etag.js';
|
|
4
|
+
import type { Publication } from './publication.js';
|
|
5
|
+
/** Compact untrusted projection that witnesses one exact Publication. */
|
|
6
|
+
export interface PublicationRef {
|
|
7
|
+
readonly origin: Key.Origin;
|
|
8
|
+
readonly identity: Publication['identity'];
|
|
9
|
+
readonly revision: Revision;
|
|
10
|
+
readonly etag: Etag;
|
|
11
|
+
}
|
|
12
|
+
export type PublicationReferenceRelation = 'exact' | 'same-schema' | 'schema-drift' | 'identity-drift' | 'origin-drift';
|
|
13
|
+
export declare function reference(input: Publication): PublicationRef;
|
|
14
|
+
export declare function acceptReference(input: unknown): PublicationRef;
|
|
15
|
+
export declare function compareReferences(input: PublicationRef, expected: PublicationRef): PublicationReferenceRelation;
|
|
16
|
+
export declare function compareReference(input: Publication, expected: PublicationRef): PublicationReferenceRelation;
|
|
17
|
+
export declare function matchesReference(input: Publication, expected: PublicationRef): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AuthValueError } from '@astrale-os/kernel-core/auth/error';
|
|
2
|
+
import { accept as acceptIssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
3
|
+
import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
4
|
+
import { deepFreeze, exact, record } from './content.js';
|
|
5
|
+
import { fail } from './error.js';
|
|
6
|
+
import { admitDigest } from './etag.js';
|
|
7
|
+
export function reference(input) {
|
|
8
|
+
return deepFreeze({
|
|
9
|
+
origin: input.origin,
|
|
10
|
+
identity: { ...input.identity },
|
|
11
|
+
revision: input.schema.revision,
|
|
12
|
+
etag: input.etag,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function acceptReference(input) {
|
|
16
|
+
const value = record(input, 'PUBLICATION_INVALID', 'Publication reference is invalid.');
|
|
17
|
+
exact(value, ['origin', 'identity', 'revision', 'etag'], 'PUBLICATION_INVALID');
|
|
18
|
+
if (typeof value.origin !== 'string' || !patterns.origin.test(value.origin)) {
|
|
19
|
+
fail('PUBLICATION_INVALID', 'Publication reference origin is invalid.');
|
|
20
|
+
}
|
|
21
|
+
const origin = value.origin;
|
|
22
|
+
const identity = record(value.identity, 'PUBLICATION_INVALID', 'Publication reference identity is invalid.');
|
|
23
|
+
exact(identity, ['issuer', 'subject'], 'PUBLICATION_INVALID');
|
|
24
|
+
const issuer = acceptIssuer(identity.issuer);
|
|
25
|
+
if (identity.subject !== origin) {
|
|
26
|
+
fail('PUBLICATION_INVALID', 'Publication reference subject must equal its root origin.');
|
|
27
|
+
}
|
|
28
|
+
if (typeof value.revision !== 'string' || !patterns.revision.test(value.revision)) {
|
|
29
|
+
fail('PUBLICATION_INVALID', 'Publication reference revision is invalid.');
|
|
30
|
+
}
|
|
31
|
+
return deepFreeze({
|
|
32
|
+
origin,
|
|
33
|
+
identity: { issuer, subject: origin },
|
|
34
|
+
revision: value.revision,
|
|
35
|
+
etag: admitDigest(value.etag),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function compareReferences(input, expected) {
|
|
39
|
+
if (input.origin !== expected.origin)
|
|
40
|
+
return 'origin-drift';
|
|
41
|
+
if (input.identity.issuer !== expected.identity.issuer ||
|
|
42
|
+
input.identity.subject !== expected.identity.subject) {
|
|
43
|
+
return 'identity-drift';
|
|
44
|
+
}
|
|
45
|
+
if (input.revision !== expected.revision)
|
|
46
|
+
return 'schema-drift';
|
|
47
|
+
return input.etag === expected.etag ? 'exact' : 'same-schema';
|
|
48
|
+
}
|
|
49
|
+
export function compareReference(input, expected) {
|
|
50
|
+
return compareReferences(reference(input), expected);
|
|
51
|
+
}
|
|
52
|
+
export function matchesReference(input, expected) {
|
|
53
|
+
return compareReference(input, expected) === 'exact';
|
|
54
|
+
}
|
|
55
|
+
function acceptIssuer(input) {
|
|
56
|
+
if (typeof input !== 'string') {
|
|
57
|
+
fail('PUBLICATION_INVALID', 'Publication reference issuer is invalid.');
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return acceptIssuerId(input);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
if (cause instanceof AuthValueError) {
|
|
64
|
+
return fail('PUBLICATION_INVALID', 'Publication reference issuer is invalid.', cause);
|
|
65
|
+
}
|
|
66
|
+
throw cause;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AuthorityOperation } from '@astrale-os/kernel-core/schema/authority/operation';
|
|
2
|
+
import type { schema } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
+
import type { ClassRef, 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 CapabilityAdminRequirements {
|
|
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 admin?: CapabilityAdminRequirements;
|
|
19
|
+
}
|
|
20
|
+
export interface PublicationRequirements {
|
|
21
|
+
readonly capabilities: CapabilityRequirements;
|
|
22
|
+
}
|
|
23
|
+
/** Admit the exact closed Publication v2 requirements object. */
|
|
24
|
+
export declare function admitRequirements(input: unknown): PublicationRequirements;
|
|
25
|
+
/** Bind explicit capability targets to exact functional dependencies in the bundle closure. */
|
|
26
|
+
export declare function validateRequirements(root: schema.DomainSchema, closure: readonly schema.DomainSchema[], requirements: PublicationRequirements): void;
|
|
@@ -0,0 +1,180 @@
|
|
|
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 { revision } from '@astrale-os/kernel-dsl/v1/schema/revision';
|
|
4
|
+
import { array, deepFreeze, exact, record, unique } from './content.js';
|
|
5
|
+
import { fail } from './error.js';
|
|
6
|
+
export const ClassOperationOrder = Object.freeze([
|
|
7
|
+
'create',
|
|
8
|
+
'read',
|
|
9
|
+
'traverse',
|
|
10
|
+
'update',
|
|
11
|
+
'delete',
|
|
12
|
+
]);
|
|
13
|
+
/** Admit the exact closed Publication v2 requirements object. */
|
|
14
|
+
export function admitRequirements(input) {
|
|
15
|
+
const requirements = record(input, 'PUBLICATION_INVALID', 'Publication requirements are invalid.');
|
|
16
|
+
exact(requirements, ['capabilities'], 'PUBLICATION_INVALID');
|
|
17
|
+
const capabilities = record(requirements.capabilities, 'PUBLICATION_INVALID', 'Publication capability requirements are invalid.');
|
|
18
|
+
exact(capabilities, ['admin', 'classes', 'functions'], 'PUBLICATION_INVALID');
|
|
19
|
+
const functions = capabilities.functions === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: nonEmpty(capabilities.functions, 'Function capability requirements').map(admitCallable);
|
|
22
|
+
if (functions !== undefined) {
|
|
23
|
+
unique(functions, 'PUBLICATION_INVALID', 'Function capability requirement');
|
|
24
|
+
canonical(functions, compare, 'Function capability requirements');
|
|
25
|
+
}
|
|
26
|
+
const classes = capabilities.classes === undefined
|
|
27
|
+
? undefined
|
|
28
|
+
: nonEmpty(capabilities.classes, 'Class capability requirements').map(admitClassRequirement);
|
|
29
|
+
if (classes !== undefined) {
|
|
30
|
+
unique(classes.map((entry) => classKey(entry.class)), 'PUBLICATION_INVALID', 'Class capability requirement');
|
|
31
|
+
canonical(classes, (left, right) => compare(classKey(left.class), classKey(right.class)), 'Class capability requirements');
|
|
32
|
+
}
|
|
33
|
+
const admin = capabilities.admin === undefined ? undefined : admitAdminRequirements(capabilities.admin);
|
|
34
|
+
return deepFreeze({
|
|
35
|
+
capabilities: {
|
|
36
|
+
...(functions === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { functions: Object.freeze(functions) }),
|
|
39
|
+
...(classes === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { classes: Object.freeze(classes) }),
|
|
42
|
+
...(admin === undefined ? {} : { admin }),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Bind explicit capability targets to exact functional dependencies in the bundle closure. */
|
|
47
|
+
export function validateRequirements(root, closure, requirements) {
|
|
48
|
+
const dependencies = exactDependencyClosure(root, closure);
|
|
49
|
+
for (const callable of requirements.capabilities.functions ?? []) {
|
|
50
|
+
const dependency = capabilityOwner(root, dependencies, callable, 'Function');
|
|
51
|
+
if (!hasCallable(dependency, callable)) {
|
|
52
|
+
fail('PUBLICATION_INVALID', 'Function capability requirement names an unknown dependency callable.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const requirement of requirements.capabilities.classes ?? []) {
|
|
56
|
+
const key = Key.of(requirement.class);
|
|
57
|
+
const dependency = capabilityOwner(root, dependencies, key, 'Class');
|
|
58
|
+
if (dependency.classes[requirement.class.name] === undefined) {
|
|
59
|
+
fail('PUBLICATION_INVALID', 'Class capability requirement names an unknown dependency Class.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function exactDependencyClosure(root, closure) {
|
|
64
|
+
const supplied = new Map(closure.map((dependency) => [dependency.origin, dependency]));
|
|
65
|
+
const reachable = new Map();
|
|
66
|
+
const visit = (owner) => {
|
|
67
|
+
for (const coordinate of Object.values(owner.dependencies)) {
|
|
68
|
+
const dependency = supplied.get(coordinate.origin);
|
|
69
|
+
if (dependency === undefined || revision(dependency) !== coordinate.revision) {
|
|
70
|
+
fail('PUBLICATION_INVALID', 'Publication Bundle omits an exact dependency.');
|
|
71
|
+
}
|
|
72
|
+
if (reachable.has(dependency.origin))
|
|
73
|
+
continue;
|
|
74
|
+
reachable.set(dependency.origin, dependency);
|
|
75
|
+
visit(dependency);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
visit(root);
|
|
79
|
+
return reachable;
|
|
80
|
+
}
|
|
81
|
+
function admitCallable(input) {
|
|
82
|
+
if (typeof input !== 'string' || !Key.is(input)) {
|
|
83
|
+
return fail('PUBLICATION_INVALID', 'Function capability reference is invalid.');
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
callableAddress(input);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return fail('PUBLICATION_INVALID', 'Function capability reference must name a Function or Method.');
|
|
90
|
+
}
|
|
91
|
+
return input;
|
|
92
|
+
}
|
|
93
|
+
function admitClassRequirement(input) {
|
|
94
|
+
const requirement = record(input, 'PUBLICATION_INVALID', 'Class capability requirement is invalid.');
|
|
95
|
+
exact(requirement, ['class', 'operations'], 'PUBLICATION_INVALID');
|
|
96
|
+
const operations = nonEmpty(requirement.operations, 'Class capability operations').map((entry) => admitOperation(entry, ClassOperationOrder, 'Class capability operation'));
|
|
97
|
+
unique(operations, 'PUBLICATION_INVALID', 'Class capability operation');
|
|
98
|
+
canonical(operations, ordered(ClassOperationOrder), 'Class capability operations');
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
class: admitClassRef(requirement.class),
|
|
101
|
+
operations: Object.freeze(operations),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function admitAdminRequirements(input) {
|
|
105
|
+
const admin = record(input, 'PUBLICATION_INVALID', 'Administrative capability requirements are invalid.');
|
|
106
|
+
exact(admin, ['grant', 'revoke'], 'PUBLICATION_INVALID');
|
|
107
|
+
if (admin.grant === undefined && admin.revoke === undefined) {
|
|
108
|
+
fail('PUBLICATION_INVALID', 'Administrative capability requirements cannot be empty.');
|
|
109
|
+
}
|
|
110
|
+
const grant = admitAuthorityOperations(admin.grant, 'grant');
|
|
111
|
+
const revoke = admitAuthorityOperations(admin.revoke, 'revoke');
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
...(grant === undefined ? {} : { grant }),
|
|
114
|
+
...(revoke === undefined ? {} : { revoke }),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function admitAuthorityOperations(input, kind) {
|
|
118
|
+
if (input === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const values = nonEmpty(input, `Administrative ${kind} operations`).map((entry) => admitOperation(entry, AuthorityOperationOrder, `Administrative ${kind} operation`));
|
|
121
|
+
unique(values, 'PUBLICATION_INVALID', `administrative ${kind} operation`);
|
|
122
|
+
canonical(values, ordered(AuthorityOperationOrder), `Administrative ${kind} operations`);
|
|
123
|
+
return Object.freeze(values);
|
|
124
|
+
}
|
|
125
|
+
function admitClassRef(input) {
|
|
126
|
+
let value;
|
|
127
|
+
try {
|
|
128
|
+
value = acceptDefinitionRef(input);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
fail('PUBLICATION_INVALID', 'Class capability reference is invalid.');
|
|
132
|
+
}
|
|
133
|
+
if (value.kind !== 'class')
|
|
134
|
+
fail('PUBLICATION_INVALID', 'Class capability reference is invalid.');
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function nonEmpty(input, label) {
|
|
138
|
+
const values = [...array(input, 'PUBLICATION_INVALID')];
|
|
139
|
+
if (values.length === 0)
|
|
140
|
+
fail('PUBLICATION_INVALID', `${label} cannot be empty.`);
|
|
141
|
+
return values;
|
|
142
|
+
}
|
|
143
|
+
function admitOperation(input, vocabulary, label) {
|
|
144
|
+
if (typeof input !== 'string' || !vocabulary.includes(input)) {
|
|
145
|
+
return fail('PUBLICATION_INVALID', `${label} is unknown.`);
|
|
146
|
+
}
|
|
147
|
+
return input;
|
|
148
|
+
}
|
|
149
|
+
function canonical(values, comparator, label) {
|
|
150
|
+
if (values.some((entry, index) => index > 0 && comparator(values[index - 1], entry) >= 0)) {
|
|
151
|
+
fail('PUBLICATION_INVALID', `${label} must be sorted canonically.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function ordered(order) {
|
|
155
|
+
return (left, right) => order.indexOf(left) - order.indexOf(right);
|
|
156
|
+
}
|
|
157
|
+
function compare(left, right) {
|
|
158
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
159
|
+
}
|
|
160
|
+
function classKey(input) {
|
|
161
|
+
return String(Key.of(input));
|
|
162
|
+
}
|
|
163
|
+
function capabilityOwner(root, dependencies, key, label) {
|
|
164
|
+
const origin = Key.origin(key);
|
|
165
|
+
if (origin === root.origin)
|
|
166
|
+
fail('PUBLICATION_INVALID', `${label} capability target cannot be local.`);
|
|
167
|
+
const dependency = dependencies.get(origin);
|
|
168
|
+
if (dependency === undefined) {
|
|
169
|
+
fail('PUBLICATION_INVALID', `${label} capability target must belong to the exact dependency closure.`);
|
|
170
|
+
}
|
|
171
|
+
return dependency;
|
|
172
|
+
}
|
|
173
|
+
function hasCallable(dependency, callable) {
|
|
174
|
+
const coordinate = callableAddress(callable);
|
|
175
|
+
if (coordinate.kind === 'function')
|
|
176
|
+
return dependency.functions[coordinate.name] !== undefined;
|
|
177
|
+
const declaration = dependency.classes[coordinate.owner.name];
|
|
178
|
+
const method = declaration?.kind === 'node' ? declaration.methods[coordinate.name] : undefined;
|
|
179
|
+
return method !== undefined && method.inheritance !== 'abstract';
|
|
180
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | '*';
|
|
3
|
+
export type SingleCredentialSource = {
|
|
4
|
+
readonly in: 'header';
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly in: 'cookie';
|
|
8
|
+
readonly name: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly in: 'query';
|
|
11
|
+
readonly name: string;
|
|
12
|
+
};
|
|
13
|
+
export type CredentialSource = SingleCredentialSource | readonly [SingleCredentialSource, ...SingleCredentialSource[]];
|
|
14
|
+
export type BodyPick = string | {
|
|
15
|
+
readonly as: 'text';
|
|
16
|
+
};
|
|
17
|
+
export type RouteBody = '*' | Readonly<Record<string, BodyPick>>;
|
|
18
|
+
export type RouteHeaders = Readonly<Record<string, string | '*'>>;
|
|
19
|
+
export interface HttpRoute {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly callable: Key.Callable;
|
|
22
|
+
readonly href: string;
|
|
23
|
+
readonly method: HttpMethod;
|
|
24
|
+
readonly requestMethod?: string;
|
|
25
|
+
readonly default?: true;
|
|
26
|
+
readonly receiver?: string;
|
|
27
|
+
readonly query?: Readonly<Record<string, string>>;
|
|
28
|
+
readonly headers?: RouteHeaders;
|
|
29
|
+
readonly body?: RouteBody;
|
|
30
|
+
readonly credential?: CredentialSource;
|
|
31
|
+
}
|
|
32
|
+
/** Provider-neutral build input; placement replaces path with the canonical absolute href. */
|
|
33
|
+
export interface HttpRouteDeclaration extends Omit<HttpRoute, 'href'> {
|
|
34
|
+
readonly path: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function admitRoute(input: unknown, origin: string): HttpRoute;
|
|
37
|
+
export declare function admitRouteDeclaration(input: unknown, origin: string): HttpRouteDeclaration;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { admitCallableKey } from './bindings/callable.js';
|
|
3
|
+
import { absoluteHttpUrl, deepFreeze, exact, record } from './content.js';
|
|
4
|
+
import { fail } from './error.js';
|
|
5
|
+
const ROUTE_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u;
|
|
6
|
+
export function admitRoute(input, origin) {
|
|
7
|
+
const value = record(input, 'PUBLICATION_ROUTE_INVALID', 'Published HTTP route is invalid.');
|
|
8
|
+
const common = admitRouteCommon(value, origin, 'href');
|
|
9
|
+
return deepFreeze({
|
|
10
|
+
...common,
|
|
11
|
+
href: absoluteHttpUrl(value.href, false, 'PUBLICATION_ROUTE_INVALID', true),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function admitRouteDeclaration(input, origin) {
|
|
15
|
+
const value = record(input, 'PUBLICATION_ROUTE_INVALID', 'HTTP route declaration is invalid.');
|
|
16
|
+
const common = admitRouteCommon(value, origin, 'path');
|
|
17
|
+
if (typeof value.path !== 'string' || !value.path.startsWith('/')) {
|
|
18
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route declaration path must be relative to one issuer.');
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(value.path, 'https://placement.invalid');
|
|
23
|
+
}
|
|
24
|
+
catch (cause) {
|
|
25
|
+
return fail('PUBLICATION_ROUTE_INVALID', 'Route declaration path is invalid.', cause);
|
|
26
|
+
}
|
|
27
|
+
if (parsed.origin !== 'https://placement.invalid' || parsed.search !== '' || parsed.hash !== '') {
|
|
28
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route declaration path cannot select an origin, query, or fragment.');
|
|
29
|
+
}
|
|
30
|
+
return deepFreeze({ ...common, path: value.path });
|
|
31
|
+
}
|
|
32
|
+
function admitRouteCommon(value, origin, location) {
|
|
33
|
+
exact(value, [
|
|
34
|
+
'name',
|
|
35
|
+
'callable',
|
|
36
|
+
location,
|
|
37
|
+
'method',
|
|
38
|
+
'requestMethod',
|
|
39
|
+
'default',
|
|
40
|
+
'receiver',
|
|
41
|
+
'query',
|
|
42
|
+
'headers',
|
|
43
|
+
'body',
|
|
44
|
+
'credential',
|
|
45
|
+
], 'PUBLICATION_ROUTE_INVALID');
|
|
46
|
+
if (typeof value.name !== 'string' || !ROUTE_NAME.test(value.name)) {
|
|
47
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route name is invalid.');
|
|
48
|
+
}
|
|
49
|
+
const callable = admitCallableKey(value.callable, origin);
|
|
50
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*'].includes(value.method)) {
|
|
51
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route method is invalid.');
|
|
52
|
+
}
|
|
53
|
+
if (value.default !== undefined && value.default !== true) {
|
|
54
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route default may only be true.');
|
|
55
|
+
}
|
|
56
|
+
if (value.requestMethod !== undefined &&
|
|
57
|
+
(typeof value.requestMethod !== 'string' || !patterns.memberName.test(value.requestMethod))) {
|
|
58
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route request-method mapping is invalid.');
|
|
59
|
+
}
|
|
60
|
+
if (value.receiver !== undefined &&
|
|
61
|
+
(typeof value.receiver !== 'string' || !patterns.memberName.test(value.receiver))) {
|
|
62
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route receiver is invalid.');
|
|
63
|
+
}
|
|
64
|
+
const query = stringRecord(value.query);
|
|
65
|
+
const headers = headerRecord(value.headers);
|
|
66
|
+
const body = admitBody(value.body);
|
|
67
|
+
const credential = admitCredential(value.credential);
|
|
68
|
+
return {
|
|
69
|
+
name: value.name,
|
|
70
|
+
callable,
|
|
71
|
+
method: value.method,
|
|
72
|
+
...(value.requestMethod === undefined ? {} : { requestMethod: value.requestMethod }),
|
|
73
|
+
...(value.default === true ? { default: true } : {}),
|
|
74
|
+
...(value.receiver === undefined ? {} : { receiver: value.receiver }),
|
|
75
|
+
...(query === undefined ? {} : { query }),
|
|
76
|
+
...(headers === undefined ? {} : { headers }),
|
|
77
|
+
...(body === undefined ? {} : { body }),
|
|
78
|
+
...(credential === undefined ? {} : { credential }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function admitCredential(input) {
|
|
82
|
+
if (input === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
const values = Array.isArray(input) ? input : [input];
|
|
85
|
+
if (values.length === 0)
|
|
86
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Credential source list is empty.');
|
|
87
|
+
const admitted = values.map((entry) => {
|
|
88
|
+
const value = record(entry, 'PUBLICATION_ROUTE_INVALID', 'Credential source is invalid.');
|
|
89
|
+
exact(value, ['in', 'name'], 'PUBLICATION_ROUTE_INVALID');
|
|
90
|
+
if (value.in === 'header') {
|
|
91
|
+
if (value.name !== undefined && (typeof value.name !== 'string' || value.name.length === 0)) {
|
|
92
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Credential header name is invalid.');
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
in: 'header',
|
|
96
|
+
...(value.name === undefined ? {} : { name: value.name }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if ((value.in === 'cookie' || value.in === 'query') &&
|
|
100
|
+
typeof value.name === 'string' &&
|
|
101
|
+
value.name.length > 0) {
|
|
102
|
+
return Object.freeze({ in: value.in, name: value.name });
|
|
103
|
+
}
|
|
104
|
+
return fail('PUBLICATION_ROUTE_INVALID', 'Credential source is invalid.');
|
|
105
|
+
});
|
|
106
|
+
return Array.isArray(input) ? Object.freeze(admitted) : admitted[0];
|
|
107
|
+
}
|
|
108
|
+
function admitBody(input) {
|
|
109
|
+
if (input === undefined || input === '*')
|
|
110
|
+
return input;
|
|
111
|
+
const value = record(input, 'PUBLICATION_ROUTE_INVALID', 'Route body mapping is invalid.');
|
|
112
|
+
const output = {};
|
|
113
|
+
for (const [field, pick] of Object.entries(value)) {
|
|
114
|
+
if (typeof pick === 'string' && pick.length > 0)
|
|
115
|
+
output[field] = pick;
|
|
116
|
+
else if (pick !== null &&
|
|
117
|
+
typeof pick === 'object' &&
|
|
118
|
+
!Array.isArray(pick) &&
|
|
119
|
+
Object.keys(pick).length === 1 &&
|
|
120
|
+
pick.as === 'text') {
|
|
121
|
+
output[field] = { as: 'text' };
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route body pick is invalid.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze(output);
|
|
128
|
+
}
|
|
129
|
+
function headerRecord(input) {
|
|
130
|
+
if (input === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const value = record(input, 'PUBLICATION_ROUTE_INVALID', 'Route header mapping is invalid.');
|
|
133
|
+
const output = {};
|
|
134
|
+
for (const [field, pick] of Object.entries(value)) {
|
|
135
|
+
if (typeof pick !== 'string' || pick.length === 0) {
|
|
136
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route header pick is invalid.');
|
|
137
|
+
}
|
|
138
|
+
output[field] = pick;
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze(output);
|
|
141
|
+
}
|
|
142
|
+
function stringRecord(input) {
|
|
143
|
+
if (input === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
const value = record(input, 'PUBLICATION_ROUTE_INVALID', 'Route mapping is invalid.');
|
|
146
|
+
const output = {};
|
|
147
|
+
for (const [field, wire] of Object.entries(value)) {
|
|
148
|
+
if (typeof wire !== 'string' || wire.length === 0) {
|
|
149
|
+
fail('PUBLICATION_ROUTE_INVALID', 'Route wire name is invalid.');
|
|
150
|
+
}
|
|
151
|
+
output[field] = wire;
|
|
152
|
+
}
|
|
153
|
+
return Object.freeze(output);
|
|
154
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Revision } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import type { BundleDescriptor } from './bundle-descriptor.js';
|
|
3
|
+
export interface Schema {
|
|
4
|
+
readonly revision: Revision;
|
|
5
|
+
readonly bundle: BundleDescriptor;
|
|
6
|
+
}
|
|
7
|
+
export declare function admitSchema(input: unknown): Schema;
|