@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,15 @@
|
|
|
1
|
+
import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { admitBundleDescriptor } from './bundle-descriptor.js';
|
|
3
|
+
import { exact, record } from './content.js';
|
|
4
|
+
import { fail } from './error.js';
|
|
5
|
+
export function admitSchema(input) {
|
|
6
|
+
const value = record(input, 'PUBLICATION_INVALID', 'Published schema is invalid.');
|
|
7
|
+
exact(value, ['revision', 'bundle'], 'PUBLICATION_INVALID');
|
|
8
|
+
if (typeof value.revision !== 'string' || !patterns.revision.test(value.revision)) {
|
|
9
|
+
fail('PUBLICATION_INVALID', 'Published schema revision is invalid.');
|
|
10
|
+
}
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
revision: value.revision,
|
|
13
|
+
bundle: admitBundleDescriptor(value.bundle),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { accept as acceptPortable } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import { acceptReference } from '../publication/reference.js';
|
|
3
|
+
import { admitRoute } from '../publication/route.js';
|
|
4
|
+
import { fail } from './error.js';
|
|
5
|
+
const AUTH = new Set(['anonymous', 'authenticated', 'authorized']);
|
|
6
|
+
const OUTPUT = new Set(['value', 'stream', 'binary']);
|
|
7
|
+
/** Admit one emitted portable Table before Server composition. */
|
|
8
|
+
export function acceptTable(input) {
|
|
9
|
+
const value = object(acceptPortable(input), 'Route Table');
|
|
10
|
+
exact(value, ['publication', 'entries'], 'Route Table');
|
|
11
|
+
const reference = acceptReference(value.publication);
|
|
12
|
+
if (!Array.isArray(value.entries))
|
|
13
|
+
invalid('Route Table entries');
|
|
14
|
+
const entries = value.entries.map((entry) => acceptRoute(entry, reference.origin));
|
|
15
|
+
if (new Set(entries.map(({ name }) => name)).size !== entries.length) {
|
|
16
|
+
fail('NATIVE_ROUTE_BINDING_INVALID', 'Route Table names must be unique.');
|
|
17
|
+
}
|
|
18
|
+
return Object.freeze({ publication: reference, entries: Object.freeze(entries) });
|
|
19
|
+
}
|
|
20
|
+
function acceptRoute(input, origin) {
|
|
21
|
+
const value = object(input, 'Route Table entry');
|
|
22
|
+
exact(value, ['name', 'callable', 'target', 'auth', 'output', 'href', 'method', 'default'], 'Route Table entry', ['requestMethod', 'query', 'headers', 'body', 'credential']);
|
|
23
|
+
if (typeof value.default !== 'boolean')
|
|
24
|
+
invalid('Route Table default');
|
|
25
|
+
if (typeof value.auth !== 'string' || !AUTH.has(value.auth)) {
|
|
26
|
+
invalid('Route Table auth');
|
|
27
|
+
}
|
|
28
|
+
if (typeof value.output !== 'string' || !OUTPUT.has(value.output))
|
|
29
|
+
invalid('Route Table output');
|
|
30
|
+
const wire = admitRoute({
|
|
31
|
+
name: value.name,
|
|
32
|
+
callable: value.callable,
|
|
33
|
+
href: value.href,
|
|
34
|
+
method: value.method,
|
|
35
|
+
...(value.default === true ? { default: true } : {}),
|
|
36
|
+
...(value.requestMethod === undefined ? {} : { requestMethod: value.requestMethod }),
|
|
37
|
+
...(value.query === undefined ? {} : { query: value.query }),
|
|
38
|
+
...(value.headers === undefined ? {} : { headers: value.headers }),
|
|
39
|
+
...(value.body === undefined ? {} : { body: value.body }),
|
|
40
|
+
...(value.credential === undefined ? {} : { credential: value.credential }),
|
|
41
|
+
...receiver(value.target),
|
|
42
|
+
}, origin);
|
|
43
|
+
const { default: _default, receiver: _receiver, ...common } = wire;
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
...common,
|
|
46
|
+
target: target(value.target),
|
|
47
|
+
auth: value.auth,
|
|
48
|
+
output: value.output,
|
|
49
|
+
default: value.default,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function target(input) {
|
|
53
|
+
const value = object(input, 'Route target');
|
|
54
|
+
if (value.kind === 'function') {
|
|
55
|
+
exact(value, ['kind'], 'Route Function target');
|
|
56
|
+
return Object.freeze({ kind: 'function' });
|
|
57
|
+
}
|
|
58
|
+
if (value.kind !== 'method' || (value.dispatch !== 'static' && value.dispatch !== 'instance')) {
|
|
59
|
+
invalid('Route Method target');
|
|
60
|
+
}
|
|
61
|
+
if (value.dispatch === 'static') {
|
|
62
|
+
exact(value, ['kind', 'dispatch'], 'Route static Method target');
|
|
63
|
+
return Object.freeze({ kind: 'method', dispatch: 'static' });
|
|
64
|
+
}
|
|
65
|
+
exact(value, ['kind', 'dispatch', 'receiver'], 'Route instance Method target');
|
|
66
|
+
if (typeof value.receiver !== 'string')
|
|
67
|
+
invalid('Route receiver');
|
|
68
|
+
return Object.freeze({ kind: 'method', dispatch: 'instance', receiver: value.receiver });
|
|
69
|
+
}
|
|
70
|
+
function receiver(input) {
|
|
71
|
+
const admitted = target(input);
|
|
72
|
+
return admitted.kind === 'method' && admitted.dispatch === 'instance'
|
|
73
|
+
? { receiver: admitted.receiver }
|
|
74
|
+
: {};
|
|
75
|
+
}
|
|
76
|
+
function object(input, label) {
|
|
77
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
78
|
+
invalid(label);
|
|
79
|
+
return input;
|
|
80
|
+
}
|
|
81
|
+
function exact(value, required, label, optional = []) {
|
|
82
|
+
const allowed = new Set([...required, ...optional]);
|
|
83
|
+
if (Object.keys(value).some((key) => !allowed.has(key)) ||
|
|
84
|
+
required.some((key) => !Object.hasOwn(value, key)))
|
|
85
|
+
invalid(label);
|
|
86
|
+
}
|
|
87
|
+
function invalid(label) {
|
|
88
|
+
return fail('NATIVE_ROUTE_BINDING_INVALID', `${label} is invalid.`);
|
|
89
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Domain } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { Publication } from '../publication/publication.js';
|
|
3
|
+
import type { Plan } from './plan.js';
|
|
4
|
+
import type { Table } from './table.js';
|
|
5
|
+
/** Bind one Plan to an exact placed Publication and matching resolved Domain. */
|
|
6
|
+
export declare function bind(plan: Plan, deployed: Publication, domain: Domain): Table;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { json } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import { reference } from '../publication/reference.js';
|
|
3
|
+
import { fail } from './error.js';
|
|
4
|
+
import { place } from './placement.js';
|
|
5
|
+
import { rejectReserved } from './reservation.js';
|
|
6
|
+
/** Bind one Plan to an exact placed Publication and matching resolved Domain. */
|
|
7
|
+
export function bind(plan, deployed, domain) {
|
|
8
|
+
if (plan.origin !== deployed.origin ||
|
|
9
|
+
plan.origin !== domain.origin ||
|
|
10
|
+
plan.revision !== deployed.schema.revision ||
|
|
11
|
+
plan.revision !== domain.revision) {
|
|
12
|
+
fail('NATIVE_ROUTE_BINDING_INVALID', 'Route Plan, Publication, and Domain identity differ.');
|
|
13
|
+
}
|
|
14
|
+
if (json.serialize(place(plan, deployed.identity.issuer)) !==
|
|
15
|
+
json.serialize(deployed.routes)) {
|
|
16
|
+
fail('NATIVE_ROUTE_BINDING_INVALID', 'Publication routes differ from the placed Route Plan.');
|
|
17
|
+
}
|
|
18
|
+
const entries = plan.entries.map((entry, index) => {
|
|
19
|
+
const { path: _path, ...portable } = entry;
|
|
20
|
+
return Object.freeze({ ...portable, href: deployed.routes[index].href });
|
|
21
|
+
});
|
|
22
|
+
rejectReserved(deployed, entries);
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
publication: reference(deployed),
|
|
25
|
+
entries: Object.freeze(entries),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { fail } from './error.js';
|
|
2
|
+
import { templatesOverlap } from './template.js';
|
|
3
|
+
export function rejectCollisions(entries) {
|
|
4
|
+
const accepted = [];
|
|
5
|
+
for (const entry of entries) {
|
|
6
|
+
for (const previous of accepted) {
|
|
7
|
+
if (overlap(entry.method, previous.method) && templatesOverlap(entry.href, previous.href)) {
|
|
8
|
+
fail('NATIVE_ROUTE_COLLISION', `Route ${entry.name} collides with ${previous.name}.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
accepted.push(entry);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function overlap(left, right) {
|
|
15
|
+
return left === '*' || right === '*' || left === right;
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Domain } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type * as publication from '../publication/index.js';
|
|
3
|
+
import type { Plan } from './plan.js';
|
|
4
|
+
export interface CompileInput {
|
|
5
|
+
readonly routes: readonly publication.HttpRouteDeclaration[];
|
|
6
|
+
readonly domain: Domain;
|
|
7
|
+
}
|
|
8
|
+
export declare function compile(input: CompileInput): Plan;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { admitRouteDeclaration } from '../publication/route.js';
|
|
2
|
+
import { rejectCollisions } from './collision.js';
|
|
3
|
+
import { fail } from './error.js';
|
|
4
|
+
import { validateCredentialMapping, validateMapping, validateWireMapping } from './mapping.js';
|
|
5
|
+
import { placePath } from './placement.js';
|
|
6
|
+
import { validateTemplate } from './template.js';
|
|
7
|
+
export function compile(input) {
|
|
8
|
+
if (input === null || typeof input !== 'object' || !Array.isArray(input.routes)) {
|
|
9
|
+
throw new TypeError('Route compilation input is invalid.');
|
|
10
|
+
}
|
|
11
|
+
const authored = input.routes.map((entry) => admitRouteDeclaration(entry, input.domain.origin));
|
|
12
|
+
const entries = authored.map((declaration) => {
|
|
13
|
+
const meaning = input.domain.callable(declaration.callable);
|
|
14
|
+
if (meaning === undefined) {
|
|
15
|
+
fail('NATIVE_ROUTE_BINDING_INVALID', `Unknown callable ${declaration.callable}.`);
|
|
16
|
+
}
|
|
17
|
+
const href = placePath(declaration.path, 'https://routes.invalid');
|
|
18
|
+
const route = { ...declaration, href };
|
|
19
|
+
validateTemplate(href);
|
|
20
|
+
validateMapping(route, meaning.inputFields, isMethod(meaning) && !meaning.static);
|
|
21
|
+
validateWireMapping(route);
|
|
22
|
+
validateCredentialMapping(route);
|
|
23
|
+
if (route.method === 'GET' && route.body !== undefined) {
|
|
24
|
+
fail('NATIVE_ROUTE_DECLARATION_INVALID', `GET route ${route.name} cannot declare a body.`);
|
|
25
|
+
}
|
|
26
|
+
const target = !isMethod(meaning)
|
|
27
|
+
? { kind: 'function' }
|
|
28
|
+
: meaning.static
|
|
29
|
+
? { kind: 'method', dispatch: 'static' }
|
|
30
|
+
: { kind: 'method', dispatch: 'instance', receiver: route.receiver };
|
|
31
|
+
const { href: _href, receiver: _receiver, ...portable } = route;
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
...portable,
|
|
34
|
+
target,
|
|
35
|
+
auth: meaning.auth,
|
|
36
|
+
output: meaning.output.mode,
|
|
37
|
+
path: declaration.path,
|
|
38
|
+
default: declaration.default === true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
rejectCollisions(entries.map((entry) => ({ ...entry, href: placePath(entry.path, 'https://routes.invalid') })));
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
origin: input.domain.origin,
|
|
44
|
+
revision: input.domain.revision,
|
|
45
|
+
entries: Object.freeze(entries),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function isMethod(meaning) {
|
|
49
|
+
return 'owner' in meaning;
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HttpRoute, SingleCredentialSource } from '../publication/index.js';
|
|
2
|
+
import type { Route } from './table.js';
|
|
3
|
+
export declare function credentialSources(route: Pick<HttpRoute, 'credential'>): readonly SingleCredentialSource[];
|
|
4
|
+
export declare function firstCredentialIsAmbient(entry: Route): boolean;
|
|
5
|
+
export declare function specificHeaderWires(entry: Route): ReadonlySet<string>;
|
|
6
|
+
export declare function putCredential(entry: Route, url: URL, headers: Record<string, string>, credential: string): void;
|
|
7
|
+
export declare function withoutCredentialHeaders(entry: Route, input: Readonly<Record<string, string>>): Record<string, string>;
|
|
8
|
+
export declare function readCredential(entry: Route, url: URL, headers: Record<string, string>): string | undefined;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { invalidRequest } from './error.js';
|
|
2
|
+
export function credentialSources(route) {
|
|
3
|
+
const source = route.credential ?? { in: 'header' };
|
|
4
|
+
return Array.isArray(source) ? source : [source];
|
|
5
|
+
}
|
|
6
|
+
export function firstCredentialIsAmbient(entry) {
|
|
7
|
+
return credentialSources(entry)[0].in === 'cookie';
|
|
8
|
+
}
|
|
9
|
+
export function specificHeaderWires(entry) {
|
|
10
|
+
return new Set(Object.values(entry.headers ?? {})
|
|
11
|
+
.filter((wire) => wire !== '*')
|
|
12
|
+
.map((wire) => wire.toLowerCase()));
|
|
13
|
+
}
|
|
14
|
+
export function putCredential(entry, url, headers, credential) {
|
|
15
|
+
if (entry.credential === undefined) {
|
|
16
|
+
headers.authorization = `Bearer ${credential}`;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const source = credentialSources(entry)[0];
|
|
20
|
+
if (source.in === 'header') {
|
|
21
|
+
headers[(source.name ?? 'authorization').toLowerCase()] = credential;
|
|
22
|
+
}
|
|
23
|
+
else if (source.in === 'query') {
|
|
24
|
+
url.searchParams.set(source.name, credential);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
invalidRequest('A cookie credential is ambient and cannot be emitted by JavaScript.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function withoutCredentialHeaders(entry, input) {
|
|
31
|
+
const output = {};
|
|
32
|
+
for (const [name, value] of Object.entries(input)) {
|
|
33
|
+
const canonical = name.toLowerCase();
|
|
34
|
+
if (Object.hasOwn(output, canonical)) {
|
|
35
|
+
invalidRequest(`Header ${canonical} occurs more than once.`);
|
|
36
|
+
}
|
|
37
|
+
output[canonical] = value;
|
|
38
|
+
}
|
|
39
|
+
for (const source of credentialSources(entry)) {
|
|
40
|
+
if (source.in === 'header') {
|
|
41
|
+
delete output[(source.name ?? 'authorization').toLowerCase()];
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (source.in === 'cookie' && output.cookie !== undefined) {
|
|
45
|
+
const retained = withoutCookie(output.cookie, source.name);
|
|
46
|
+
if (retained === undefined)
|
|
47
|
+
delete output.cookie;
|
|
48
|
+
else
|
|
49
|
+
output.cookie = retained;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return output;
|
|
53
|
+
}
|
|
54
|
+
export function readCredential(entry, url, headers) {
|
|
55
|
+
const found = [];
|
|
56
|
+
if (entry.credential === undefined) {
|
|
57
|
+
const authorization = headers.authorization;
|
|
58
|
+
if (authorization === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const matched = /^Bearer[ \t]+([^\s]+)$/iu.exec(authorization.trim());
|
|
61
|
+
if (matched === null)
|
|
62
|
+
invalidRequest('Authorization must contain one Bearer credential.');
|
|
63
|
+
return matched[1];
|
|
64
|
+
}
|
|
65
|
+
for (const source of credentialSources(entry)) {
|
|
66
|
+
let value;
|
|
67
|
+
if (source.in === 'header') {
|
|
68
|
+
value = headers[(source.name ?? 'authorization').toLowerCase()];
|
|
69
|
+
}
|
|
70
|
+
else if (source.in === 'query') {
|
|
71
|
+
const values = url.searchParams.getAll(source.name);
|
|
72
|
+
if (values.length > 1) {
|
|
73
|
+
invalidRequest(`Credential query ${source.name} occurs more than once.`);
|
|
74
|
+
}
|
|
75
|
+
value = values[0];
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
value = cookie(headers.cookie, source.name);
|
|
79
|
+
}
|
|
80
|
+
if (value !== undefined)
|
|
81
|
+
found.push(value);
|
|
82
|
+
}
|
|
83
|
+
if (found.length > 1)
|
|
84
|
+
invalidRequest('Request supplies more than one Astrale credential channel.');
|
|
85
|
+
return found[0];
|
|
86
|
+
}
|
|
87
|
+
function cookie(header, name) {
|
|
88
|
+
if (header === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const values = [];
|
|
91
|
+
for (const item of header.split(';')) {
|
|
92
|
+
const split = item.indexOf('=');
|
|
93
|
+
if (item.slice(0, split).trim() === name) {
|
|
94
|
+
try {
|
|
95
|
+
values.push(decodeURIComponent(item.slice(split + 1).trim()));
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
if (cause instanceof URIError)
|
|
99
|
+
invalidRequest('Credential cookie is invalid.');
|
|
100
|
+
throw cause;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (values.length > 1)
|
|
105
|
+
invalidRequest(`Credential cookie ${name} occurs more than once.`);
|
|
106
|
+
return values[0];
|
|
107
|
+
}
|
|
108
|
+
function withoutCookie(header, name) {
|
|
109
|
+
const retained = header.split(';').filter((item) => {
|
|
110
|
+
const split = item.indexOf('=');
|
|
111
|
+
return split < 0 || item.slice(0, split).trim() !== name;
|
|
112
|
+
});
|
|
113
|
+
const value = retained
|
|
114
|
+
.map((item) => item.trim())
|
|
115
|
+
.filter((item) => item.length > 0)
|
|
116
|
+
.join('; ');
|
|
117
|
+
return value.length === 0 ? undefined : value;
|
|
118
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Domain } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { PublicationRef } from '../publication/index.js';
|
|
3
|
+
import type { HttpRequest, Invocation } from './request.js';
|
|
4
|
+
import type { Route } from './table.js';
|
|
5
|
+
export declare function decode(entry: Route, request: HttpRequest, domain: Domain, served: PublicationRef): Invocation;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { NodePath, Path } from '@astrale-os/kernel-core/path';
|
|
2
|
+
import { callableAddress } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
import * as portable from '@astrale-os/kernel-dsl/value';
|
|
4
|
+
import { readCredential, specificHeaderWires, withoutCredentialHeaders } from './credential.js';
|
|
5
|
+
import { fail, invalidRequest } from './error.js';
|
|
6
|
+
import { isReservedHeader } from './mapping.js';
|
|
7
|
+
import { methodMatches } from './match.js';
|
|
8
|
+
import { bodyText, normalizeHeaders, parseJsonObject } from './request.js';
|
|
9
|
+
import { decodeRouting } from './routing.js';
|
|
10
|
+
import { decodeScalar } from './scalar.js';
|
|
11
|
+
import { requiresReceiver } from './target.js';
|
|
12
|
+
import { matchTemplate, PLACEHOLDER } from './template.js';
|
|
13
|
+
export function decode(entry, request, domain, served) {
|
|
14
|
+
if (!methodMatches(entry, request.method))
|
|
15
|
+
invalidRequest('HTTP method does not match the route.');
|
|
16
|
+
const matched = matchTemplate(entry.href, request.url);
|
|
17
|
+
if (matched === undefined)
|
|
18
|
+
invalidRequest('URL does not match the route template.');
|
|
19
|
+
const values = {};
|
|
20
|
+
for (const [name, value] of Object.entries(matched)) {
|
|
21
|
+
values[name] = decodeScalar(entry, name, value, domain);
|
|
22
|
+
}
|
|
23
|
+
if (entry.requestMethod !== undefined) {
|
|
24
|
+
values[entry.requestMethod] = decodeScalar(entry, entry.requestMethod, request.method.toUpperCase(), domain);
|
|
25
|
+
}
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(request.url);
|
|
29
|
+
}
|
|
30
|
+
catch (cause) {
|
|
31
|
+
if (cause instanceof TypeError) {
|
|
32
|
+
return fail('NATIVE_ROUTE_REQUEST_INVALID', 'Native route URL is invalid.', cause);
|
|
33
|
+
}
|
|
34
|
+
throw cause;
|
|
35
|
+
}
|
|
36
|
+
const headers = normalizeHeaders(request.headers);
|
|
37
|
+
const credential = readCredential(entry, url, headers);
|
|
38
|
+
const routing = decodeRouting(request, served);
|
|
39
|
+
const applicationHeaders = withoutCredentialHeaders(entry, headers);
|
|
40
|
+
for (const [name, wire] of Object.entries(entry.query ?? {})) {
|
|
41
|
+
const found = url.searchParams.getAll(wire);
|
|
42
|
+
if (found.length !== 1)
|
|
43
|
+
invalidRequest(`Query parameter ${wire} must occur exactly once.`);
|
|
44
|
+
values[name] = decodeScalar(entry, name, found[0], domain);
|
|
45
|
+
}
|
|
46
|
+
for (const [name, pick] of Object.entries(entry.headers ?? {})) {
|
|
47
|
+
if (pick === '*') {
|
|
48
|
+
const explicit = specificHeaderWires(entry);
|
|
49
|
+
values[name] = Object.fromEntries(Object.entries(applicationHeaders).filter(([header]) => !explicit.has(header) &&
|
|
50
|
+
header !== 'accept' &&
|
|
51
|
+
!isReservedHeader(header) &&
|
|
52
|
+
(entry.body === undefined || header !== 'content-type')));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const value = headers[pick.toLowerCase()];
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
invalidRequest(`Missing header ${pick}.`);
|
|
58
|
+
values[name] = decodeScalar(entry, name, value, domain);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (entry.body === '*') {
|
|
62
|
+
const body = parseJsonObject(request.body);
|
|
63
|
+
const allowed = remainderFields(entry, domain);
|
|
64
|
+
for (const name of Object.keys(body)) {
|
|
65
|
+
if (!allowed.has(name)) {
|
|
66
|
+
invalidRequest(`JSON body field ${name} is not owned by the route remainder mapping.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
Object.assign(values, body);
|
|
70
|
+
}
|
|
71
|
+
else if (entry.body !== undefined) {
|
|
72
|
+
const needsText = Object.values(entry.body).some((pick) => typeof pick !== 'string');
|
|
73
|
+
const body = needsText ? bodyText(request.body) : parseJsonObject(request.body);
|
|
74
|
+
for (const [name, pick] of Object.entries(entry.body)) {
|
|
75
|
+
if (typeof pick !== 'string') {
|
|
76
|
+
values[name] = body;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const value = body[pick];
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
invalidRequest(`Missing JSON body field ${pick}.`);
|
|
82
|
+
values[name] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let receiver;
|
|
87
|
+
if (requiresReceiver(entry)) {
|
|
88
|
+
const receiverName = entry.target.kind === 'method' && entry.target.dispatch === 'instance'
|
|
89
|
+
? entry.target.receiver
|
|
90
|
+
: undefined;
|
|
91
|
+
const encodedReceiver = receiverName === undefined ? undefined : values[receiverName];
|
|
92
|
+
if (typeof encodedReceiver !== 'string')
|
|
93
|
+
invalidRequest('Instance route receiver is missing.');
|
|
94
|
+
try {
|
|
95
|
+
receiver = NodePath(encodedReceiver);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
if (cause instanceof TypeError) {
|
|
99
|
+
return fail('NATIVE_ROUTE_REQUEST_INVALID', 'Instance route receiver is invalid.', cause);
|
|
100
|
+
}
|
|
101
|
+
throw cause;
|
|
102
|
+
}
|
|
103
|
+
delete values[receiverName];
|
|
104
|
+
}
|
|
105
|
+
let input;
|
|
106
|
+
try {
|
|
107
|
+
input = portable.accept(values);
|
|
108
|
+
}
|
|
109
|
+
catch (cause) {
|
|
110
|
+
if (cause instanceof portable.BoundaryError) {
|
|
111
|
+
return fail('NATIVE_ROUTE_REQUEST_INVALID', 'Native route input is not portable.', cause);
|
|
112
|
+
}
|
|
113
|
+
throw cause;
|
|
114
|
+
}
|
|
115
|
+
const coordinate = callableAddress(entry.callable);
|
|
116
|
+
const target = coordinate.kind === 'method'
|
|
117
|
+
? receiver === undefined
|
|
118
|
+
? Path.staticMethod(Path.project(coordinate.owner), coordinate.name).raw
|
|
119
|
+
: Path.instanceMethod(Path.from(receiver), coordinate.name).raw
|
|
120
|
+
: Path.project({
|
|
121
|
+
origin: coordinate.origin,
|
|
122
|
+
kind: 'function',
|
|
123
|
+
name: coordinate.name,
|
|
124
|
+
}).raw;
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
call: Object.freeze({ target, input }),
|
|
127
|
+
...(credential === undefined ? {} : { credential }),
|
|
128
|
+
...(routing === undefined ? {} : { routing }),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function remainderFields(entry, domain) {
|
|
132
|
+
const claimed = new Set([
|
|
133
|
+
...[...entry.href.matchAll(PLACEHOLDER)].map((item) => item[1]),
|
|
134
|
+
...(entry.requestMethod === undefined ? [] : [entry.requestMethod]),
|
|
135
|
+
...Object.keys(entry.query ?? {}),
|
|
136
|
+
...Object.keys(entry.headers ?? {}),
|
|
137
|
+
]);
|
|
138
|
+
const fields = domain.callable(entry.callable)?.inputFields ?? [];
|
|
139
|
+
const allowed = new Set(fields.filter((name) => !claimed.has(name)));
|
|
140
|
+
const receiver = entry.target.kind === 'method' && entry.target.dispatch === 'instance'
|
|
141
|
+
? entry.target.receiver
|
|
142
|
+
: undefined;
|
|
143
|
+
if (receiver !== undefined && !claimed.has(receiver)) {
|
|
144
|
+
allowed.add(receiver);
|
|
145
|
+
}
|
|
146
|
+
return allowed;
|
|
147
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Path } from '@astrale-os/kernel-core/path';
|
|
2
|
+
import { callableAddress } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
import * as portable from '@astrale-os/kernel-dsl/value';
|
|
4
|
+
import { firstCredentialIsAmbient, putCredential, specificHeaderWires, withoutCredentialHeaders, } from './credential.js';
|
|
5
|
+
import { fail, invalidRequest } from './error.js';
|
|
6
|
+
import { isReservedHeader } from './mapping.js';
|
|
7
|
+
import { encodeStrongEtag, HTTP_ROUTE_ISSUER, HTTP_ROUTE_PUBLICATION } from './routing.js';
|
|
8
|
+
import { asObject, encodeScalar, scalar } from './scalar.js';
|
|
9
|
+
import { requiresReceiver } from './target.js';
|
|
10
|
+
import { authorityEnd, encodeTemplate, PLACEHOLDER } from './template.js';
|
|
11
|
+
export function encode(entry, invocation, domain) {
|
|
12
|
+
const input = asObject(invocation.call.input);
|
|
13
|
+
const target = Path.from(invocation.call.target);
|
|
14
|
+
const needsReceiver = requiresReceiver(entry);
|
|
15
|
+
const coordinate = callableAddress(entry.callable);
|
|
16
|
+
const methodName = coordinate.kind === 'method' ? coordinate.name : undefined;
|
|
17
|
+
const receiver = needsReceiver ? instanceSource(target, methodName) : undefined;
|
|
18
|
+
const source = receiver?.raw;
|
|
19
|
+
const expected = needsReceiver
|
|
20
|
+
? Path.instanceMethod(receiver, methodName).raw
|
|
21
|
+
: coordinate.kind === 'method'
|
|
22
|
+
? Path.staticMethod(Path.project(coordinate.owner), methodName).raw
|
|
23
|
+
: Path.project({
|
|
24
|
+
origin: coordinate.origin,
|
|
25
|
+
kind: 'function',
|
|
26
|
+
name: coordinate.name,
|
|
27
|
+
}).raw;
|
|
28
|
+
if (target.raw !== expected)
|
|
29
|
+
invalidRequest('Invocation Path does not match the selected route.');
|
|
30
|
+
const receiverName = entry.target.kind === 'method' && entry.target.dispatch === 'instance'
|
|
31
|
+
? entry.target.receiver
|
|
32
|
+
: undefined;
|
|
33
|
+
const field = (name) => (name === receiverName ? source : input[name]);
|
|
34
|
+
const wireField = (name) => encodeScalar(entry, name, field(name), domain);
|
|
35
|
+
const hostEnd = authorityEnd(entry.href);
|
|
36
|
+
let url = entry.href.replaceAll(PLACEHOLDER, (_whole, name, catchall, offset) => encodeTemplate(wireField(name), catchall, offset < hostEnd));
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
for (const [name, wire] of Object.entries(entry.query ?? {})) {
|
|
39
|
+
parsed.searchParams.append(wire, wireField(name));
|
|
40
|
+
}
|
|
41
|
+
let headers = {};
|
|
42
|
+
for (const [name, pick] of Object.entries(entry.headers ?? {})) {
|
|
43
|
+
if (pick !== '*')
|
|
44
|
+
headers[pick] = wireField(name);
|
|
45
|
+
}
|
|
46
|
+
const headerRemainder = Object.entries(entry.headers ?? {}).find(([, pick]) => pick === '*');
|
|
47
|
+
if (headerRemainder !== undefined) {
|
|
48
|
+
const explicit = specificHeaderWires(entry);
|
|
49
|
+
for (const [header, headerValue] of Object.entries(asObject(field(headerRemainder[0])))) {
|
|
50
|
+
const canonical = header.toLowerCase();
|
|
51
|
+
if (header !== canonical || explicit.has(canonical) || isReservedHeader(canonical)) {
|
|
52
|
+
invalidRequest('Wildcard header input overlaps an explicit, reserved, or non-canonical header name.');
|
|
53
|
+
}
|
|
54
|
+
headers[canonical] = scalar(headerValue);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let body;
|
|
58
|
+
if (entry.body === '*') {
|
|
59
|
+
const claimed = new Set([
|
|
60
|
+
...[...entry.href.matchAll(PLACEHOLDER)].map((item) => item[1]),
|
|
61
|
+
...(entry.requestMethod === undefined ? [] : [entry.requestMethod]),
|
|
62
|
+
...Object.keys(entry.query ?? {}),
|
|
63
|
+
...Object.keys(entry.headers ?? {}),
|
|
64
|
+
]);
|
|
65
|
+
const output = {};
|
|
66
|
+
for (const [name, value] of Object.entries(input)) {
|
|
67
|
+
if (!claimed.has(name))
|
|
68
|
+
output[name] = value;
|
|
69
|
+
}
|
|
70
|
+
if (receiverName !== undefined && !claimed.has(receiverName)) {
|
|
71
|
+
output[receiverName] = source;
|
|
72
|
+
}
|
|
73
|
+
body = portable.json.serialize(output);
|
|
74
|
+
headers['content-type'] = 'application/json';
|
|
75
|
+
}
|
|
76
|
+
else if (entry.body !== undefined) {
|
|
77
|
+
const output = {};
|
|
78
|
+
for (const [name, pick] of Object.entries(entry.body)) {
|
|
79
|
+
if (typeof pick !== 'string')
|
|
80
|
+
body = scalar(field(name));
|
|
81
|
+
else
|
|
82
|
+
output[pick] = field(name);
|
|
83
|
+
}
|
|
84
|
+
if (body === undefined) {
|
|
85
|
+
body = portable.json.serialize(output);
|
|
86
|
+
headers['content-type'] = 'application/json';
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
headers['content-type'] = 'text/plain; charset=utf-8';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
headers = withoutCredentialHeaders(entry, headers);
|
|
93
|
+
const ambientCredential = firstCredentialIsAmbient(entry);
|
|
94
|
+
if (invocation.credential !== undefined && ambientCredential) {
|
|
95
|
+
invalidRequest('A cookie credential is ambient and cannot be emitted by JavaScript.');
|
|
96
|
+
}
|
|
97
|
+
if (invocation.credential !== undefined) {
|
|
98
|
+
putCredential(entry, parsed, headers, invocation.credential);
|
|
99
|
+
}
|
|
100
|
+
for (const name of Object.keys(headers)) {
|
|
101
|
+
if (isReservedHeader(name)) {
|
|
102
|
+
invalidRequest(`Header ${name} is reserved by the invocation protocol.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (invocation.routing !== undefined) {
|
|
106
|
+
headers[HTTP_ROUTE_PUBLICATION] = encodeStrongEtag(invocation.routing.publication.etag);
|
|
107
|
+
headers[HTTP_ROUTE_ISSUER] = invocation.routing.issuer;
|
|
108
|
+
}
|
|
109
|
+
url = parsed.toString();
|
|
110
|
+
const mappedMethod = entry.requestMethod === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: encodeScalar(entry, entry.requestMethod, input[entry.requestMethod], domain).toUpperCase();
|
|
113
|
+
const requestedMethod = invocation.method?.toUpperCase();
|
|
114
|
+
if (mappedMethod !== undefined &&
|
|
115
|
+
requestedMethod !== undefined &&
|
|
116
|
+
mappedMethod !== requestedMethod) {
|
|
117
|
+
invalidRequest('Native route request method differs from its mapped input field.');
|
|
118
|
+
}
|
|
119
|
+
const methodCandidate = entry.method === '*' ? (requestedMethod ?? mappedMethod) : entry.method;
|
|
120
|
+
if (methodCandidate === undefined) {
|
|
121
|
+
fail('NATIVE_ROUTE_REQUEST_INVALID', 'Wildcard route invocation requires an HTTP method.');
|
|
122
|
+
}
|
|
123
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(methodCandidate)) {
|
|
124
|
+
invalidRequest('Native route request method is unsupported.');
|
|
125
|
+
}
|
|
126
|
+
const method = methodCandidate;
|
|
127
|
+
if (mappedMethod !== undefined && mappedMethod !== method) {
|
|
128
|
+
invalidRequest('Native route input method does not match the declared route method.');
|
|
129
|
+
}
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
url,
|
|
132
|
+
method,
|
|
133
|
+
headers: Object.freeze(headers),
|
|
134
|
+
...(body === undefined ? {} : { body }),
|
|
135
|
+
...(ambientCredential ? { credentials: 'include' } : {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function instanceSource(target, name) {
|
|
139
|
+
const steps = target.ast.steps;
|
|
140
|
+
const last = steps.at(-1);
|
|
141
|
+
if (last?.kind !== 'method' || last.dispatch !== 'instance' || last.name !== name) {
|
|
142
|
+
invalidRequest('Invocation Path is not the selected instance Method.');
|
|
143
|
+
}
|
|
144
|
+
return Path.fromAST({ anchor: target.ast.anchor, steps: steps.slice(0, -1) });
|
|
145
|
+
}
|