@astrale-os/sdk 0.5.0-beta.66 → 0.5.0-beta.68
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/CHANGELOG.md +19 -0
- package/dist/application/mutation/authoring/builder.d.ts +6 -21
- package/dist/application/mutation/authoring/builder.js +4 -65
- package/dist/application/mutation/authoring/transition.d.ts +17 -0
- package/dist/application/mutation/authoring/transition.js +47 -0
- package/dist/application/mutation/define.d.ts +3 -1
- package/dist/application/mutation/execution/execute.js +1 -1
- package/dist/application/mutation/failure.d.ts +10 -2
- package/dist/application/mutation/failure.js +24 -1
- package/dist/application/state/authority.d.ts +3 -0
- package/dist/application/state/authority.js +7 -0
- package/dist/application/state/define.js +4 -1
- package/dist/execution/identity/authentication.d.ts +2 -1
- package/dist/execution/identity/authentication.js +3 -3
- package/dist/execution/identity/exchange.d.ts +2 -3
- package/dist/execution/identity/exchange.js +6 -2
- package/dist/execution/identity/identity.js +4 -1
- package/dist/execution/identity/jwks.d.ts +9 -1
- package/dist/execution/identity/jwks.js +126 -2
- package/dist/platform/schema/index.d.ts +2 -0
- package/dist/platform/schema/index.js +1 -0
- package/dist/platform/schema/state-property/authoring.d.ts +15 -0
- package/dist/platform/schema/state-property/authoring.js +49 -0
- package/dist/platform/schema/state-property/edge.d.ts +16 -0
- package/dist/platform/schema/state-property/edge.js +21 -0
- package/dist/platform/schema/state-property/index.d.ts +4 -0
- package/dist/platform/schema/state-property/index.js +3 -0
- package/dist/platform/schema/state-property/model.d.ts +49 -0
- package/dist/platform/schema/state-property/model.js +1 -0
- package/dist/platform/schema/state-property/registry.d.ts +9 -0
- package/dist/platform/schema/state-property/registry.js +74 -0
- package/dist/tooling/linter/implementations/source/mutations.js +19 -30
- package/dist/tooling/linter/implementations/source/schema.js +89 -6
- package/dist/tooling/linter/implementations/source/state-machine.d.ts +4 -0
- package/dist/tooling/linter/implementations/source/state-machine.js +62 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0-beta.68](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.67...sdk-v0.5.0-beta.68) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Performance Improvements
|
|
7
|
+
|
|
8
|
+
* **execution:** cache issuer verification basis ([#307](https://github.com/astrale-os/sdk/issues/307)) ([a88f227](https://github.com/astrale-os/sdk/commit/a88f22700e61f5c5926621f95fc66638b462fbf5))
|
|
9
|
+
|
|
10
|
+
## [0.5.0-beta.67](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.66...sdk-v0.5.0-beta.67) (2026-08-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* make StateProperty transitions intrinsic ([#306](https://github.com/astrale-os/sdk/issues/306)) ([a2b2575](https://github.com/astrale-os/sdk/commit/a2b2575cf19c585c26539839c4777c0c2d413275))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* **state:** ratify intrinsic property transitions ([#303](https://github.com/astrale-os/sdk/issues/303)) ([d484e58](https://github.com/astrale-os/sdk/commit/d484e58bc47f80c2571886b5c85d91d91da52b25))
|
|
21
|
+
|
|
3
22
|
## [0.5.0-beta.66](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.65...sdk-v0.5.0-beta.66) (2026-08-27)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { EdgeSlug } from '@astrale-os/kernel-core/graph/edge';
|
|
2
2
|
import type { MutationBuilder } from '@astrale-os/kernel-core/graph/mutate';
|
|
3
3
|
import type { QueryEdgeAST, QueryNodeAST } from '@astrale-os/kernel-core/graph/query';
|
|
4
|
-
import type { ResolvedClass } from '../../../platform/schema/index.js';
|
|
5
|
-
import type {
|
|
4
|
+
import type { Domain, ResolvedClass } from '../../../platform/schema/index.js';
|
|
5
|
+
import type { EventOf, StateOf } from '../../state/machine.js';
|
|
6
6
|
import type { CreatedNode, ExistingEndpoint, MutationEndpoint } from './endpoint.js';
|
|
7
7
|
import type { QueryPrecondition } from './precondition.js';
|
|
8
|
-
import type { RichPropertyConditions, RichPropertyDelta, RichPropertyInput
|
|
8
|
+
import type { RichPropertyConditions, RichPropertyDelta, RichPropertyInput } from './properties.js';
|
|
9
|
+
import { type StateMachineOf, type StatePropertyName, type StateTransitionInput } from './transition.js';
|
|
9
10
|
export interface RichMutationBuilder {
|
|
10
11
|
readonly expect: {
|
|
11
12
|
node<Class extends ResolvedClass<'node'>>(input: {
|
|
@@ -29,14 +30,7 @@ export interface RichMutationBuilder {
|
|
|
29
30
|
readonly class: Class;
|
|
30
31
|
readonly props: RichPropertyDelta<Class>;
|
|
31
32
|
}): this;
|
|
32
|
-
transition<
|
|
33
|
-
readonly machine: Machine;
|
|
34
|
-
readonly node: ExistingEndpoint;
|
|
35
|
-
readonly class: Class;
|
|
36
|
-
readonly property: Property;
|
|
37
|
-
readonly decision: AllowedTransitionDecision<Machine>;
|
|
38
|
-
readonly props?: RichPropertyConditions<Class>;
|
|
39
|
-
}): this;
|
|
33
|
+
transition<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>, Machine extends StateMachineOf<Class, Property>, From extends StateOf<Machine>, Event extends EventOf<Machine, From>>(input: StateTransitionInput<Class, Property, Machine, From, Event>): this;
|
|
40
34
|
deleteNode<Class extends ResolvedClass<'node'>>(input: {
|
|
41
35
|
readonly node: ExistingEndpoint;
|
|
42
36
|
readonly class: Class;
|
|
@@ -59,13 +53,4 @@ export interface EdgeSelector<Class extends ResolvedClass<'edge'>> {
|
|
|
59
53
|
readonly target: ExistingEndpoint;
|
|
60
54
|
readonly class: Class;
|
|
61
55
|
}
|
|
62
|
-
export declare function richMutationBuilder(core: MutationBuilder): RichMutationBuilder;
|
|
63
|
-
type StateMachinePropertyName<Class extends ResolvedClass<'node'>, Machine extends StateMachine> = {
|
|
64
|
-
[Name in RichPropertyName<Class>]: string extends StateOf<Machine> ? never : IsExact<Exclude<RichPropertyInput<Class>[Name], undefined>, StateOf<Machine>> extends true ? Name : never;
|
|
65
|
-
}[RichPropertyName<Class>];
|
|
66
|
-
type AllowedTransitionDecision<Machine extends StateMachine> = Readonly<{
|
|
67
|
-
kind: 'allowed';
|
|
68
|
-
transition: TransitionOf<Machine>;
|
|
69
|
-
}>;
|
|
70
|
-
type IsExact<Left, Right> = [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false;
|
|
71
|
-
export {};
|
|
56
|
+
export declare function richMutationBuilder(core: MutationBuilder, domain: Domain): RichMutationBuilder;
|
|
@@ -2,8 +2,9 @@ import { LocalBinding } from '@astrale-os/kernel-core/graph/graph';
|
|
|
2
2
|
import { ClassKey } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
3
|
import { endpoint, existingEndpoint } from './endpoint.js';
|
|
4
4
|
import { precondition } from './precondition.js';
|
|
5
|
-
import { conditions, delta, properties
|
|
6
|
-
|
|
5
|
+
import { conditions, delta, properties } from './properties.js';
|
|
6
|
+
import { authorStateTransition, } from './transition.js';
|
|
7
|
+
export function richMutationBuilder(core, domain) {
|
|
7
8
|
const expectation = Object.freeze({
|
|
8
9
|
node(input) {
|
|
9
10
|
core.expect.node({
|
|
@@ -47,66 +48,7 @@ export function richMutationBuilder(core) {
|
|
|
47
48
|
return rich;
|
|
48
49
|
},
|
|
49
50
|
transition(input) {
|
|
50
|
-
|
|
51
|
-
const kind = submitted.kind;
|
|
52
|
-
const rawTransition = submitted.transition;
|
|
53
|
-
if (kind !== 'allowed' ||
|
|
54
|
-
rawTransition === undefined ||
|
|
55
|
-
rawTransition === null ||
|
|
56
|
-
typeof rawTransition !== 'object') {
|
|
57
|
-
throw new TypeError('StateMachine transition decision must be allowed.');
|
|
58
|
-
}
|
|
59
|
-
const fromState = rawTransition.from;
|
|
60
|
-
const event = rawTransition.event;
|
|
61
|
-
const toState = rawTransition.to;
|
|
62
|
-
if (typeof fromState !== 'string' ||
|
|
63
|
-
typeof event !== 'string' ||
|
|
64
|
-
typeof toState !== 'string') {
|
|
65
|
-
throw new TypeError('StateMachine transition decision must be allowed.');
|
|
66
|
-
}
|
|
67
|
-
const transition = Object.freeze({
|
|
68
|
-
from: fromState,
|
|
69
|
-
event: event,
|
|
70
|
-
to: toState,
|
|
71
|
-
});
|
|
72
|
-
let decision;
|
|
73
|
-
try {
|
|
74
|
-
decision = input.machine.decide(transition.from, transition.event);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
invalidTransition(transition);
|
|
78
|
-
}
|
|
79
|
-
if (decision.kind !== 'allowed' || decision.transition.to !== transition.to) {
|
|
80
|
-
invalidTransition(transition);
|
|
81
|
-
}
|
|
82
|
-
const selected = resolvedProperty(input.class, input.property);
|
|
83
|
-
for (const state of [transition.from, transition.to]) {
|
|
84
|
-
if (selected.validate(state).length > 0) {
|
|
85
|
-
throw new TypeError(`StateMachine-backed property ${String(input.property)} does not admit state ${state}.`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const from = { [input.property]: transition.from };
|
|
89
|
-
const to = { [input.property]: transition.to };
|
|
90
|
-
const suppliedEquals = input.props?.equals;
|
|
91
|
-
if ((suppliedEquals !== undefined &&
|
|
92
|
-
Object.hasOwn(suppliedEquals, input.property) &&
|
|
93
|
-
suppliedEquals[input.property] !== transition.from) ||
|
|
94
|
-
input.props?.absent?.includes(input.property) === true) {
|
|
95
|
-
throw new TypeError(`StateMachine transition conditions conflict with property ${String(input.property)}.`);
|
|
96
|
-
}
|
|
97
|
-
core.expect.node({
|
|
98
|
-
node: existingEndpoint(input.node),
|
|
99
|
-
class: classKey(input.class),
|
|
100
|
-
props: conditions(input.class, {
|
|
101
|
-
...input.props,
|
|
102
|
-
equals: { ...suppliedEquals, ...from },
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
core.updateNode({
|
|
106
|
-
node: existingEndpoint(input.node),
|
|
107
|
-
class: classKey(input.class),
|
|
108
|
-
props: delta(input.class, { set: to }),
|
|
109
|
-
});
|
|
51
|
+
authorStateTransition(core, domain, input);
|
|
110
52
|
return rich;
|
|
111
53
|
},
|
|
112
54
|
deleteNode(input) {
|
|
@@ -158,6 +100,3 @@ export function richMutationBuilder(core) {
|
|
|
158
100
|
function classKey(selected) {
|
|
159
101
|
return ClassKey(selected.key);
|
|
160
102
|
}
|
|
161
|
-
function invalidTransition(transition) {
|
|
162
|
-
throw new TypeError(`StateMachine transition decision is not legal: ${transition.from} + ${transition.event} -> ${transition.to}.`);
|
|
163
|
-
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MutationBuilder } from '@astrale-os/kernel-core/graph/mutate';
|
|
2
|
+
import type { Domain, ResolvedClass } from '../../../platform/schema/index.js';
|
|
3
|
+
import type { StatePropertiesOfClass } from '../../../platform/schema/state-property/index.js';
|
|
4
|
+
import type { EventOf, StateMachine, StateOf } from '../../state/machine.js';
|
|
5
|
+
import type { ExistingEndpoint } from './endpoint.js';
|
|
6
|
+
import type { RichPropertyConditions, RichPropertyName } from './properties.js';
|
|
7
|
+
export type StatePropertyName<Class extends ResolvedClass<'node'>> = Extract<keyof StatePropertiesOfClass<Class>, RichPropertyName<Class>>;
|
|
8
|
+
export type StateMachineOf<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>> = Extract<StatePropertiesOfClass<Class>[Property], StateMachine>;
|
|
9
|
+
export interface StateTransitionInput<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>, Machine extends StateMachineOf<Class, Property>, From extends StateOf<Machine>, Event extends EventOf<Machine, From>> {
|
|
10
|
+
readonly node: ExistingEndpoint;
|
|
11
|
+
readonly class: Class;
|
|
12
|
+
readonly property: Property;
|
|
13
|
+
readonly from: From;
|
|
14
|
+
readonly event: Event;
|
|
15
|
+
readonly props?: RichPropertyConditions<Class>;
|
|
16
|
+
}
|
|
17
|
+
export declare function authorStateTransition<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>, Machine extends StateMachineOf<Class, Property>, From extends StateOf<Machine>, Event extends EventOf<Machine, From>>(core: MutationBuilder, domain: Domain, input: StateTransitionInput<Class, Property, Machine, From, Event>): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ClassKey } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { stateMachineFor } from '../../../platform/schema/state-property/registry.js';
|
|
3
|
+
import { stateTransitionFailure } from '../failure.js';
|
|
4
|
+
import { existingEndpoint } from './endpoint.js';
|
|
5
|
+
import { conditions, delta, resolvedProperty } from './properties.js';
|
|
6
|
+
export function authorStateTransition(core, domain, input) {
|
|
7
|
+
const node = existingEndpoint(input.node);
|
|
8
|
+
const selectedClass = input.class;
|
|
9
|
+
const property = input.property;
|
|
10
|
+
const fromState = input.from;
|
|
11
|
+
const event = input.event;
|
|
12
|
+
const suppliedProps = input.props;
|
|
13
|
+
const equals = suppliedProps?.equals;
|
|
14
|
+
const absent = suppliedProps?.absent;
|
|
15
|
+
const suppliedEquals = equals === undefined ? undefined : { ...equals };
|
|
16
|
+
const suppliedAbsent = absent === undefined ? undefined : [...absent];
|
|
17
|
+
const selected = resolvedProperty(selectedClass, property);
|
|
18
|
+
const selectedClassKey = ClassKey(selectedClass.key);
|
|
19
|
+
const machine = stateMachineFor(domain, selected);
|
|
20
|
+
if (machine === undefined) {
|
|
21
|
+
throw new TypeError(`State transition requires StateProperty ${selectedClassKey}.${String(property)} in the selected Domain.`);
|
|
22
|
+
}
|
|
23
|
+
const decision = machine.decide(fromState, event);
|
|
24
|
+
if (decision.kind === 'rejected')
|
|
25
|
+
throw stateTransitionFailure(decision.from, decision.event);
|
|
26
|
+
const from = { [property]: fromState };
|
|
27
|
+
const to = { [property]: decision.transition.to };
|
|
28
|
+
if ((suppliedEquals !== undefined &&
|
|
29
|
+
Object.hasOwn(suppliedEquals, property) &&
|
|
30
|
+
suppliedEquals[property] !== fromState) ||
|
|
31
|
+
suppliedAbsent?.includes(property) === true) {
|
|
32
|
+
throw new TypeError(`StateMachine transition conditions conflict with property ${String(property)}.`);
|
|
33
|
+
}
|
|
34
|
+
core.expect.node({
|
|
35
|
+
node,
|
|
36
|
+
class: selectedClassKey,
|
|
37
|
+
props: conditions(selectedClass, {
|
|
38
|
+
...(suppliedAbsent === undefined ? {} : { absent: suppliedAbsent }),
|
|
39
|
+
equals: { ...suppliedEquals, ...from },
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
core.updateNode({
|
|
43
|
+
node,
|
|
44
|
+
class: selectedClassKey,
|
|
45
|
+
props: delta(selectedClass, { set: to }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { MutationResult } from '@astrale-os/kernel-core/graph/mutate';
|
|
2
2
|
import type { Domain, schema } from '../../platform/schema/index.js';
|
|
3
|
+
import type { StatefulDomainOf } from '../../platform/schema/state-property/index.js';
|
|
3
4
|
import type { Mutation, MutationDefinition } from './mutation.js';
|
|
4
5
|
type ResolvedDomainOf<Schema extends schema.DomainSchema> = Extract<schema.DomainOf<Schema>, Domain>;
|
|
6
|
+
type AuthoringDomainOf<Schema extends schema.DomainSchema> = StatefulDomainOf<Schema>;
|
|
5
7
|
interface MutationProjector<Schema extends schema.DomainSchema> {
|
|
6
|
-
<Input, Output = MutationResult>(project: (domain:
|
|
8
|
+
<Input, Output = MutationResult>(project: (domain: AuthoringDomainOf<Schema>) => MutationDefinition<Input, Output>): Mutation<ResolvedDomainOf<Schema>, Input, Output>;
|
|
7
9
|
}
|
|
8
10
|
/** Define one inert Mutation recipe projected from its exact loaded Domain. */
|
|
9
11
|
export declare function defineMutation<Schema extends schema.DomainSchema>(): MutationProjector<Schema>;
|
|
@@ -9,7 +9,7 @@ export async function executeMutation(client, domain, definition, input) {
|
|
|
9
9
|
}
|
|
10
10
|
const operation = realizeMutation(definition, domain);
|
|
11
11
|
try {
|
|
12
|
-
const ast = MutationAST.build((builder) => operation.build(input, richMutationBuilder(builder)));
|
|
12
|
+
const ast = MutationAST.build((builder) => operation.build(input, richMutationBuilder(builder, domain)));
|
|
13
13
|
return projectMutation(operation, await client.mutate(ast, { expected: domain.closure }), input);
|
|
14
14
|
}
|
|
15
15
|
catch (cause) {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
readonly code: string
|
|
1
|
+
export interface KernelMutationFailure extends Error {
|
|
2
|
+
readonly code: `MUTATION_${string}`;
|
|
3
3
|
readonly outcomeKnown: boolean;
|
|
4
4
|
}
|
|
5
|
+
export interface StateTransitionFailure extends Error {
|
|
6
|
+
readonly code: 'STATE_TRANSITION_ILLEGAL';
|
|
7
|
+
readonly outcomeKnown: true;
|
|
8
|
+
readonly from: string;
|
|
9
|
+
readonly event: string;
|
|
10
|
+
}
|
|
11
|
+
export type MutationFailure = KernelMutationFailure | StateTransitionFailure;
|
|
12
|
+
export declare function stateTransitionFailure(from: string, event: string): StateTransitionFailure;
|
|
5
13
|
export declare function mutationFailure(input: unknown): MutationFailure | undefined;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
const UNKNOWN_OUTCOME_CODES = new Set(['MUTATION_OUTCOME_UNKNOWN']);
|
|
2
|
+
const localStateTransitionFailures = new WeakSet();
|
|
3
|
+
export function stateTransitionFailure(from, event) {
|
|
4
|
+
const failure = Object.assign(new Error(`State transition is illegal: ${from} + ${event}.`), {
|
|
5
|
+
code: 'STATE_TRANSITION_ILLEGAL',
|
|
6
|
+
outcomeKnown: true,
|
|
7
|
+
from,
|
|
8
|
+
event,
|
|
9
|
+
});
|
|
10
|
+
localStateTransitionFailures.add(failure);
|
|
11
|
+
return failure;
|
|
12
|
+
}
|
|
2
13
|
export function mutationFailure(input) {
|
|
3
14
|
if (!(input instanceof Error))
|
|
4
15
|
return undefined;
|
|
@@ -8,6 +19,15 @@ export function mutationFailure(input) {
|
|
|
8
19
|
const explicit = input.outcomeKnown;
|
|
9
20
|
if (explicit !== undefined && typeof explicit !== 'boolean')
|
|
10
21
|
return undefined;
|
|
22
|
+
if (code === 'STATE_TRANSITION_ILLEGAL') {
|
|
23
|
+
if (!localStateTransitionFailures.has(input))
|
|
24
|
+
return undefined;
|
|
25
|
+
const from = Reflect.get(input, 'from');
|
|
26
|
+
const event = Reflect.get(input, 'event');
|
|
27
|
+
if (typeof from !== 'string' || typeof event !== 'string' || explicit === false)
|
|
28
|
+
return undefined;
|
|
29
|
+
return Object.assign(input, { code, outcomeKnown: true, from, event });
|
|
30
|
+
}
|
|
11
31
|
return Object.assign(input, {
|
|
12
32
|
code,
|
|
13
33
|
outcomeKnown: explicit !== false && !UNKNOWN_OUTCOME_CODES.has(code),
|
|
@@ -15,9 +35,12 @@ export function mutationFailure(input) {
|
|
|
15
35
|
}
|
|
16
36
|
function mutationCode(input) {
|
|
17
37
|
const value = input;
|
|
38
|
+
if (value.code === 'STATE_TRANSITION_ILLEGAL')
|
|
39
|
+
return value.code;
|
|
18
40
|
for (const candidate of [value.code, nestedCode(value.reason), nestedCode(value.data)]) {
|
|
19
|
-
if (typeof candidate === 'string' && candidate.startsWith('MUTATION_'))
|
|
41
|
+
if (typeof candidate === 'string' && candidate.startsWith('MUTATION_')) {
|
|
20
42
|
return candidate;
|
|
43
|
+
}
|
|
21
44
|
}
|
|
22
45
|
return undefined;
|
|
23
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { rememberStateMachine } from './authority.js';
|
|
2
3
|
import { relation } from './relation.js';
|
|
3
4
|
export function stateMachine(input) {
|
|
4
5
|
const admitted = relation(input);
|
|
@@ -10,7 +11,7 @@ export function stateMachine(input) {
|
|
|
10
11
|
const eventSchema = z.enum(events);
|
|
11
12
|
const stateSet = new Set(states);
|
|
12
13
|
const eventSet = new Set(events);
|
|
13
|
-
|
|
14
|
+
const machine = Object.freeze({
|
|
14
15
|
initial: admitted.initial,
|
|
15
16
|
stateSchema,
|
|
16
17
|
eventSchema,
|
|
@@ -40,6 +41,8 @@ export function stateMachine(input) {
|
|
|
40
41
|
});
|
|
41
42
|
},
|
|
42
43
|
});
|
|
44
|
+
rememberStateMachine(machine);
|
|
45
|
+
return machine;
|
|
43
46
|
}
|
|
44
47
|
function admitState(state, states) {
|
|
45
48
|
if (!states.has(state))
|
|
@@ -2,7 +2,7 @@ import type { Fetch } from '@astrale-os/kernel-client';
|
|
|
2
2
|
import type { CredentialInput, Grant, IssuerId } from '../../platform/auth/index.js';
|
|
3
3
|
import type { AuthenticatedInvocation } from '../invocation/index.js';
|
|
4
4
|
import type { InvocationAdmissionInput } from '../invocation/index.js';
|
|
5
|
-
import type { IdentityNetworkPolicy, VerificationBasis } from './jwks.js';
|
|
5
|
+
import type { IdentityNetworkPolicy, VerificationBasis, VerificationBasisResolver } from './jwks.js';
|
|
6
6
|
import type { SigningMaterial } from './signing.js';
|
|
7
7
|
export declare function admit(request: InvocationAdmissionInput, configuration: AuthenticationConfiguration): Promise<unknown>;
|
|
8
8
|
export interface AuthenticationConfiguration {
|
|
@@ -11,6 +11,7 @@ export interface AuthenticationConfiguration {
|
|
|
11
11
|
readonly signing: SigningMaterial;
|
|
12
12
|
readonly fetch: Fetch;
|
|
13
13
|
readonly policy: IdentityNetworkPolicy;
|
|
14
|
+
readonly verification: VerificationBasisResolver;
|
|
14
15
|
}
|
|
15
16
|
export declare function authenticate(credentialInput: CredentialInput, expectedKernel: IssuerId, execution: {
|
|
16
17
|
readonly deadline: number;
|
|
@@ -48,7 +48,7 @@ export async function admit(request, configuration) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
export async function authenticate(credentialInput, expectedKernel, execution, configuration) {
|
|
51
|
-
const established = await establishCaller(credentialInput, configuration.issuer, configuration.
|
|
51
|
+
const established = await establishCaller(credentialInput, configuration.issuer, configuration.verification, execution, configuration.policy);
|
|
52
52
|
if (established.verified.iss !== expectedKernel) {
|
|
53
53
|
throw protocolError('AUTH_INVALID', 'Credential is invalid.');
|
|
54
54
|
}
|
|
@@ -104,10 +104,10 @@ export async function authenticate(credentialInput, expectedKernel, execution, c
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
async function establishCaller(input, audience,
|
|
107
|
+
async function establishCaller(input, audience, verification, execution, policy) {
|
|
108
108
|
requireLive(execution);
|
|
109
109
|
try {
|
|
110
|
-
const context = await verifyCredential(input, audience,
|
|
110
|
+
const context = await verifyCredential(input, audience, verification, execution.signal, policy);
|
|
111
111
|
const carried = delegation.accept(context.verified.claims.delegation);
|
|
112
112
|
if (typeof carried.credential !== 'string') {
|
|
113
113
|
throw new CredentialRejected('Destination carrier has no Kernel authority proof.');
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { Fetch } from '@astrale-os/kernel-client';
|
|
2
1
|
import type { exchange } from '@astrale-os/kernel-server';
|
|
3
2
|
import type { IssuerId } from '../../platform/auth/index.js';
|
|
4
|
-
import type { IdentityNetworkPolicy } from './jwks.js';
|
|
3
|
+
import type { IdentityNetworkPolicy, VerificationBasisResolver } from './jwks.js';
|
|
5
4
|
import type { SigningMaterial } from './signing.js';
|
|
6
5
|
export interface ExchangeOptions {
|
|
7
6
|
readonly maximumTtlSeconds?: number;
|
|
8
7
|
}
|
|
9
8
|
export declare function maximumExchangeTtl(input: ExchangeOptions | undefined): number;
|
|
10
|
-
export declare function tokenExchange(issuer: IssuerId, subject: string, signing: SigningMaterial,
|
|
9
|
+
export declare function tokenExchange(issuer: IssuerId, subject: string, signing: SigningMaterial, policy: IdentityNetworkPolicy, verification: VerificationBasisResolver, maximumTtlSeconds: number): exchange.Provider;
|
|
@@ -22,19 +22,22 @@ export function maximumExchangeTtl(input) {
|
|
|
22
22
|
}
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
|
-
export function tokenExchange(issuer, subject, signing,
|
|
25
|
+
export function tokenExchange(issuer, subject, signing, policy, verification, maximumTtlSeconds) {
|
|
26
26
|
return Object.freeze({
|
|
27
27
|
async exchange(request, context) {
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
context.signal.throwIfAborted();
|
|
30
|
+
const outer = await verifyCredential(request.token, issuer, verification, context.signal, policy);
|
|
30
31
|
requireProfile(outer.verified, 'outer');
|
|
31
32
|
const carried = delegation.accept(outer.verified.claims.delegation);
|
|
32
33
|
if (typeof carried.credential !== 'string')
|
|
33
34
|
invalid();
|
|
35
|
+
context.signal.throwIfAborted();
|
|
34
36
|
const inner = await verifyCredentialAgainst(carried.credential, outer.verified.iss, outer.basis);
|
|
35
37
|
requireProfile(inner.verified, 'inner');
|
|
36
38
|
requireCoherence(outer.verified, inner.verified);
|
|
37
39
|
acceptResolvedDelegation(inner.verified.claims.delegation);
|
|
40
|
+
context.signal.throwIfAborted();
|
|
38
41
|
const now = Math.floor(Date.now() / 1_000);
|
|
39
42
|
const expiresAt = Math.min(outer.verified.exp, inner.verified.exp, now + maximumTtlSeconds);
|
|
40
43
|
if (expiresAt <= now)
|
|
@@ -55,6 +58,7 @@ export function tokenExchange(issuer, subject, signing, fetch, policy, maximumTt
|
|
|
55
58
|
.setIssuedAt(now)
|
|
56
59
|
.setExpirationTime(expiresAt)
|
|
57
60
|
.sign(await signing.privateKey);
|
|
61
|
+
context.signal.throwIfAborted();
|
|
58
62
|
return issuerExchange.response(token, expiresAt);
|
|
59
63
|
}
|
|
60
64
|
catch (cause) {
|
|
@@ -2,6 +2,7 @@ import { admit, authenticate } from './authentication.js';
|
|
|
2
2
|
import { publicationDelivery } from './delivery.js';
|
|
3
3
|
import { httpIssuer } from './errors.js';
|
|
4
4
|
import { maximumExchangeTtl, tokenExchange } from './exchange.js';
|
|
5
|
+
import { createVerificationBasisResolver } from './jwks.js';
|
|
5
6
|
import { signing } from './signing.js';
|
|
6
7
|
/** Create one stateless execution identity backed by an asymmetric private JWK. */
|
|
7
8
|
export function createIdentity(input) {
|
|
@@ -25,16 +26,18 @@ export function createIdentity(input) {
|
|
|
25
26
|
...(input.allowEndpoint === undefined ? {} : { allowEndpoint: input.allowEndpoint }),
|
|
26
27
|
...(input.allowInsecureHttp === true ? { allowInsecureHttp: true } : {}),
|
|
27
28
|
});
|
|
29
|
+
const verification = createVerificationBasisResolver(fetch, policy);
|
|
28
30
|
const configuration = Object.freeze({
|
|
29
31
|
issuer,
|
|
30
32
|
subject: input.subject,
|
|
31
33
|
signing: material,
|
|
32
34
|
fetch,
|
|
33
35
|
policy,
|
|
36
|
+
verification,
|
|
34
37
|
});
|
|
35
38
|
const exchangeProvider = input.tokenExchange === false
|
|
36
39
|
? undefined
|
|
37
|
-
: tokenExchange(issuer, input.subject, material,
|
|
40
|
+
: tokenExchange(issuer, input.subject, material, policy, verification, maximumExchangeTtl(input.tokenExchange));
|
|
38
41
|
return identity({
|
|
39
42
|
issuer,
|
|
40
43
|
subject: input.subject,
|
|
@@ -10,7 +10,15 @@ export interface VerificationBasis {
|
|
|
10
10
|
readonly algorithms: readonly [string, ...string[]];
|
|
11
11
|
readonly keys: readonly JsonWebKey[];
|
|
12
12
|
}
|
|
13
|
-
export
|
|
13
|
+
export interface VerificationBasisResolver {
|
|
14
|
+
resolve(issuer: IssuerId, keyId: string | undefined, signal: AbortSignal): Promise<VerificationBasis>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Retain only recently admitted public issuer material for one execution Identity.
|
|
18
|
+
* An unseen key ID may refresh once per cooldown; failures never replace a usable basis.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createVerificationBasisResolver(fetch: Fetch, policy: IdentityNetworkPolicy, now?: () => number): VerificationBasisResolver;
|
|
21
|
+
export declare function verifyCredential(input: CredentialInput, audience: IssuerId, bases: VerificationBasisResolver, signal: AbortSignal, policy: IdentityNetworkPolicy): Promise<{
|
|
14
22
|
readonly raw: CredentialInput;
|
|
15
23
|
readonly verified: VerifiedCredential;
|
|
16
24
|
readonly basis: VerificationBasis;
|
|
@@ -5,7 +5,94 @@ import { errors as joseErrors, importJWK, jwtVerify } from 'jose';
|
|
|
5
5
|
import { credential } from '../../platform/auth/index.js';
|
|
6
6
|
import { CredentialRejected, IssuerUnavailable, admitEndpoint, httpIssuer } from './errors.js';
|
|
7
7
|
const MAXIMUM_DISCOVERY_BYTES = 256 * 1024;
|
|
8
|
-
|
|
8
|
+
const MAXIMUM_VERIFICATION_BASES = 64;
|
|
9
|
+
const VERIFICATION_BASIS_TTL_MS = 60_000;
|
|
10
|
+
const UNKNOWN_KEY_REFRESH_COOLDOWN_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Retain only recently admitted public issuer material for one execution Identity.
|
|
13
|
+
* An unseen key ID may refresh once per cooldown; failures never replace a usable basis.
|
|
14
|
+
*/
|
|
15
|
+
export function createVerificationBasisResolver(fetch, policy, now = Date.now) {
|
|
16
|
+
const bases = new Map();
|
|
17
|
+
const pending = new Map();
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
async resolve(issuer, keyId, signal) {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
const cached = bases.get(issuer);
|
|
22
|
+
const observedAt = now();
|
|
23
|
+
const age = cached === undefined ? Number.POSITIVE_INFINITY : observedAt - cached.refreshedAt;
|
|
24
|
+
const fresh = cached !== undefined && age >= 0 && age < VERIFICATION_BASIS_TTL_MS;
|
|
25
|
+
const knownKey = keyId === undefined ||
|
|
26
|
+
cached?.basis.keys.some((candidate) => candidate.kid === keyId) === true;
|
|
27
|
+
const unknownKeyCooldown = cached?.unknownKeyRefreshedAt !== undefined &&
|
|
28
|
+
observedAt - cached.unknownKeyRefreshedAt >= 0 &&
|
|
29
|
+
observedAt - cached.unknownKeyRefreshedAt < UNKNOWN_KEY_REFRESH_COOLDOWN_MS;
|
|
30
|
+
if (fresh && knownKey) {
|
|
31
|
+
bases.delete(issuer);
|
|
32
|
+
bases.set(issuer, cached);
|
|
33
|
+
return cached.basis;
|
|
34
|
+
}
|
|
35
|
+
const unknownKeyRefresh = cached !== undefined && !knownKey;
|
|
36
|
+
let refresh = pending.get(issuer);
|
|
37
|
+
if (refresh === undefined && fresh && unknownKeyCooldown) {
|
|
38
|
+
bases.delete(issuer);
|
|
39
|
+
bases.set(issuer, cached);
|
|
40
|
+
return cached.basis;
|
|
41
|
+
}
|
|
42
|
+
if (refresh === undefined && unknownKeyRefresh && cached !== undefined) {
|
|
43
|
+
bases.set(issuer, Object.freeze({ ...cached, unknownKeyRefreshedAt: observedAt }));
|
|
44
|
+
}
|
|
45
|
+
if (refresh === undefined) {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
refresh = {
|
|
48
|
+
controller,
|
|
49
|
+
operation: undefined,
|
|
50
|
+
subscribers: 0,
|
|
51
|
+
settled: false,
|
|
52
|
+
unknownKeyRefresh,
|
|
53
|
+
requestedKeyIds: new Set(keyId === undefined ? [] : [keyId]),
|
|
54
|
+
};
|
|
55
|
+
const owned = refresh;
|
|
56
|
+
owned.operation = discover(issuer, fetch, controller.signal, policy)
|
|
57
|
+
.then((basis) => {
|
|
58
|
+
controller.signal.throwIfAborted();
|
|
59
|
+
const refreshedAt = now();
|
|
60
|
+
if (bases.size >= MAXIMUM_VERIFICATION_BASES && !bases.has(issuer)) {
|
|
61
|
+
const oldest = bases.keys().next().value;
|
|
62
|
+
if (oldest !== undefined)
|
|
63
|
+
bases.delete(oldest);
|
|
64
|
+
}
|
|
65
|
+
bases.delete(issuer);
|
|
66
|
+
bases.set(issuer, Object.freeze({
|
|
67
|
+
basis,
|
|
68
|
+
refreshedAt,
|
|
69
|
+
...(owned.unknownKeyRefresh ||
|
|
70
|
+
[...owned.requestedKeyIds].some((requested) => !basis.keys.some((candidate) => candidate.kid === requested))
|
|
71
|
+
? { unknownKeyRefreshedAt: refreshedAt }
|
|
72
|
+
: {}),
|
|
73
|
+
}));
|
|
74
|
+
return basis;
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
owned.settled = true;
|
|
78
|
+
if (pending.get(issuer) === owned)
|
|
79
|
+
pending.delete(issuer);
|
|
80
|
+
});
|
|
81
|
+
pending.set(issuer, owned);
|
|
82
|
+
}
|
|
83
|
+
else if (unknownKeyRefresh) {
|
|
84
|
+
refresh.unknownKeyRefresh = true;
|
|
85
|
+
}
|
|
86
|
+
if (keyId !== undefined)
|
|
87
|
+
refresh.requestedKeyIds.add(keyId);
|
|
88
|
+
const basis = await subscribe(refresh, issuer, pending, signal);
|
|
89
|
+
signal.throwIfAborted();
|
|
90
|
+
return basis;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export async function verifyCredential(input, audience, bases, signal, policy) {
|
|
95
|
+
signal.throwIfAborted();
|
|
9
96
|
let inspected;
|
|
10
97
|
try {
|
|
11
98
|
inspected = credential.inspect(input);
|
|
@@ -22,11 +109,48 @@ export async function verifyCredential(input, audience, fetch, signal, policy) {
|
|
|
22
109
|
catch (cause) {
|
|
23
110
|
throw new CredentialRejected('Credential issuer is invalid.', cause);
|
|
24
111
|
}
|
|
25
|
-
const basis = await
|
|
112
|
+
const basis = await bases.resolve(admittedIssuer, inspected.kid, signal);
|
|
113
|
+
signal.throwIfAborted();
|
|
26
114
|
const verified = await verifyCredentialAgainst(input, audience, basis, inspected);
|
|
115
|
+
signal.throwIfAborted();
|
|
27
116
|
admitEndpoint(`${admittedIssuer.replace(/\/+$/u, '')}/invoke`, policy);
|
|
28
117
|
return Object.freeze({ raw: input, verified: verified.verified, basis });
|
|
29
118
|
}
|
|
119
|
+
function subscribe(refresh, issuer, pending, signal) {
|
|
120
|
+
signal.throwIfAborted();
|
|
121
|
+
refresh.subscribers += 1;
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
let complete = false;
|
|
124
|
+
const finish = () => {
|
|
125
|
+
if (complete)
|
|
126
|
+
return false;
|
|
127
|
+
complete = true;
|
|
128
|
+
signal.removeEventListener('abort', cancel);
|
|
129
|
+
refresh.subscribers -= 1;
|
|
130
|
+
if (!refresh.settled && refresh.subscribers === 0) {
|
|
131
|
+
if (pending.get(issuer) === refresh)
|
|
132
|
+
pending.delete(issuer);
|
|
133
|
+
refresh.controller.abort(new DOMException('Issuer discovery was cancelled.', 'AbortError'));
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
const cancel = () => {
|
|
138
|
+
if (!finish())
|
|
139
|
+
return;
|
|
140
|
+
reject(signal.reason);
|
|
141
|
+
};
|
|
142
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
143
|
+
refresh.operation.then((basis) => {
|
|
144
|
+
if (!finish())
|
|
145
|
+
return;
|
|
146
|
+
resolve(basis);
|
|
147
|
+
}, (cause) => {
|
|
148
|
+
if (!finish())
|
|
149
|
+
return;
|
|
150
|
+
reject(cause);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
30
154
|
export async function verifyCredentialAgainst(input, audience, basis, preinspected) {
|
|
31
155
|
let inspected;
|
|
32
156
|
try {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { KernelSchema as CoreKernelSchema } from '@astrale-os/kernel-core/schema';
|
|
2
|
+
export { defineSchema, edgeClass, nodeClass, stateProperty } from './state-property/index.js';
|
|
3
|
+
export type { StateClass, StateProperty, StateSchema } from './state-property/index.js';
|
|
2
4
|
export * from '@astrale-os/kernel-dsl/v1';
|
|
3
5
|
export { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
4
6
|
export type { CallablePolicyBuilder, CallablePolicyContext, CallableProjection, AuthoredMethodProjection, AuthoredPropertyProjection, ClassIdentityProjection, ClassOwnerProjection, CoreEdge, CoreEndpoint, CoreNode, DefinedSchema, DirectedEdgeClass, ClassReferenceProjection, ClassProjection, FunctionProjection, EdgeClass, Function, Method, MethodProjection, Lazy, NodeClass, Policy, Property, PropertyProjection, ResolvedDomainOfInput, View, } from '@astrale-os/kernel-dsl/v1/builder';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { KernelSchema as CoreKernelSchema } from '@astrale-os/kernel-core/schema';
|
|
2
2
|
/** Canonical Schema authoring, compilation, loading, and resolved Domain surface. */
|
|
3
3
|
import { schema as language } from '@astrale-os/kernel-dsl/v1';
|
|
4
|
+
export { defineSchema, edgeClass, nodeClass, stateProperty } from './state-property/index.js';
|
|
4
5
|
export * from '@astrale-os/kernel-dsl/v1';
|
|
5
6
|
export { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
6
7
|
/** Kernel-owned Schema values used by authored product Schemas and graph operations. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DefinedSchema, NodeClassInput, PropertyOptions, ResolvedDomainOfInput, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
|
|
2
|
+
import { nodeClass as languageNodeClass } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
+
import type { StateMachine } from '../../../application/state/machine.js';
|
|
4
|
+
import type { StateClass, StateClassesOfInput, StateDependenciesOfInput, StatePropertiesOfConfig, StateProperty, StateSchema } from './model.js';
|
|
5
|
+
interface StatePropertyOptions {
|
|
6
|
+
readonly visibility?: PropertyOptions['visibility'];
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
}
|
|
9
|
+
type RequiredStatePropertyOptions<Options extends StatePropertyOptions> = Readonly<Options & {
|
|
10
|
+
readonly required: true;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function stateProperty<const Machine extends StateMachine, const Options extends StatePropertyOptions = Record<never, never>>(machine: Machine, options?: Options & Record<Exclude<keyof Options, keyof StatePropertyOptions>, never>): StateProperty<Machine, RequiredStatePropertyOptions<Options>>;
|
|
13
|
+
export declare function nodeClass<const Config extends NodeClassInput = Record<never, never>>(config?: Config & Record<Exclude<keyof Config, keyof NodeClassInput>, never>): ReturnType<typeof languageNodeClass<Config>> & StateClass<StatePropertiesOfConfig<Config>>;
|
|
14
|
+
export declare function defineSchema<const Origin extends string, const Input extends DomainSchemaInput>(origin: Origin, input: Input): DefinedSchema<Origin, ResolvedDomainOfInput<Origin, Input>> & StateSchema<StateClassesOfInput<Input>, StateDependenciesOfInput<Input>>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineSchema as defineLanguageSchema, nodeClass as languageNodeClass, property, schema, } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import { isStateMachine } from '../../../application/state/authority.js';
|
|
3
|
+
import { rememberStateClass, rememberStateProperty, rememberStateSchema } from './registry.js';
|
|
4
|
+
export function stateProperty(machine, options) {
|
|
5
|
+
if (!isStateMachine(machine))
|
|
6
|
+
throw new TypeError('stateProperty requires an SDK StateMachine.');
|
|
7
|
+
if (options !== undefined &&
|
|
8
|
+
(typeof options !== 'object' ||
|
|
9
|
+
options === null ||
|
|
10
|
+
Array.isArray(options) ||
|
|
11
|
+
Reflect.ownKeys(options).some((key) => key !== 'visibility' && key !== 'description'))) {
|
|
12
|
+
throw new TypeError('stateProperty options are visibility and description only.');
|
|
13
|
+
}
|
|
14
|
+
const value = property(machine.stateSchema, {
|
|
15
|
+
...options,
|
|
16
|
+
required: true,
|
|
17
|
+
});
|
|
18
|
+
rememberStateProperty(value, machine);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function nodeClass(config) {
|
|
22
|
+
const captured = captureNodeClassInput(config);
|
|
23
|
+
const value = languageNodeClass(captured);
|
|
24
|
+
rememberStateClass(value, captured?.properties);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function defineSchema(origin, input) {
|
|
28
|
+
const captured = captureSchemaInput(input);
|
|
29
|
+
const source = defineLanguageSchema(origin, captured);
|
|
30
|
+
rememberStateSchema(source, captured, schema.resolve(source));
|
|
31
|
+
return source;
|
|
32
|
+
}
|
|
33
|
+
function captureNodeClassInput(config) {
|
|
34
|
+
if (config === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const captured = { ...config };
|
|
37
|
+
return {
|
|
38
|
+
...captured,
|
|
39
|
+
...(captured.properties === undefined ? {} : { properties: { ...captured.properties } }),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function captureSchemaInput(input) {
|
|
43
|
+
const captured = { ...input };
|
|
44
|
+
return {
|
|
45
|
+
...captured,
|
|
46
|
+
...(captured.dependencies === undefined ? {} : { dependencies: { ...captured.dependencies } }),
|
|
47
|
+
...(captured.classes === undefined ? {} : { classes: { ...captured.classes } }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DirectedEdgeInput, UndirectedEdgeInput } from '@astrale-os/kernel-dsl/v1/builder';
|
|
2
|
+
import { edgeClass as languageEdgeClass } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
+
import type { StateProperty } from './model.js';
|
|
4
|
+
interface EdgeClassAuthoring {
|
|
5
|
+
directed<const Input extends Omit<DirectedEdgeInput, 'orientation'>>(input: Parameters<typeof languageEdgeClass.directed<Input>>[0] & WithoutStateProperties<Input>): ReturnType<typeof languageEdgeClass.directed<Input>>;
|
|
6
|
+
undirected<const Input extends Omit<UndirectedEdgeInput, 'orientation'>>(input: Parameters<typeof languageEdgeClass.undirected<Input>>[0] & WithoutStateProperties<Input>): ReturnType<typeof languageEdgeClass.undirected<Input>>;
|
|
7
|
+
}
|
|
8
|
+
type WithoutStateProperties<Input> = Input extends {
|
|
9
|
+
readonly properties?: infer Properties extends Readonly<Record<string, unknown>>;
|
|
10
|
+
} ? {
|
|
11
|
+
readonly properties?: {
|
|
12
|
+
readonly [Name in keyof Properties]: Properties[Name] extends StateProperty ? never : Properties[Name];
|
|
13
|
+
};
|
|
14
|
+
} : unknown;
|
|
15
|
+
export declare const edgeClass: Readonly<EdgeClassAuthoring>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { edgeClass as languageEdgeClass } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import { isStateProperty } from './registry.js';
|
|
3
|
+
export const edgeClass = Object.freeze({
|
|
4
|
+
directed(input) {
|
|
5
|
+
return languageEdgeClass.directed(captureEdgeInput(input));
|
|
6
|
+
},
|
|
7
|
+
undirected(input) {
|
|
8
|
+
return languageEdgeClass.undirected(captureEdgeInput(input));
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
function captureEdgeInput(input) {
|
|
12
|
+
const captured = { ...input };
|
|
13
|
+
const properties = captured.properties === undefined ? undefined : { ...captured.properties };
|
|
14
|
+
if (Object.values(properties ?? {}).some(isStateProperty)) {
|
|
15
|
+
throw new TypeError('StateProperty is supported only on Node Classes.');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
...captured,
|
|
19
|
+
...(properties === undefined ? {} : { properties }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { defineSchema, nodeClass, stateProperty } from './authoring.js';
|
|
2
|
+
export { edgeClass } from './edge.js';
|
|
3
|
+
export type { StateClass, StatePropertiesOfClass, StateProperty, StateSchema, StatefulDomainOf, } from './model.js';
|
|
4
|
+
export { stateMachineFor } from './registry.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Domain, ResolvedClass } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { NodeClassInput, Property, PropertyOptions, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
|
|
3
|
+
import type * as languageSchema from '@astrale-os/kernel-dsl/v1/schema';
|
|
4
|
+
import type { StateMachine } from '../../../application/state/machine.js';
|
|
5
|
+
declare const STATE_PROPERTY_MACHINE: unique symbol;
|
|
6
|
+
declare const STATE_CLASS_PROPERTIES: unique symbol;
|
|
7
|
+
declare const STATE_SCHEMA_CLASSES: unique symbol;
|
|
8
|
+
export interface StateProperty<Machine extends StateMachine = StateMachine, Options extends PropertyOptions = PropertyOptions> extends Property<Machine['stateSchema'], Options> {
|
|
9
|
+
readonly [STATE_PROPERTY_MACHINE]: Machine;
|
|
10
|
+
}
|
|
11
|
+
export interface StateClass<Properties extends Readonly<Record<string, StateMachine>>> {
|
|
12
|
+
readonly [STATE_CLASS_PROPERTIES]: Properties;
|
|
13
|
+
}
|
|
14
|
+
export interface StateSchema<Classes extends Readonly<Record<string, Readonly<Record<string, StateMachine>>>>, Dependencies extends Readonly<Record<string, languageSchema.DomainSchema>> = Readonly<Record<never, never>>> {
|
|
15
|
+
readonly [STATE_SCHEMA_CLASSES]: {
|
|
16
|
+
readonly classes: Classes;
|
|
17
|
+
readonly dependencies: Dependencies;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export type StatePropertiesOfConfig<Config extends NodeClassInput> = Merge<ParentStateProperties<Config>, OwnStateProperties<Config>>;
|
|
21
|
+
export type StateClassesOfInput<Input extends DomainSchemaInput> = Input extends {
|
|
22
|
+
readonly classes?: infer Classes extends Readonly<Record<string, unknown>>;
|
|
23
|
+
} ? {
|
|
24
|
+
readonly [Name in keyof Classes]: Classes[Name] extends StateClass<infer Properties> ? Properties : Readonly<Record<never, never>>;
|
|
25
|
+
} : Readonly<Record<never, never>>;
|
|
26
|
+
export type StateDependenciesOfInput<Input extends DomainSchemaInput> = Input extends {
|
|
27
|
+
readonly dependencies?: infer Dependencies extends Readonly<Record<string, languageSchema.DomainSchema>>;
|
|
28
|
+
} ? Dependencies : Readonly<Record<never, never>>;
|
|
29
|
+
export type StatefulDomainOf<Schema extends languageSchema.DomainSchema> = Schema extends StateSchema<infer Classes, infer Dependencies> ? Extract<languageSchema.DomainOf<Schema>, Domain> extends infer DomainValue extends Domain ? DomainValue & {
|
|
30
|
+
readonly classes: {
|
|
31
|
+
readonly [Name in keyof DomainValue['classes']]: DomainValue['classes'][Name] & StateClass<Name extends keyof Classes ? Extract<Classes[Name], Readonly<Record<string, StateMachine>>> : Readonly<Record<never, never>>>;
|
|
32
|
+
};
|
|
33
|
+
readonly dependencies: {
|
|
34
|
+
readonly [Name in keyof DomainValue['dependencies']]: Name extends keyof Dependencies ? Dependencies[Name] extends languageSchema.DomainSchema ? StatefulDomainOf<Dependencies[Name]> : DomainValue['dependencies'][Name] : DomainValue['dependencies'][Name];
|
|
35
|
+
};
|
|
36
|
+
} : never : Extract<languageSchema.DomainOf<Schema>, Domain>;
|
|
37
|
+
export type StatePropertiesOfClass<Class extends ResolvedClass<'node'>> = Class extends StateClass<infer Properties> ? Properties : Readonly<Record<never, never>>;
|
|
38
|
+
type OwnStateProperties<Config extends NodeClassInput> = Config extends {
|
|
39
|
+
readonly properties?: infer Properties extends Readonly<Record<string, unknown>>;
|
|
40
|
+
} ? {
|
|
41
|
+
readonly [Name in keyof Properties as Properties[Name] extends StateProperty ? Name : never]: Properties[Name] extends StateProperty<infer Machine> ? Machine : never;
|
|
42
|
+
} : Readonly<Record<never, never>>;
|
|
43
|
+
type ParentStateProperties<Config extends NodeClassInput> = Config extends {
|
|
44
|
+
readonly extends?: readonly (infer Parent)[];
|
|
45
|
+
} ? UnionToIntersection<StatePropertiesOfReference<Parent>> : Readonly<Record<never, never>>;
|
|
46
|
+
type StatePropertiesOfReference<Value, Depth extends readonly unknown[] = []> = Depth['length'] extends 32 ? Readonly<Record<never, never>> : Value extends () => infer Resolved ? StatePropertiesOfReference<Resolved, readonly [...Depth, unknown]> : Value extends StateClass<infer Properties> ? Properties : Readonly<Record<never, never>>;
|
|
47
|
+
type UnionToIntersection<Value> = (Value extends unknown ? (input: Value) => void : never) extends (input: infer Intersection) => void ? Intersection : never;
|
|
48
|
+
type Merge<Parent, Own> = Readonly<Omit<Parent, keyof Own> & Own>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Domain, ResolvedProperty } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { Property, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
|
|
3
|
+
import type * as languageSchema from '@astrale-os/kernel-dsl/v1/schema';
|
|
4
|
+
import type { StateMachine } from '../../../application/state/machine.js';
|
|
5
|
+
export declare function rememberStateProperty(property: Property, machine: StateMachine): void;
|
|
6
|
+
export declare function isStateProperty(value: unknown): boolean;
|
|
7
|
+
export declare function rememberStateClass(selected: object, properties?: Readonly<Record<string, unknown>>): void;
|
|
8
|
+
export declare function rememberStateSchema(source: languageSchema.DomainSchema, input: DomainSchemaInput, domain: Domain): void;
|
|
9
|
+
export declare function stateMachineFor(domain: Domain, selected: ResolvedProperty<unknown>): StateMachine | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const propertyMachines = new WeakMap();
|
|
2
|
+
const classProperties = new WeakMap();
|
|
3
|
+
const schemaProperties = new WeakMap();
|
|
4
|
+
export function rememberStateProperty(property, machine) {
|
|
5
|
+
propertyMachines.set(property, machine);
|
|
6
|
+
}
|
|
7
|
+
export function isStateProperty(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && propertyMachines.has(value);
|
|
9
|
+
}
|
|
10
|
+
export function rememberStateClass(selected, properties) {
|
|
11
|
+
const machines = new Map();
|
|
12
|
+
for (const [name, property] of Object.entries(properties ?? {})) {
|
|
13
|
+
if (typeof property !== 'object' || property === null)
|
|
14
|
+
continue;
|
|
15
|
+
const machine = propertyMachines.get(property);
|
|
16
|
+
if (machine !== undefined)
|
|
17
|
+
machines.set(name, machine);
|
|
18
|
+
}
|
|
19
|
+
classProperties.set(selected, machines);
|
|
20
|
+
}
|
|
21
|
+
export function rememberStateSchema(source, input, domain) {
|
|
22
|
+
const byKey = new Map();
|
|
23
|
+
for (const dependency of Object.values(input.dependencies ?? {})) {
|
|
24
|
+
const associated = schemaProperties.get(dependency);
|
|
25
|
+
if (associated === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
for (const [key, machine] of associated.byKey)
|
|
28
|
+
bindKey(byKey, key, machine);
|
|
29
|
+
}
|
|
30
|
+
for (const [className, authored] of Object.entries(input.classes ?? {})) {
|
|
31
|
+
const machines = classProperties.get(authored);
|
|
32
|
+
if (machines === undefined || machines.size === 0)
|
|
33
|
+
continue;
|
|
34
|
+
const resolved = Reflect.get(domain.classes, className);
|
|
35
|
+
if (resolved === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
for (const [propertyName, machine] of machines) {
|
|
38
|
+
const property = Reflect.get(resolved.self.properties, propertyName);
|
|
39
|
+
if (property !== undefined)
|
|
40
|
+
bindKey(byKey, property.key, machine);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const byProperty = new WeakMap();
|
|
44
|
+
bindDomainProperties(domain, byKey, byProperty, new Set());
|
|
45
|
+
schemaProperties.set(source, { byKey, byProperty });
|
|
46
|
+
}
|
|
47
|
+
export function stateMachineFor(domain, selected) {
|
|
48
|
+
const source = Reflect.get(domain, 'source');
|
|
49
|
+
if (typeof source !== 'object' || source === null)
|
|
50
|
+
return undefined;
|
|
51
|
+
return schemaProperties.get(source)?.byProperty.get(selected);
|
|
52
|
+
}
|
|
53
|
+
function bindKey(byKey, key, machine) {
|
|
54
|
+
const existing = byKey.get(key);
|
|
55
|
+
if (existing !== undefined && existing !== machine) {
|
|
56
|
+
throw new TypeError(`StateProperty ${key} has more than one StateMachine authority.`);
|
|
57
|
+
}
|
|
58
|
+
byKey.set(key, machine);
|
|
59
|
+
}
|
|
60
|
+
function bindDomainProperties(domain, byKey, byProperty, seen) {
|
|
61
|
+
if (seen.has(domain))
|
|
62
|
+
return;
|
|
63
|
+
seen.add(domain);
|
|
64
|
+
for (const definition of domain.definitions('class')) {
|
|
65
|
+
for (const property of definition.self.properties) {
|
|
66
|
+
const machine = byKey.get(property.key);
|
|
67
|
+
if (machine !== undefined)
|
|
68
|
+
byProperty.set(property, machine);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const dependency of Object.values(domain.dependencies)) {
|
|
72
|
+
bindDomainProperties(dependency, byKey, byProperty, seen);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { importedSymbol, propertyChain, staticText, unwrap, visit, } from '../../adapters/typescript/index.js';
|
|
3
3
|
import { ambiguity, callback, DSL_MODULES, definitionOriginAmbiguity, definitionObjects, domainProjectorEvidence, forbiddenIoImport, hasForbiddenGlobalCall, production, violation, } from './shared.js';
|
|
4
|
-
import { stateMachineIdentity,
|
|
4
|
+
import { stateMachineIdentity, statePropertyIdentity } from './state-machine.js';
|
|
5
5
|
const FORBIDDEN_LAYERS = new Set(['integrations', 'actions', 'providers', 'queries', 'workflows']);
|
|
6
6
|
const FORBIDDEN_CALLS = new Set(['invoke', 'query', 'retry', 'run', 'submit']);
|
|
7
7
|
export const mutationRules = [
|
|
@@ -150,19 +150,27 @@ export const mutationRules = [
|
|
|
150
150
|
evidence.push(ambiguity(file, node, 'StateMachine transition input is not one static object.'));
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
const removed = [
|
|
154
|
+
'machine',
|
|
155
|
+
'lifecycle',
|
|
156
|
+
'decision',
|
|
157
|
+
'to',
|
|
158
|
+
'transition',
|
|
159
|
+
'witness',
|
|
160
|
+
'step',
|
|
161
|
+
].filter((name) => assignedValue(input, name) !== undefined);
|
|
162
|
+
if (removed.length > 0) {
|
|
163
|
+
evidence.push(violation(file, input, `StateMachine transition derives authority and target from StateProperty; remove ${removed.join(', ')}.`));
|
|
158
164
|
}
|
|
159
165
|
const spread = hasSpread(input);
|
|
160
166
|
if (spread) {
|
|
161
167
|
evidence.push(ambiguity(file, input, 'StateMachine transition input contains a spread that may override or add governed fields.'));
|
|
162
168
|
}
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
if (!spread) {
|
|
170
|
+
for (const required of ['from', 'event']) {
|
|
171
|
+
if (assignedValue(input, required) !== undefined)
|
|
172
|
+
continue;
|
|
173
|
+
evidence.push(violation(file, input, `StateMachine transition has no ${required} intent.`));
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
const selectedClass = propertiesForClass(input, machineProperties);
|
|
@@ -177,25 +185,6 @@ export const mutationRules = [
|
|
|
177
185
|
(selectedClass.kind === 'resolved' && !selectedClass.properties.has(property))) {
|
|
178
186
|
evidence.push(violation(file, assignedValue(input, 'property') ?? input, `StateMachine transition property ${property} is not a machine-backed Property of the selected Class.`));
|
|
179
187
|
}
|
|
180
|
-
const legacyLifecycle = assignedValue(input, 'lifecycle');
|
|
181
|
-
if (legacyLifecycle !== undefined) {
|
|
182
|
-
evidence.push(violation(file, legacyLifecycle, 'StateMachine transition uses removed lifecycle field; use machine.'));
|
|
183
|
-
}
|
|
184
|
-
const machineInput = assignedValue(input, 'machine');
|
|
185
|
-
const machine = machineInput === undefined
|
|
186
|
-
? { kind: 'absent' }
|
|
187
|
-
: stateMachineIdentity(project, file, machineInput);
|
|
188
|
-
if (machine.kind !== 'resolved') {
|
|
189
|
-
if (legacyLifecycle === undefined) {
|
|
190
|
-
evidence.push(ambiguity(file, machineInput ?? input, 'StateMachine transition machine has no resolvable canonical States source.'));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
else if (property !== undefined &&
|
|
194
|
-
selectedClass.kind === 'resolved' &&
|
|
195
|
-
selectedClass.properties.get(property) !== undefined &&
|
|
196
|
-
selectedClass.properties.get(property) !== machine.identity) {
|
|
197
|
-
evidence.push(violation(file, machineInput ?? input, `StateMachine transition uses a different authority than property ${property}.`));
|
|
198
|
-
}
|
|
199
188
|
});
|
|
200
189
|
}
|
|
201
190
|
return evidence;
|
|
@@ -587,7 +576,7 @@ function machinePropertiesByClass(project) {
|
|
|
587
576
|
const ambiguousByClass = new Map();
|
|
588
577
|
const ownerSources = new Map();
|
|
589
578
|
const record = (file, owner, name, value) => {
|
|
590
|
-
const machine =
|
|
579
|
+
const machine = statePropertyIdentity(project, file, value);
|
|
591
580
|
if (machine.kind === 'resolved') {
|
|
592
581
|
const identities = all.get(name) ?? new Set();
|
|
593
582
|
identities.add(machine.identity);
|
|
@@ -635,7 +624,7 @@ function machinePropertiesByClass(project) {
|
|
|
635
624
|
}
|
|
636
625
|
}
|
|
637
626
|
else if (owner !== undefined) {
|
|
638
|
-
const machine =
|
|
627
|
+
const machine = statePropertyIdentity(project, file, node.expression);
|
|
639
628
|
if (machine.kind !== 'absent') {
|
|
640
629
|
const names = ambiguousByClass.get(owner) ?? new Set();
|
|
641
630
|
names.add('<spread>');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { importedSymbol, propertyChain, staticText, unwrap, visit, } from '../../adapters/typescript/index.js';
|
|
3
|
-
import { ambiguity, forbiddenIoImport, production, violation } from './shared.js';
|
|
4
|
-
import { stateMachineConstructorOrigin, stateMachineOrigin, stateSchemaIdentity, } from './state-machine.js';
|
|
3
|
+
import { ambiguity, DSL_MODULES, forbiddenIoImport, production, violation } from './shared.js';
|
|
4
|
+
import { stateMachineConstructorOrigin, stateMachineOrigin, statePropertyIdentity, statePropertyOrigin, stateSchemaIdentity, } from './state-machine.js';
|
|
5
5
|
const FORBIDDEN_LAYERS = new Set([
|
|
6
6
|
'integrations',
|
|
7
7
|
'actions',
|
|
@@ -32,10 +32,23 @@ export const schemaRules = [
|
|
|
32
32
|
const ownedVocabularies = file.submodule === undefined ? undefined : machineVocabularies.get(file.submodule);
|
|
33
33
|
const reportedEnums = new Set();
|
|
34
34
|
visit(file.source, (node) => {
|
|
35
|
-
if (ts.isPropertyAssignment(node)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) &&
|
|
36
|
+
isPropertiesMember(node)) {
|
|
37
|
+
const value = ts.isPropertyAssignment(node) ? node.initializer : node.name;
|
|
38
|
+
const result = persistedStatePropertyEvidence(project, file, value, node);
|
|
39
|
+
if (result !== undefined)
|
|
40
|
+
evidence.push(result);
|
|
41
|
+
}
|
|
42
|
+
if (ts.isSpreadAssignment(node) && isPropertiesObject(node.parent)) {
|
|
43
|
+
const spread = staticSchemaObject(file, node.expression);
|
|
44
|
+
for (const entry of spread?.properties ?? []) {
|
|
45
|
+
if (!ts.isPropertyAssignment(entry) && !ts.isShorthandPropertyAssignment(entry)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const value = ts.isPropertyAssignment(entry) ? entry.initializer : entry.name;
|
|
49
|
+
const result = persistedStatePropertyEvidence(project, file, value, node);
|
|
50
|
+
if (result !== undefined)
|
|
51
|
+
evidence.push(result);
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
54
|
if (ts.isCallExpression(node)) {
|
|
@@ -277,9 +290,79 @@ function propertyName(name) {
|
|
|
277
290
|
? (staticText(name.expression) ?? '<dynamic>')
|
|
278
291
|
: '<dynamic>';
|
|
279
292
|
}
|
|
293
|
+
function persistedStatePropertyEvidence(project, file, value, placement) {
|
|
294
|
+
const associationOrigin = statePropertyOrigin(file, value);
|
|
295
|
+
const association = statePropertyIdentity(project, file, value);
|
|
296
|
+
if (associationOrigin === 'resolved' && isEdgePropertyPlacement(file, placement)) {
|
|
297
|
+
return violation(file, value, 'Schema StateProperty is supported only on Node Classes.');
|
|
298
|
+
}
|
|
299
|
+
if (associationOrigin === 'resolved' && association.kind === 'absent') {
|
|
300
|
+
return violation(file, value, 'Schema stateProperty does not reference an exported StateMachine authority.');
|
|
301
|
+
}
|
|
302
|
+
if (associationOrigin === 'ambiguous' || association.kind === 'ambiguous') {
|
|
303
|
+
return ambiguity(file, value, 'Schema StateProperty has no exact StateMachine source.');
|
|
304
|
+
}
|
|
305
|
+
if (associationOrigin !== 'absent')
|
|
306
|
+
return undefined;
|
|
307
|
+
const codec = stateSchemaIdentity(project, file, value);
|
|
308
|
+
if (codec.kind === 'resolved') {
|
|
309
|
+
return violation(file, value, 'Schema persists a StateMachine codec without retaining its authority; use stateProperty(machine).');
|
|
310
|
+
}
|
|
311
|
+
return codec.kind === 'ambiguous'
|
|
312
|
+
? ambiguity(file, value, 'Schema machine-backed Property has no exact stateProperty source.')
|
|
313
|
+
: undefined;
|
|
314
|
+
}
|
|
315
|
+
function isEdgePropertyPlacement(file, placement) {
|
|
316
|
+
let current = placement;
|
|
317
|
+
while (current !== undefined && !ts.isSourceFile(current)) {
|
|
318
|
+
if (ts.isCallExpression(current)) {
|
|
319
|
+
const expression = unwrap(current.expression);
|
|
320
|
+
if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
|
|
321
|
+
const member = ts.isPropertyAccessExpression(expression)
|
|
322
|
+
? expression.name.text
|
|
323
|
+
: staticText(expression.argumentExpression);
|
|
324
|
+
const owner = importedSymbol(file, expression.expression);
|
|
325
|
+
if ((member === 'directed' || member === 'undirected') &&
|
|
326
|
+
owner?.name === 'edgeClass' &&
|
|
327
|
+
DSL_MODULES.has(owner.module)) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
current = current.parent;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
function staticSchemaObject(file, expression, seen = new Set()) {
|
|
337
|
+
const value = unwrap(expression);
|
|
338
|
+
if (ts.isObjectLiteralExpression(value))
|
|
339
|
+
return value;
|
|
340
|
+
if (!ts.isIdentifier(value) || seen.has(value.text))
|
|
341
|
+
return undefined;
|
|
342
|
+
for (const statement of file.source.statements) {
|
|
343
|
+
if (!ts.isVariableStatement(statement))
|
|
344
|
+
continue;
|
|
345
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
346
|
+
continue;
|
|
347
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
348
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
349
|
+
declaration.name.text === value.text &&
|
|
350
|
+
declaration.initializer !== undefined) {
|
|
351
|
+
return staticSchemaObject(file, declaration.initializer, new Set([...seen, value.text]));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
280
357
|
function isPropertiesMember(node) {
|
|
281
358
|
const owner = node.parent.parent;
|
|
282
359
|
return (ts.isPropertyAssignment(owner) &&
|
|
283
360
|
owner.name !== undefined &&
|
|
284
361
|
propertyName(owner.name) === 'properties');
|
|
285
362
|
}
|
|
363
|
+
function isPropertiesObject(node) {
|
|
364
|
+
const owner = node.parent;
|
|
365
|
+
return (ts.isPropertyAssignment(owner) &&
|
|
366
|
+
owner.name !== undefined &&
|
|
367
|
+
propertyName(owner.name) === 'properties');
|
|
368
|
+
}
|
|
@@ -16,5 +16,9 @@ export declare function stateMachineOrigin(project: SourceProject, file: SourceF
|
|
|
16
16
|
export declare function stateMachineIdentity(project: SourceProject, file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineResolution;
|
|
17
17
|
/** Resolve a Schema codec expression to the exact machine whose stateSchema it aliases. */
|
|
18
18
|
export declare function stateSchemaIdentity(project: SourceProject, file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineResolution;
|
|
19
|
+
/** Resolve a persisted Schema stateProperty to its exact StateMachine authority. */
|
|
20
|
+
export declare function statePropertyIdentity(project: SourceProject, file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineResolution;
|
|
21
|
+
/** Classify whether an expression is produced by the canonical SDK stateProperty helper. */
|
|
22
|
+
export declare function statePropertyOrigin(file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineOrigin;
|
|
19
23
|
/** Classify whether a stateMachine call is the value of an exported top-level binding. */
|
|
20
24
|
export declare function machineExportStatus(file: SourceFile, call: ts.CallExpression): MachineExportStatus;
|
|
@@ -7,6 +7,7 @@ const SCHEMA_MODULES = new Set([
|
|
|
7
7
|
'@astrale-os/kernel-dsl',
|
|
8
8
|
'@astrale-os/kernel-dsl/v1',
|
|
9
9
|
]);
|
|
10
|
+
const STATE_PROPERTY_MODULES = new Set(['@astrale-os/sdk/schema']);
|
|
10
11
|
/** Resolve a direct or static local alias of the SDK stateMachine constructor. */
|
|
11
12
|
export function stateMachineConstructorOrigin(file, expression, seen = new Set()) {
|
|
12
13
|
const value = unwrap(expression);
|
|
@@ -107,6 +108,67 @@ export function stateSchemaIdentity(project, file, expression, seen = new Set())
|
|
|
107
108
|
});
|
|
108
109
|
return candidate ? { kind: 'ambiguous' } : { kind: 'absent' };
|
|
109
110
|
}
|
|
111
|
+
/** Resolve a persisted Schema stateProperty to its exact StateMachine authority. */
|
|
112
|
+
export function statePropertyIdentity(project, file, expression, seen = new Set()) {
|
|
113
|
+
const value = unwrap(expression);
|
|
114
|
+
if (ts.isCallExpression(value)) {
|
|
115
|
+
const symbol = resolveImportedSymbol(file, value.expression);
|
|
116
|
+
if (symbol.kind === 'resolved' &&
|
|
117
|
+
symbol.name === 'stateProperty' &&
|
|
118
|
+
STATE_PROPERTY_MODULES.has(symbol.module)) {
|
|
119
|
+
const machine = value.arguments[0];
|
|
120
|
+
return machine === undefined
|
|
121
|
+
? { kind: 'absent' }
|
|
122
|
+
: stateMachineIdentity(project, file, machine);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (ts.isIdentifier(value)) {
|
|
126
|
+
const declaration = localVariable(file, value.text);
|
|
127
|
+
if (declaration === undefined)
|
|
128
|
+
return { kind: 'absent' };
|
|
129
|
+
if (seen.has(value.text))
|
|
130
|
+
return { kind: 'ambiguous' };
|
|
131
|
+
const next = new Set(seen);
|
|
132
|
+
next.add(value.text);
|
|
133
|
+
const resolution = statePropertyIdentity(project, file, declaration.initializer, next);
|
|
134
|
+
return declaration.constant || resolution.kind === 'absent' ? resolution : { kind: 'ambiguous' };
|
|
135
|
+
}
|
|
136
|
+
let candidate = false;
|
|
137
|
+
visit(value, (node) => {
|
|
138
|
+
if (!ts.isCallExpression(node))
|
|
139
|
+
return;
|
|
140
|
+
const symbol = resolveImportedSymbol(file, node.expression);
|
|
141
|
+
if (symbol.kind !== 'local' &&
|
|
142
|
+
symbol.name === 'stateProperty' &&
|
|
143
|
+
(symbol.kind === 'ambiguous' || STATE_PROPERTY_MODULES.has(symbol.module))) {
|
|
144
|
+
candidate = true;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return candidate ? { kind: 'ambiguous' } : { kind: 'absent' };
|
|
148
|
+
}
|
|
149
|
+
/** Classify whether an expression is produced by the canonical SDK stateProperty helper. */
|
|
150
|
+
export function statePropertyOrigin(file, expression, seen = new Set()) {
|
|
151
|
+
const value = unwrap(expression);
|
|
152
|
+
if (ts.isCallExpression(value)) {
|
|
153
|
+
const symbol = resolveImportedSymbol(file, value.expression);
|
|
154
|
+
if (symbol.kind === 'resolved') {
|
|
155
|
+
return symbol.name === 'stateProperty' && STATE_PROPERTY_MODULES.has(symbol.module)
|
|
156
|
+
? 'resolved'
|
|
157
|
+
: 'absent';
|
|
158
|
+
}
|
|
159
|
+
if (symbol.kind === 'ambiguous' && symbol.name === 'stateProperty')
|
|
160
|
+
return 'ambiguous';
|
|
161
|
+
}
|
|
162
|
+
if (!ts.isIdentifier(value) || seen.has(value.text))
|
|
163
|
+
return 'absent';
|
|
164
|
+
const declaration = localVariable(file, value.text);
|
|
165
|
+
if (declaration === undefined)
|
|
166
|
+
return 'absent';
|
|
167
|
+
const next = new Set(seen);
|
|
168
|
+
next.add(value.text);
|
|
169
|
+
const origin = statePropertyOrigin(file, declaration.initializer, next);
|
|
170
|
+
return declaration.constant || origin === 'absent' ? origin : 'ambiguous';
|
|
171
|
+
}
|
|
110
172
|
function staticMemberReceiver(expression, member) {
|
|
111
173
|
const value = unwrap(expression);
|
|
112
174
|
if (ts.isPropertyAccessExpression(value) && value.name.text === member) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/sdk",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.68",
|
|
4
4
|
"description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
"build": "node scripts/clean-dist.mjs && tsgo && node scripts/verify-no-source-maps.mjs dist && pnpm run verify:package",
|
|
288
288
|
"e2e": "node __e2e__/flagship/runner.mjs",
|
|
289
289
|
"typecheck": "tsgo --noEmit && pnpm run typecheck:specs && pnpm run typecheck:types && pnpm run typecheck:tests && pnpm run typecheck:e2e && pnpm run typecheck:knowledge",
|
|
290
|
-
"typecheck:specs": "tsgo --noEmit -p src/application/query/.spec/tsconfig.json",
|
|
290
|
+
"typecheck:specs": "tsgo --noEmit -p src/application/query/.spec/tsconfig.json && tsgo --noEmit -p src/platform/schema/.spec/tsconfig.json && tsgo --noEmit -p src/application/mutation/.spec/tsconfig.json",
|
|
291
291
|
"typecheck:types": "tsgo --noEmit -p __tests__/tsconfig.types.json",
|
|
292
292
|
"typecheck:tests": "tsgo --noEmit -p tsconfig.test.json",
|
|
293
293
|
"typecheck:e2e": "tsgo --noEmit -p __e2e__/flagship/fixtures/tsconfig.v1.json && tsgo --noEmit -p __e2e__/flagship/fixtures/tsconfig.json",
|