@astrale-os/sdk 0.6.0-beta.35 → 0.6.0-beta.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/application/action/context.d.ts +14 -11
- package/dist/application/action/index.d.ts +1 -1
- package/dist/application/dependency/invoker.d.ts +10 -4
- package/dist/application/integration/define.d.ts +8 -1
- package/dist/application/integration/index.d.ts +2 -2
- package/dist/application/integration/integration.d.ts +13 -1
- package/dist/application/integration/operation.d.ts +14 -1
- package/dist/application/mutation/execution/execute.js +1 -1
- package/dist/application/mutation/failure.d.ts +6 -0
- package/dist/application/mutation/failure.js +24 -18
- package/dist/application/query/collection/union.js +11 -4
- package/dist/application/query/conformance.d.ts +15 -0
- package/dist/application/query/conformance.js +46 -0
- package/dist/application/query/define.js +4 -3
- package/dist/application/query/execution/execute.d.ts +0 -1
- package/dist/application/query/execution/execute.js +7 -1
- package/dist/application/query/failure.d.ts +8 -0
- package/dist/application/query/failure.js +8 -0
- package/dist/application/query/index.d.ts +1 -1
- package/dist/application/query/index.js +1 -1
- package/dist/application/workflow/authoring.d.ts +1 -1
- package/dist/application/workflow/context.d.ts +1 -1
- package/dist/application/workflow/durable.d.ts +55 -20
- package/dist/application/workflow/index.d.ts +1 -1
- package/dist/execution/cache/cache.d.ts +17 -0
- package/dist/execution/cache/cache.js +11 -0
- package/dist/execution/cache/index.d.ts +2 -0
- package/dist/execution/cache/index.js +2 -0
- package/dist/execution/cache/memory.d.ts +13 -0
- package/dist/execution/cache/memory.js +48 -0
- package/dist/execution/identity/authentication.js +12 -6
- package/dist/execution/identity/callback.d.ts +7 -8
- package/dist/execution/identity/callback.js +17 -14
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.js +1 -0
- package/dist/execution/invocation/admission/age.d.ts +57 -0
- package/dist/execution/invocation/admission/age.js +208 -0
- package/dist/execution/invocation/admission/authentication.d.ts +2 -0
- package/dist/execution/invocation/admission/authentication.js +6 -2
- package/dist/execution/invocation/admission/index.d.ts +3 -2
- package/dist/execution/invocation/admission/index.js +3 -2
- package/dist/execution/invocation/admission/target.d.ts +11 -0
- package/dist/execution/invocation/admission/target.js +22 -3
- package/dist/execution/invocation/capabilities/bind.d.ts +11 -0
- package/dist/execution/invocation/capabilities/bind.js +25 -0
- package/dist/execution/invocation/capabilities/index.d.ts +1 -0
- package/dist/execution/invocation/capabilities/index.js +1 -0
- package/dist/execution/invocation/dispatch/context.js +2 -21
- package/dist/execution/invocation/handler.js +8 -2
- package/dist/execution/invocation/integration/authentication.d.ts +3 -3
- package/dist/execution/invocation/integration/authentication.js +7 -7
- package/dist/execution/invocation/integration/call.js +2 -2
- package/dist/execution/invocation/integration/index.d.ts +1 -0
- package/dist/execution/invocation/integration/index.js +1 -0
- package/dist/execution/invocation/invocation.d.ts +6 -0
- package/dist/execution/invocation/invocation.js +94 -19
- package/dist/execution/invocation/lifecycle/active.d.ts +8 -2
- package/dist/execution/invocation/lifecycle/active.js +3 -3
- package/dist/execution/invocation/lifecycle/index.d.ts +1 -1
- package/dist/execution/serve.d.ts +8 -0
- package/dist/execution/serve.js +7 -0
- package/dist/execution/service/create.js +2 -0
- package/dist/execution/service/service.d.ts +6 -0
- package/dist/execution/workflows/engine.d.ts +12 -2
- package/dist/execution/workflows/index.d.ts +1 -1
- package/dist/execution/workflows/integrations.d.ts +13 -0
- package/dist/execution/workflows/integrations.js +25 -0
- package/dist/execution/workflows/options.js +3 -1
- package/dist/execution/workflows/receiver.d.ts +3 -0
- package/dist/execution/workflows/receiver.js +7 -2
- package/dist/execution/workflows/run.d.ts +3 -4
- package/dist/execution/workflows/run.js +32 -60
- package/dist/execution/workflows/steps.d.ts +42 -0
- package/dist/execution/workflows/steps.js +67 -0
- package/dist/platform/schema/index.d.ts +1 -1
- package/dist/testing/durable/engine.d.ts +111 -0
- package/dist/testing/durable/engine.js +540 -0
- package/dist/testing/durable/index.d.ts +3 -0
- package/dist/testing/durable/index.js +1 -0
- package/dist/testing/durable/steps.d.ts +30 -0
- package/dist/testing/durable/steps.js +40 -0
- package/dist/testing/index.d.ts +6 -1
- package/dist/testing/index.js +5 -1
- package/dist/tooling/cli/dotenv.d.ts +3 -3
- package/dist/tooling/cli/dotenv.js +3 -3
- package/dist/tooling/cli/log.d.ts +0 -2
- package/dist/tooling/cli/log.js +0 -2
- package/dist/tooling/linter/adapters/typescript/imports.d.ts +0 -1
- package/dist/tooling/linter/adapters/typescript/imports.js +1 -5
- package/dist/tooling/linter/adapters/typescript/index.d.ts +1 -1
- package/dist/tooling/linter/adapters/typescript/index.js +1 -1
- package/dist/tooling/linter/adapters/typescript/syntax.d.ts +3 -3
- package/dist/tooling/linter/adapters/typescript/syntax.js +13 -25
- package/dist/tooling/linter/implementations/source/callable-requirements.d.ts +0 -1
- package/dist/tooling/linter/implementations/source/callable-requirements.js +1 -6
- package/dist/tooling/linter/implementations/source/dependencies.js +12 -1
- package/dist/tooling/linter/implementations/source/mutations.js +1 -12
- package/dist/tooling/linter/implementations/source/queries.js +1 -17
- package/dist/tooling/linter/implementations/source/shared.d.ts +0 -4
- package/dist/tooling/linter/implementations/source/shared.js +1 -33
- package/dist/tooling/linter/implementations/source/workflows.js +5 -15
- package/dist/tooling/linter/policy/generated.js +2 -2
- package/package.json +6 -6
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { callableAddress, Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { admissionInputFields, MAXIMUM_ADMISSION_AGE_SECONDS, } from '@astrale-os/kernel-dsl/v1/language';
|
|
3
|
+
import { Path } from '../../../platform/graph/path/index.js';
|
|
4
|
+
import { target as acceptTarget } from './target.js';
|
|
5
|
+
/**
|
|
6
|
+
* How long reading a kept admission may take before the call asks its Kernel instead. Reads of a
|
|
7
|
+
* shared platform cache answer in single-digit milliseconds; `Function.admit` costs tens.
|
|
8
|
+
*/
|
|
9
|
+
export const ADMISSION_RECALL_TIMEOUT_MS = 30;
|
|
10
|
+
/** Version of the key and entry encodings below; a change makes every kept admission a miss. */
|
|
11
|
+
const ENCODING = 'astrale.admission/1';
|
|
12
|
+
const ENTRY_FIELDS = new Set(['v', 'callable', 'receiver', 'expiresAt']);
|
|
13
|
+
/**
|
|
14
|
+
* The scope of a call whose target names, before the graph is read, implementations of which one
|
|
15
|
+
* declares an admission age. Undefined otherwise: a target that names no local implementation (a
|
|
16
|
+
* callable Node id, a path ending in a traversal), only callables without an age, or a durable
|
|
17
|
+
* Workflow, which runs only as a Kernel Execution. The Path language dispatches exactly, so a named
|
|
18
|
+
* target selects one declaration; the fields are still the union over every implementation named.
|
|
19
|
+
*/
|
|
20
|
+
export function admissionScope(registry, call) {
|
|
21
|
+
const names = declarationsNamedBy(call.target);
|
|
22
|
+
const named = registry.implementations.filter(({ callable }) => names(callable.key));
|
|
23
|
+
if (named.every(({ callable }) => callable.admission === undefined))
|
|
24
|
+
return undefined;
|
|
25
|
+
if (named.some((implementation) => implementation.kind === 'workflow' && implementation.durable)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const fields = new Set();
|
|
29
|
+
for (const { callable } of named) {
|
|
30
|
+
for (const field of admissionInputFields(callable.policy))
|
|
31
|
+
fields.add(field);
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
callables: new Set(named.map(({ callable }) => callable.key)),
|
|
35
|
+
fields: Object.freeze([...fields].sort()),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Whether any implementation of this registry declares an admission age. */
|
|
39
|
+
export function declaresAdmissionAge(registry) {
|
|
40
|
+
return registry.implementations.some(({ callable }) => callable.admission !== undefined);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The cache key of one call's admission: a SHA-256 over the source Kernel, the served release, the
|
|
44
|
+
* verified principal and caller, the canonical call target (receiver included) and the input
|
|
45
|
+
* projected on the fields its Policy reads. A missing field differs from `null`, object keys are
|
|
46
|
+
* sorted at every depth, and no other part of the input enters the key.
|
|
47
|
+
*/
|
|
48
|
+
export async function admissionKey(scope, partition, call) {
|
|
49
|
+
const encoded = JSON.stringify([
|
|
50
|
+
ENCODING,
|
|
51
|
+
partition.kernel,
|
|
52
|
+
partition.release,
|
|
53
|
+
partition.caller?.principal ?? null,
|
|
54
|
+
partition.caller?.identity ?? null,
|
|
55
|
+
canonical(call.target),
|
|
56
|
+
projection(call.input, scope.fields),
|
|
57
|
+
]);
|
|
58
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(encoded));
|
|
59
|
+
return `admission/${hex(new Uint8Array(digest))}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The target of an admission this deployment kept under `key`, while it is younger than the age it
|
|
63
|
+
* carried. A missing, expired, malformed, out-of-scope, failed or late read is a miss.
|
|
64
|
+
*/
|
|
65
|
+
export async function recallAdmission(cache, key, scope) {
|
|
66
|
+
const value = await boundedRead(cache, key);
|
|
67
|
+
return value === undefined ? undefined : keptTarget(value, scope, Date.now());
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Keep a successful admission that carries an age, until `sentAt` plus that age, `sentAt` being when
|
|
71
|
+
* `Function.admit` was asked. Written in the background: the call never waits on the cache, and a
|
|
72
|
+
* cache that fails to keep it only costs a later call its admission round trip.
|
|
73
|
+
*/
|
|
74
|
+
export function keepAdmission(cache, key, scope, admitted, sentAt, background) {
|
|
75
|
+
if (admitted.maxAge === undefined || !scope.callables.has(admitted.target.callable))
|
|
76
|
+
return;
|
|
77
|
+
const expiresAt = sentAt + admitted.maxAge * 1000;
|
|
78
|
+
const value = JSON.stringify({
|
|
79
|
+
v: 1,
|
|
80
|
+
callable: admitted.target.callable,
|
|
81
|
+
...(admitted.target.receiver === undefined ? {} : { receiver: admitted.target.receiver }),
|
|
82
|
+
expiresAt,
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
background(async () => {
|
|
86
|
+
try {
|
|
87
|
+
await cache.put(key, value, expiresAt);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// A cache that cannot keep this admission leaves the next call to its Kernel.
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// A Server that no longer accepts background work keeps nothing; the call is unaffected.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function keptTarget(value, scope, now) {
|
|
99
|
+
let entry;
|
|
100
|
+
try {
|
|
101
|
+
entry = JSON.parse(value);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(entry) || Reflect.ownKeys(entry).some((key) => !ENTRY_FIELDS.has(key))) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const { v, callable, receiver, expiresAt } = entry;
|
|
110
|
+
if (v !== 1 ||
|
|
111
|
+
typeof callable !== 'string' ||
|
|
112
|
+
!scope.callables.has(callable) ||
|
|
113
|
+
typeof expiresAt !== 'number' ||
|
|
114
|
+
!Number.isSafeInteger(expiresAt) ||
|
|
115
|
+
expiresAt <= now ||
|
|
116
|
+
expiresAt > now + MAXIMUM_ADMISSION_AGE_SECONDS * 1000) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return acceptTarget({ callable, ...(receiver === undefined ? {} : { receiver }) });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** One read bounded in time; a cache error is a miss. No pending read is shared across calls. */
|
|
127
|
+
async function boundedRead(cache, key) {
|
|
128
|
+
let timer;
|
|
129
|
+
const read = Promise.resolve()
|
|
130
|
+
.then(() => cache.get(key))
|
|
131
|
+
.then((value) => (typeof value === 'string' ? value : undefined), () => undefined);
|
|
132
|
+
const late = new Promise((resolve) => {
|
|
133
|
+
timer = setTimeout(() => resolve(undefined), ADMISSION_RECALL_TIMEOUT_MS);
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
return await Promise.race([read, late]);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
if (timer !== undefined)
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** The declarations `target` names before the graph is read, as a test on a callable key. */
|
|
144
|
+
function declarationsNamedBy(target) {
|
|
145
|
+
if (typeof target !== 'string')
|
|
146
|
+
return (key) => target.callable === key;
|
|
147
|
+
let path;
|
|
148
|
+
try {
|
|
149
|
+
path = Path.parse(target);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return () => false;
|
|
153
|
+
}
|
|
154
|
+
const { anchor, steps } = path.ast;
|
|
155
|
+
const last = steps.at(-1);
|
|
156
|
+
const first = steps[0];
|
|
157
|
+
return (key) => {
|
|
158
|
+
const coordinate = callableAddress(key);
|
|
159
|
+
if (coordinate.kind === 'method') {
|
|
160
|
+
if (last?.kind !== 'method' || last.name !== coordinate.name)
|
|
161
|
+
return false;
|
|
162
|
+
if (last.dispatch === 'instance')
|
|
163
|
+
return last.class === Key.of(coordinate.owner);
|
|
164
|
+
return (anchor.kind === 'domain' &&
|
|
165
|
+
anchor.origin === coordinate.owner.origin &&
|
|
166
|
+
steps.length === 2 &&
|
|
167
|
+
first?.kind === 'projection' &&
|
|
168
|
+
first.projection.kind === 'class' &&
|
|
169
|
+
first.projection.name === coordinate.owner.name);
|
|
170
|
+
}
|
|
171
|
+
return (anchor.kind === 'domain' &&
|
|
172
|
+
anchor.origin === coordinate.origin &&
|
|
173
|
+
steps.length === 1 &&
|
|
174
|
+
first?.kind === 'projection' &&
|
|
175
|
+
first.projection.kind === 'function' &&
|
|
176
|
+
first.projection.name === coordinate.name);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** The input on the fields a Policy reads: each present field with its value, or its name alone. */
|
|
180
|
+
function projection(input, fields) {
|
|
181
|
+
if (fields.length === 0)
|
|
182
|
+
return [];
|
|
183
|
+
if (!isRecord(input))
|
|
184
|
+
return ['other'];
|
|
185
|
+
return [
|
|
186
|
+
'object',
|
|
187
|
+
...fields.map((field) => Object.hasOwn(input, field) ? [field, canonical(input[field])] : [field]),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
/** Canonical JSON text of a portable value: object keys sorted at every depth. */
|
|
191
|
+
function canonical(value) {
|
|
192
|
+
if (Array.isArray(value))
|
|
193
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
194
|
+
if (value !== null && typeof value === 'object') {
|
|
195
|
+
const object = value;
|
|
196
|
+
return `{${Object.keys(object)
|
|
197
|
+
.sort()
|
|
198
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`)
|
|
199
|
+
.join(',')}}`;
|
|
200
|
+
}
|
|
201
|
+
return JSON.stringify(value);
|
|
202
|
+
}
|
|
203
|
+
function isRecord(input) {
|
|
204
|
+
return input !== null && typeof input === 'object' && !Array.isArray(input);
|
|
205
|
+
}
|
|
206
|
+
function hex(bytes) {
|
|
207
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
208
|
+
}
|
|
@@ -13,3 +13,5 @@ export type InvocationAuthentication = AnonymousInvocation | AuthenticatedInvoca
|
|
|
13
13
|
*/
|
|
14
14
|
export type InvocationSessions = InvocationAuthentication['graph'];
|
|
15
15
|
export declare function authentication(callable: ResolvedCallable, request: HandlerRequest, domain: Domain, authority: InvocationAuthority): Promise<InvocationAuthentication>;
|
|
16
|
+
/** Verify the credential a call carries and admit the caller it proves, whatever the callable. */
|
|
17
|
+
export declare function credentialAuthentication(request: HandlerRequest, domain: Domain, authority: InvocationAuthority): Promise<AuthenticatedInvocation>;
|
|
@@ -5,10 +5,14 @@ import { protocolError } from '../result/index.js';
|
|
|
5
5
|
import { requireVia } from './authority.js';
|
|
6
6
|
const ANONYMOUS = Object.freeze({ kind: 'anonymous' });
|
|
7
7
|
export async function authentication(callable, request, domain, authority) {
|
|
8
|
-
|
|
9
|
-
if (callable.auth === 'anonymous' && credential === undefined) {
|
|
8
|
+
if (callable.auth === 'anonymous' && request.credential === undefined) {
|
|
10
9
|
return anonymous(await authority.self({ via: requireVia(request), domain, context: request.context }));
|
|
11
10
|
}
|
|
11
|
+
return credentialAuthentication(request, domain, authority);
|
|
12
|
+
}
|
|
13
|
+
/** Verify the credential a call carries and admit the caller it proves, whatever the callable. */
|
|
14
|
+
export async function credentialAuthentication(request, domain, authority) {
|
|
15
|
+
const credential = request.credential;
|
|
12
16
|
if (credential === undefined) {
|
|
13
17
|
throw protocolError('AUTH_MISSING', 'Authentication is required.');
|
|
14
18
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { ADMISSION_RECALL_TIMEOUT_MS, admissionKey, admissionScope, declaresAdmissionAge, keepAdmission, recallAdmission, type AdmissionPartition, type AdmissionScope, } from './age.js';
|
|
1
2
|
export { authority, requireVia } from './authority.js';
|
|
2
3
|
export type { AuthenticatedInvocation, InvocationAdmissionInput, InvocationAuthenticationInput, InvocationAuthority, InvocationSelfInput, SelfInvocation, } from './authority.js';
|
|
3
|
-
export { authentication, type AnonymousInvocation, type InvocationAuthentication, type InvocationSessions, } from './authentication.js';
|
|
4
|
+
export { authentication, credentialAuthentication, type AnonymousInvocation, type InvocationAuthentication, type InvocationSessions, } from './authentication.js';
|
|
4
5
|
export { input } from './input.js';
|
|
5
|
-
export { target, type InvocationTarget } from './target.js';
|
|
6
|
+
export { admission, target, type Admission, type InvocationTarget } from './target.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
export { ADMISSION_RECALL_TIMEOUT_MS, admissionKey, admissionScope, declaresAdmissionAge, keepAdmission, recallAdmission, } from './age.js';
|
|
1
2
|
export { authority, requireVia } from './authority.js';
|
|
2
|
-
export { authentication, } from './authentication.js';
|
|
3
|
+
export { authentication, credentialAuthentication, } from './authentication.js';
|
|
3
4
|
export { input } from './input.js';
|
|
4
|
-
export { target } from './target.js';
|
|
5
|
+
export { admission, target } from './target.js';
|
|
@@ -4,5 +4,16 @@ export interface InvocationTarget {
|
|
|
4
4
|
readonly callable: SemanticKey.Callable;
|
|
5
5
|
readonly receiver?: NodePath;
|
|
6
6
|
}
|
|
7
|
+
/** The source Kernel's answer to `Function.admit`. */
|
|
8
|
+
export interface Admission {
|
|
9
|
+
readonly target: InvocationTarget;
|
|
10
|
+
/**
|
|
11
|
+
* How long, in seconds from when it was asked, this deployment may reuse the admission: the
|
|
12
|
+
* callable's declared admission age bounded by the Kernel cap. Absent: this call only.
|
|
13
|
+
*/
|
|
14
|
+
readonly maxAge?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Admit the `Function.admit` answer: the selected target and the admission age it carries. */
|
|
17
|
+
export declare function admission(input: unknown): Admission;
|
|
7
18
|
/** Admit the portable target selected by the execution Identity owner. */
|
|
8
19
|
export declare function target(input: unknown): InvocationTarget;
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { callableAddress, Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { isAdmissionMaxAge } from '@astrale-os/kernel-dsl/v1/language';
|
|
2
3
|
import { NodePath as acceptNodePath } from '../../../platform/graph/path/index.js';
|
|
3
4
|
import { InvocationDefect } from '../result/index.js';
|
|
5
|
+
/** Admit the `Function.admit` answer: the selected target and the admission age it carries. */
|
|
6
|
+
export function admission(input) {
|
|
7
|
+
const value = record(input);
|
|
8
|
+
if (Reflect.ownKeys(value).some((key) => key !== 'callable' && key !== 'receiver' && key !== 'maxAge')) {
|
|
9
|
+
invalid();
|
|
10
|
+
}
|
|
11
|
+
const { maxAge } = value;
|
|
12
|
+
if (maxAge !== undefined && !isAdmissionMaxAge(maxAge))
|
|
13
|
+
invalid();
|
|
14
|
+
const selected = target({
|
|
15
|
+
callable: value.callable,
|
|
16
|
+
...(value.receiver === undefined ? {} : { receiver: value.receiver }),
|
|
17
|
+
});
|
|
18
|
+
return Object.freeze({ target: selected, ...(maxAge === undefined ? {} : { maxAge }) });
|
|
19
|
+
}
|
|
4
20
|
/** Admit the portable target selected by the execution Identity owner. */
|
|
5
21
|
export function target(input) {
|
|
6
|
-
|
|
7
|
-
invalid();
|
|
8
|
-
const value = input;
|
|
22
|
+
const value = record(input);
|
|
9
23
|
if (!Object.hasOwn(value, 'callable') ||
|
|
10
24
|
Reflect.ownKeys(value).some((key) => key !== 'callable' && key !== 'receiver') ||
|
|
11
25
|
typeof value.callable !== 'string') {
|
|
@@ -25,6 +39,11 @@ export function target(input) {
|
|
|
25
39
|
}
|
|
26
40
|
return Object.freeze({ callable, ...(receiver === undefined ? {} : { receiver }) });
|
|
27
41
|
}
|
|
42
|
+
function record(input) {
|
|
43
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
44
|
+
invalid();
|
|
45
|
+
return input;
|
|
46
|
+
}
|
|
28
47
|
function invalid() {
|
|
29
48
|
throw new InvocationDefect('INVOCATION_TARGET_INVALID', 'Invocation target is invalid.');
|
|
30
49
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DomainCapabilities } from '../../../application/action/index.js';
|
|
2
|
+
import type { BoundClientSession } from '../../../platform/client/session/index.js';
|
|
3
|
+
import type { Domain } from '../../../platform/schema/index.js';
|
|
4
|
+
import type { InvocationSessions } from '../admission/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Bind what code acting under `sessions` holds: self-default Kernel and graph, and dependency
|
|
7
|
+
* invokers per authority. An Action and a durable Step receive these same capabilities.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bindCapabilities(domain: Domain, sessions: Readonly<Record<'caller' | 'self', BoundClientSession>>): DomainCapabilities;
|
|
10
|
+
export declare function bindCapabilities(domain: Domain, sessions: Readonly<Record<'self', BoundClientSession>>): DomainCapabilities<Domain, 'self'>;
|
|
11
|
+
export declare function bindCapabilities(domain: Domain, sessions: InvocationSessions): DomainCapabilities | DomainCapabilities<Domain, 'self'>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { bindMutation } from '../../../application/mutation/execution/executor.js';
|
|
2
|
+
import { bindQuery } from '../../../application/query/execution/executor.js';
|
|
3
|
+
import { bindDependencies } from '../dependency/index.js';
|
|
4
|
+
export function bindCapabilities(domain, sessions) {
|
|
5
|
+
const graph = bindGraph(sessions, domain);
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
kernel: Object.freeze({ ...sessions.self, ...sessions }),
|
|
8
|
+
dependencies: bindDependencies(domain, sessions),
|
|
9
|
+
graph,
|
|
10
|
+
query: graph.self.query,
|
|
11
|
+
mutate: graph.self.mutate,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function bindGraph(sessions, domain) {
|
|
15
|
+
const bind = (client) => Object.freeze({
|
|
16
|
+
query: bindQuery(client, domain),
|
|
17
|
+
mutate: bindMutation(client, domain),
|
|
18
|
+
});
|
|
19
|
+
const self = bind(sessions.self);
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
...self,
|
|
22
|
+
...('caller' in sessions ? { caller: bind(sessions.caller) } : {}),
|
|
23
|
+
self,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { bindCapabilities } from './bind.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { bindCapabilities } from './bind.js';
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { bindQuery } from '../../../application/query/execution/executor.js';
|
|
3
|
-
import { bindDependencies } from '../dependency/index.js';
|
|
1
|
+
import { bindCapabilities } from '../capabilities/index.js';
|
|
4
2
|
import { clients } from '../integration/index.js';
|
|
5
3
|
export function context(request, runtime, authentication, self) {
|
|
6
4
|
const sessions = authentication.graph;
|
|
7
|
-
const graph = bindGraph(sessions, runtime.loaded.domain);
|
|
8
5
|
const execution = Object.freeze({
|
|
9
6
|
signal: request.context.execution.signal,
|
|
10
7
|
deadline: request.context.execution.deadline,
|
|
@@ -20,11 +17,7 @@ export function context(request, runtime, authentication, self) {
|
|
|
20
17
|
input: request.call.input,
|
|
21
18
|
...(request.context.binary === undefined ? {} : { body: request.context.binary }),
|
|
22
19
|
caller: authentication.caller,
|
|
23
|
-
|
|
24
|
-
dependencies: bindDependencies(runtime.loaded.domain, sessions),
|
|
25
|
-
graph,
|
|
26
|
-
query: graph.self.query,
|
|
27
|
-
mutate: graph.self.mutate,
|
|
20
|
+
...bindCapabilities(runtime.loaded.domain, sessions),
|
|
28
21
|
integrations: clients(runtime, request.context, {
|
|
29
22
|
...sessions,
|
|
30
23
|
...(authentication.caller.kind !== 'authenticated' ||
|
|
@@ -36,15 +29,3 @@ export function context(request, runtime, authentication, self) {
|
|
|
36
29
|
...(self === undefined ? {} : { self }),
|
|
37
30
|
});
|
|
38
31
|
}
|
|
39
|
-
function bindGraph(sessions, domain) {
|
|
40
|
-
const bind = (client) => Object.freeze({
|
|
41
|
-
query: bindQuery(client, domain),
|
|
42
|
-
mutate: bindMutation(client, domain),
|
|
43
|
-
});
|
|
44
|
-
const self = bind(sessions.self);
|
|
45
|
-
return Object.freeze({
|
|
46
|
-
...self,
|
|
47
|
-
...('caller' in sessions ? { caller: bind(sessions.caller) } : {}),
|
|
48
|
-
self,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInlineWorkflowRunner } from '../../application/workflow/runner/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { deploymentCache } from '../cache/index.js';
|
|
3
|
+
import { authority, declaresAdmissionAge } from './admission/index.js';
|
|
3
4
|
import { invoke } from './invocation.js';
|
|
4
5
|
import { projectError } from './result/index.js';
|
|
5
6
|
/** Compose one ready semantic Handler below Kernel Server transport ownership. */
|
|
@@ -8,7 +9,7 @@ export function createHandler(input) {
|
|
|
8
9
|
invalid();
|
|
9
10
|
if (!Object.hasOwn(input, 'runtime') ||
|
|
10
11
|
!Object.hasOwn(input, 'authority') ||
|
|
11
|
-
Reflect.ownKeys(input).some((key) => key !== 'runtime' && key !== 'authority' && key !== 'workflows')) {
|
|
12
|
+
Reflect.ownKeys(input).some((key) => key !== 'runtime' && key !== 'authority' && key !== 'workflows' && key !== 'cache')) {
|
|
12
13
|
invalid();
|
|
13
14
|
}
|
|
14
15
|
const capturedAuthority = authority(input.authority);
|
|
@@ -17,6 +18,10 @@ export function createHandler(input) {
|
|
|
17
18
|
invalid();
|
|
18
19
|
}
|
|
19
20
|
const capturedWorkflows = Object.freeze({ run: workflows.run.bind(workflows) });
|
|
21
|
+
// A Domain whose callables declare no admission age never reads its cache.
|
|
22
|
+
const cache = input.cache === undefined || !declaresAdmissionAge(input.runtime.loaded.registry)
|
|
23
|
+
? undefined
|
|
24
|
+
: deploymentCache(input.cache);
|
|
20
25
|
return Object.freeze({
|
|
21
26
|
async dispatch(request) {
|
|
22
27
|
try {
|
|
@@ -24,6 +29,7 @@ export function createHandler(input) {
|
|
|
24
29
|
runtime: input.runtime,
|
|
25
30
|
authority: capturedAuthority,
|
|
26
31
|
workflows: capturedWorkflows,
|
|
32
|
+
...(cache === undefined ? {} : { cache }),
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
35
|
catch (cause) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { InvocationContext } from '@astrale-os/kernel-server';
|
|
2
1
|
import type { IntegrationAuthentication } from '../../../application/integration/index.js';
|
|
3
2
|
import type { BoundClientSession } from '../../../platform/client/session/index.js';
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import type { Lifetime } from '../lifecycle/index.js';
|
|
4
|
+
/** Capture only self mint, bounded by its lifetime and the verified credential horizon. */
|
|
5
|
+
export declare function bindIntegrationAuthentication(lifetime: Lifetime, self: BoundClientSession, expiresAt?: number): IntegrationAuthentication;
|
|
@@ -3,15 +3,15 @@ import { active } from '../lifecycle/index.js';
|
|
|
3
3
|
import { protocolError } from '../result/index.js';
|
|
4
4
|
// Match the callback resolver's reserve; a child credential cannot outlive its source.
|
|
5
5
|
const TRANSPORT_RESERVE_SECONDS = 5;
|
|
6
|
-
/** Capture only self mint, bounded by
|
|
7
|
-
export function bindIntegrationAuthentication(
|
|
6
|
+
/** Capture only self mint, bounded by its lifetime and the verified credential horizon. */
|
|
7
|
+
export function bindIntegrationAuthentication(lifetime, self, expiresAt) {
|
|
8
8
|
if (expiresAt !== undefined && !Number.isFinite(expiresAt)) {
|
|
9
9
|
throw new TypeError('Invalid Integration authentication expiry.');
|
|
10
10
|
}
|
|
11
11
|
const mint = self.auth.mint.bind(self.auth);
|
|
12
12
|
return Object.freeze({
|
|
13
13
|
async mint(input) {
|
|
14
|
-
active(
|
|
14
|
+
active(lifetime);
|
|
15
15
|
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
16
16
|
invalidInput();
|
|
17
17
|
const keys = Reflect.ownKeys(input);
|
|
@@ -25,16 +25,16 @@ export function bindIntegrationAuthentication(context, self, expiresAt) {
|
|
|
25
25
|
ttlSeconds < 1 ||
|
|
26
26
|
ttlSeconds > 60)
|
|
27
27
|
invalidInput();
|
|
28
|
-
const horizon = Math.min(
|
|
28
|
+
const horizon = Math.min(lifetime.deadline / 1000, expiresAt ?? Infinity);
|
|
29
29
|
const remaining = Math.floor(horizon) - Math.floor(Date.now() / 1000) - TRANSPORT_RESERVE_SECONDS;
|
|
30
30
|
if (remaining < 1) {
|
|
31
31
|
throw protocolError('DEADLINE_EXCEEDED', 'Insufficient lifetime for Integration credential.');
|
|
32
32
|
}
|
|
33
33
|
const credential = await mint({ audience, ttlSeconds: Math.min(ttlSeconds, remaining) }, {
|
|
34
|
-
signal:
|
|
35
|
-
timeoutMs: Math.max(1,
|
|
34
|
+
signal: lifetime.signal,
|
|
35
|
+
timeoutMs: Math.max(1, lifetime.deadline - Date.now()),
|
|
36
36
|
});
|
|
37
|
-
active(
|
|
37
|
+
active(lifetime);
|
|
38
38
|
// Inspect only the result of the trusted Kernel call, not a Provider-supplied token.
|
|
39
39
|
// The reserve is an optimization; this absolute check is the lifetime boundary.
|
|
40
40
|
const expiry = credentials.inspect(credential).claims.exp;
|
|
@@ -6,11 +6,11 @@ export function calls(context, sessions = null) {
|
|
|
6
6
|
const invoke = sessions?.caller?.invoke.bind(sessions.caller);
|
|
7
7
|
const auth = sessions === null
|
|
8
8
|
? undefined
|
|
9
|
-
: bindIntegrationAuthentication(context, sessions.self, sessions.expiresAt);
|
|
9
|
+
: bindIntegrationAuthentication(context.execution, sessions.self, sessions.expiresAt);
|
|
10
10
|
let sequence = 0;
|
|
11
11
|
return Object.freeze({
|
|
12
12
|
invoke(call) {
|
|
13
|
-
active(context);
|
|
13
|
+
active(context.execution);
|
|
14
14
|
sequence += 1;
|
|
15
15
|
return call.attempt({
|
|
16
16
|
callId: `${prefix}:${sequence}`,
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { HandlerRequest, HandlerResult } from '@astrale-os/kernel-server';
|
|
2
2
|
import type { WorkflowRunner } from '../../application/workflow/runner/index.js';
|
|
3
|
+
import type { DeploymentCache } from '../cache/index.js';
|
|
3
4
|
import type { InitializedRuntime } from '../runtime/index.js';
|
|
4
5
|
import type { InvocationAuthority } from './admission/index.js';
|
|
5
6
|
export interface InvocationInput {
|
|
6
7
|
readonly runtime: InitializedRuntime;
|
|
7
8
|
readonly authority: InvocationAuthority;
|
|
8
9
|
readonly workflows: WorkflowRunner;
|
|
10
|
+
/**
|
|
11
|
+
* Where admissions of callables that declare an admission age are kept for the next calls. Absent:
|
|
12
|
+
* every call is admitted by its source Kernel.
|
|
13
|
+
*/
|
|
14
|
+
readonly cache?: DeploymentCache;
|
|
9
15
|
}
|
|
10
16
|
export declare function invoke(request: HandlerRequest, inputConfig: InvocationInput): Promise<HandlerResult>;
|