@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/application/mutation/authoring/builder.d.ts +6 -21
  3. package/dist/application/mutation/authoring/builder.js +4 -65
  4. package/dist/application/mutation/authoring/transition.d.ts +17 -0
  5. package/dist/application/mutation/authoring/transition.js +47 -0
  6. package/dist/application/mutation/define.d.ts +3 -1
  7. package/dist/application/mutation/execution/execute.js +1 -1
  8. package/dist/application/mutation/failure.d.ts +10 -2
  9. package/dist/application/mutation/failure.js +24 -1
  10. package/dist/application/state/authority.d.ts +3 -0
  11. package/dist/application/state/authority.js +7 -0
  12. package/dist/application/state/define.js +4 -1
  13. package/dist/execution/identity/authentication.d.ts +2 -1
  14. package/dist/execution/identity/authentication.js +3 -3
  15. package/dist/execution/identity/exchange.d.ts +2 -3
  16. package/dist/execution/identity/exchange.js +6 -2
  17. package/dist/execution/identity/identity.js +4 -1
  18. package/dist/execution/identity/jwks.d.ts +9 -1
  19. package/dist/execution/identity/jwks.js +126 -2
  20. package/dist/platform/schema/index.d.ts +2 -0
  21. package/dist/platform/schema/index.js +1 -0
  22. package/dist/platform/schema/state-property/authoring.d.ts +15 -0
  23. package/dist/platform/schema/state-property/authoring.js +49 -0
  24. package/dist/platform/schema/state-property/edge.d.ts +16 -0
  25. package/dist/platform/schema/state-property/edge.js +21 -0
  26. package/dist/platform/schema/state-property/index.d.ts +4 -0
  27. package/dist/platform/schema/state-property/index.js +3 -0
  28. package/dist/platform/schema/state-property/model.d.ts +49 -0
  29. package/dist/platform/schema/state-property/model.js +1 -0
  30. package/dist/platform/schema/state-property/registry.d.ts +9 -0
  31. package/dist/platform/schema/state-property/registry.js +74 -0
  32. package/dist/tooling/linter/implementations/source/mutations.js +19 -30
  33. package/dist/tooling/linter/implementations/source/schema.js +89 -6
  34. package/dist/tooling/linter/implementations/source/state-machine.d.ts +4 -0
  35. package/dist/tooling/linter/implementations/source/state-machine.js +62 -0
  36. 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 { StateMachine, StateOf, TransitionOf } from '../../state/machine.js';
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, RichPropertyName } from './properties.js';
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<Machine extends StateMachine, Class extends ResolvedClass<'node'>, Property extends StateMachinePropertyName<Class, Machine>>(input: {
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, resolvedProperty } from './properties.js';
6
- export function richMutationBuilder(core) {
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
- const submitted = input.decision;
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: ResolvedDomainOf<Schema>) => MutationDefinition<Input, Output>): Mutation<ResolvedDomainOf<Schema>, Input, Output>;
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 MutationFailure extends Error {
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
  }
@@ -0,0 +1,3 @@
1
+ import type { StateMachine } from './machine.js';
2
+ export declare function rememberStateMachine(machine: StateMachine): void;
3
+ export declare function isStateMachine(input: unknown): input is StateMachine;
@@ -0,0 +1,7 @@
1
+ const machines = new WeakSet();
2
+ export function rememberStateMachine(machine) {
3
+ machines.add(machine);
4
+ }
5
+ export function isStateMachine(input) {
6
+ return typeof input === 'object' && input !== null && machines.has(input);
7
+ }
@@ -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
- return Object.freeze({
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.fetch, execution, configuration.policy);
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, fetch, execution, policy) {
107
+ async function establishCaller(input, audience, verification, execution, policy) {
108
108
  requireLive(execution);
109
109
  try {
110
- const context = await verifyCredential(input, audience, fetch, execution.signal, policy);
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, fetch: Fetch, policy: IdentityNetworkPolicy, maximumTtlSeconds: number): exchange.Provider;
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, fetch, policy, maximumTtlSeconds) {
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
- const outer = await verifyCredential(request.token, issuer, fetch, context.signal, policy);
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, fetch, policy, maximumExchangeTtl(input.tokenExchange));
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 declare function verifyCredential(input: CredentialInput, audience: IssuerId, fetch: Fetch, signal: AbortSignal, policy: IdentityNetworkPolicy): Promise<{
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
- export async function verifyCredential(input, audience, fetch, signal, policy) {
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 discover(admittedIssuer, fetch, signal, policy);
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,3 @@
1
+ export { defineSchema, nodeClass, stateProperty } from './authoring.js';
2
+ export { edgeClass } from './edge.js';
3
+ 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, stateSchemaIdentity } from './state-machine.js';
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
- if (assignedValue(input, 'to') !== undefined ||
154
- assignedValue(input, 'event') !== undefined ||
155
- assignedValue(input, 'transition') !== undefined ||
156
- assignedValue(input, 'witness') !== undefined) {
157
- evidence.push(violation(file, input, 'StateMachine transition accepts one allowed decision, not a target, event, transition, or witness field.'));
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 (assignedValue(input, 'decision') === undefined) {
164
- if (!spread) {
165
- evidence.push(violation(file, input, 'StateMachine transition has no allowed decision.'));
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 = stateSchemaIdentity(project, file, value);
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 = stateSchemaIdentity(project, file, node.expression);
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) && isPropertiesMember(node)) {
36
- const codec = stateSchemaIdentity(project, file, node.initializer);
37
- if (codec.kind === 'ambiguous') {
38
- evidence.push(ambiguity(file, node.initializer, 'Schema machine-backed Property codec has no exact stateSchema source.'));
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.66",
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",