@astrale-os/sdk 0.5.0-beta.95 → 0.5.0-beta.97

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0-beta.97](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.96...sdk-v0.5.0-beta.97) (2026-08-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **tooling:** admit stable local state machines ([#374](https://github.com/astrale-os/sdk/issues/374)) ([7ebd1b5](https://github.com/astrale-os/sdk/commit/7ebd1b54e282e1b85587e8d33d3faa22b5421cc6))
9
+
10
+ ## [0.5.0-beta.96](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.95...sdk-v0.5.0-beta.96) (2026-08-31)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep external token exchange caller-only ([#366](https://github.com/astrale-os/sdk/issues/366)) ([45d39de](https://github.com/astrale-os/sdk/commit/45d39debce782d1d4f76cf75e9ecf214ca57ddba))
16
+
3
17
  ## [0.5.0-beta.95](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.94...sdk-v0.5.0-beta.95) (2026-08-30)
4
18
 
5
19
 
@@ -34,25 +34,36 @@ export interface DomainGraphExecutor<DomainValue extends Domain = Domain> {
34
34
  readonly mutate: MutationExecutor<DomainValue>;
35
35
  }
36
36
  export type DomainGraphExecutors<DomainValue extends Domain = Domain> = Readonly<Record<'caller' | 'self' | 'union', DomainGraphExecutor<DomainValue>>>;
37
+ /** Explicit full Kernel sessions. No authority mode is selected implicitly. */
38
+ export type DomainKernelSessions = Readonly<Record<'caller' | 'self' | 'union', BoundClientSession>>;
37
39
  type AuthorityOf<Callable, DomainValue extends Domain> = Callable extends {
38
40
  readonly auth: 'anonymous';
39
41
  } ? {
40
42
  readonly caller: AnonymousCaller;
41
43
  readonly client: null;
44
+ readonly kernel: null;
42
45
  readonly graph: null;
43
46
  readonly query?: never;
44
47
  readonly mutate?: never;
45
48
  } | {
46
49
  readonly caller: AuthenticatedCaller;
50
+ /** Caller-only Kernel client. Privileged Domain work must select kernel.self explicitly. */
47
51
  readonly client: BoundClientSession;
52
+ readonly kernel: DomainKernelSessions;
48
53
  readonly graph: DomainGraphExecutors<DomainValue>;
54
+ /** Caller-only convenience executor. */
49
55
  readonly query: QueryExecutor<DomainValue>;
56
+ /** Caller-only convenience executor. */
50
57
  readonly mutate: MutationExecutor<DomainValue>;
51
58
  } : {
52
59
  readonly caller: AuthenticatedCaller;
60
+ /** Caller-only Kernel client. Privileged Domain work must select kernel.self explicitly. */
53
61
  readonly client: BoundClientSession;
62
+ readonly kernel: DomainKernelSessions;
54
63
  readonly graph: DomainGraphExecutors<DomainValue>;
64
+ /** Caller-only convenience executor. */
55
65
  readonly query: QueryExecutor<DomainValue>;
66
+ /** Caller-only convenience executor. */
56
67
  readonly mutate: MutationExecutor<DomainValue>;
57
68
  };
58
69
  type SelfOf<Callable> = Callable extends ResolvedMethod<unknown, unknown, false> ? {
@@ -1,15 +1,18 @@
1
1
  import type { AuthorityRequirementsInput } from './authority.js';
2
2
  import type { ClassRequirementInput } from './classes.js';
3
3
  import type { FunctionRequirementTarget } from './functions.js';
4
+ import type { CapabilityProfileRequirementInput } from './profiles.js';
4
5
  import type { Requirements } from './requirement.js';
5
- export type { AuthorityRequirements, ClassRequirement, Requirements } from './requirement.js';
6
+ export type { AuthorityRequirements, CapabilityProfileRequirement, ClassRequirement, Requirements, } from './requirement.js';
6
7
  export { ClassOperationOrder, type ClassOperation, type ClassRequirementInput } from './classes.js';
7
8
  export type { FunctionRequirementTarget } from './functions.js';
8
9
  export type { AuthorityRequirementsInput } from './authority.js';
10
+ export type { CapabilityProfileRequirementInput } from './profiles.js';
9
11
  export interface RequirementsInput {
10
12
  readonly functions?: readonly FunctionRequirementTarget[];
11
13
  readonly classes?: readonly ClassRequirementInput[];
12
14
  readonly authority?: AuthorityRequirementsInput;
15
+ readonly profiles?: readonly CapabilityProfileRequirementInput[];
13
16
  }
14
17
  /** Capture canonical dependency requirements; Build proves the pinned dependency closure. */
15
18
  export declare function requirements(input?: RequirementsInput): Requirements;
@@ -1,21 +1,24 @@
1
1
  import { captureAuthority } from './authority.js';
2
2
  import { captureClasses } from './classes.js';
3
3
  import { captureFunctions } from './functions.js';
4
+ import { captureProfiles } from './profiles.js';
4
5
  export { ClassOperationOrder } from './classes.js';
5
6
  const admittedRequirements = new WeakSet();
6
7
  /** Capture canonical dependency requirements; Build proves the pinned dependency closure. */
7
8
  export function requirements(input = {}) {
8
9
  if (input === null || typeof input !== 'object' || Array.isArray(input))
9
10
  invalid();
10
- if (Reflect.ownKeys(input).some((key) => key !== 'functions' && key !== 'classes' && key !== 'authority')) {
11
+ if (Reflect.ownKeys(input).some((key) => key !== 'functions' && key !== 'classes' && key !== 'authority' && key !== 'profiles')) {
11
12
  invalid();
12
13
  }
13
14
  const functions = captureFunctions(input.functions);
14
15
  const classes = captureClasses(input.classes);
15
16
  const authority = captureAuthority(input.authority);
17
+ const profiles = captureProfiles(input.profiles);
16
18
  const value = Object.freeze({
17
19
  functions,
18
20
  classes,
21
+ profiles,
19
22
  ...(authority === undefined ? {} : { authority }),
20
23
  });
21
24
  admittedRequirements.add(value);
@@ -25,5 +28,5 @@ export function isRequirements(input) {
25
28
  return input !== null && typeof input === 'object' && admittedRequirements.has(input);
26
29
  }
27
30
  function invalid() {
28
- throw new TypeError('Requirements definition may contain only functions, classes, and authority.');
31
+ throw new TypeError('Requirements definition may contain only functions, classes, authority, and profiles.');
29
32
  }
@@ -0,0 +1,12 @@
1
+ import type { ResolvedCoreDefinition } from '../../platform/schema/index.js';
2
+ import type { AuthorityRequirementsInput } from './authority.js';
3
+ import type { ClassRequirementInput } from './classes.js';
4
+ import type { FunctionRequirementTarget } from './functions.js';
5
+ import type { CapabilityProfileRequirement } from './requirement.js';
6
+ export interface CapabilityProfileRequirementInput {
7
+ readonly subject: Pick<ResolvedCoreDefinition, 'ref'>;
8
+ readonly functions?: readonly FunctionRequirementTarget[];
9
+ readonly classes?: readonly ClassRequirementInput[];
10
+ readonly authority?: AuthorityRequirementsInput;
11
+ }
12
+ export declare function captureProfiles(input: readonly CapabilityProfileRequirementInput[] | undefined): readonly CapabilityProfileRequirement[];
@@ -0,0 +1,41 @@
1
+ import { acceptDefinitionRef, Key } from '../../platform/schema/index.js';
2
+ import { captureAuthority } from './authority.js';
3
+ import { captureClasses } from './classes.js';
4
+ import { captureFunctions } from './functions.js';
5
+ export function captureProfiles(input) {
6
+ if (input === undefined)
7
+ return Object.freeze([]);
8
+ if (!Array.isArray(input))
9
+ throw new TypeError('Requirement profiles must be an array.');
10
+ const profiles = input.map((entry) => {
11
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
12
+ invalid();
13
+ if (Reflect.ownKeys(entry).some((key) => key !== 'subject' && key !== 'functions' && key !== 'classes' && key !== 'authority')) {
14
+ invalid();
15
+ }
16
+ const admitted = acceptDefinitionRef(entry.subject?.ref);
17
+ if (admitted.kind !== 'core')
18
+ invalid();
19
+ const subject = admitted;
20
+ const functions = captureFunctions(entry.functions);
21
+ const classes = captureClasses(entry.classes);
22
+ const authority = captureAuthority(entry.authority);
23
+ if (functions.length === 0 && classes.length === 0 && authority === undefined)
24
+ invalid();
25
+ return Object.freeze({
26
+ subject,
27
+ functions,
28
+ classes,
29
+ ...(authority === undefined ? {} : { authority }),
30
+ });
31
+ });
32
+ if (new Set(profiles.map(({ subject }) => Key.of(subject))).size !== profiles.length)
33
+ invalid();
34
+ return Object.freeze([...profiles].sort((left, right) => compare(Key.of(left.subject), Key.of(right.subject))));
35
+ }
36
+ function invalid() {
37
+ throw new TypeError('Capability profile requirement is invalid.');
38
+ }
39
+ function compare(left, right) {
40
+ return left < right ? -1 : left > right ? 1 : 0;
41
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AuthorityOperation } from '../../platform/schema/index.js';
2
- import type { ClassRef, Key } from '../../platform/schema/index.js';
2
+ import type { ClassRef, CoreRef, Key } from '../../platform/schema/index.js';
3
3
  import type { ClassOperation } from './classes.js';
4
4
  export interface ClassRequirement {
5
5
  readonly class: ClassRef;
@@ -14,4 +14,11 @@ export interface Requirements {
14
14
  readonly functions: readonly Key.Callable[];
15
15
  readonly classes: readonly ClassRequirement[];
16
16
  readonly authority?: AuthorityRequirements;
17
+ readonly profiles: readonly CapabilityProfileRequirement[];
18
+ }
19
+ export interface CapabilityProfileRequirement {
20
+ readonly subject: CoreRef;
21
+ readonly functions: readonly Key.Callable[];
22
+ readonly classes: readonly ClassRequirement[];
23
+ readonly authority?: AuthorityRequirements;
17
24
  }
@@ -54,6 +54,27 @@ function compileRequirements(domain, input, sourceBundle) {
54
54
  throw new TypeError('Class requirement is absent from the dependency closure.');
55
55
  }
56
56
  }
57
+ for (const profile of input?.profiles ?? []) {
58
+ const subject = domain.core.nodes[profile.subject.name];
59
+ if (profile.subject.origin !== domain.origin || subject === undefined) {
60
+ throw new TypeError('Capability profile subject must name one local Core Node.');
61
+ }
62
+ for (const key of profile.functions) {
63
+ requireDirectDependency(domain, key, 'Capability profile Function');
64
+ if (domain.callable(key) === undefined) {
65
+ throw new TypeError(`Capability profile Function ${key} is absent from the dependency closure.`);
66
+ }
67
+ }
68
+ for (const requirement of profile.classes) {
69
+ const key = Key.of(requirement.class);
70
+ if (requirement.class.origin !== domain.origin) {
71
+ requireDirectDependency(domain, key, 'Capability profile Class');
72
+ }
73
+ if (domain.definition(requirement.class) === undefined) {
74
+ throw new TypeError('Capability profile Class is absent from the dependency closure.');
75
+ }
76
+ }
77
+ }
57
78
  const candidate = publication.admitRequirements({
58
79
  capabilities: {
59
80
  ...(input === undefined || input.functions.length === 0
@@ -61,6 +82,16 @@ function compileRequirements(domain, input, sourceBundle) {
61
82
  : { functions: input.functions }),
62
83
  ...(input === undefined || input.classes.length === 0 ? {} : { classes: input.classes }),
63
84
  ...(input?.authority === undefined ? {} : { authority: input.authority }),
85
+ ...(input === undefined || input.profiles.length === 0
86
+ ? {}
87
+ : {
88
+ profiles: input.profiles.map((profile) => ({
89
+ subject: profile.subject,
90
+ ...(profile.functions.length === 0 ? {} : { functions: profile.functions }),
91
+ ...(profile.classes.length === 0 ? {} : { classes: profile.classes }),
92
+ ...(profile.authority === undefined ? {} : { authority: profile.authority }),
93
+ })),
94
+ }),
64
95
  },
65
96
  });
66
97
  publication.validateRequirements(sourceBundle.root, sourceBundle.closure, candidate);
@@ -66,7 +66,7 @@ export async function authenticate(credentialInput, expectedKernel, execution, c
66
66
  let closed = false;
67
67
  return Object.freeze({
68
68
  caller: established.caller,
69
- client: graph.union,
69
+ client: graph.caller,
70
70
  graph,
71
71
  close() {
72
72
  if (closed)
@@ -4,15 +4,6 @@ import type { IdentityNetworkPolicy, VerificationBasisResolver } from './jwks.js
4
4
  import type { SigningMaterial } from './signing.js';
5
5
  export interface ExchangeOptions {
6
6
  readonly maximumTtlSeconds?: number;
7
- /** Authority carried by the exchanged Domain credential. Defaults to the authenticated caller. */
8
- readonly grantMode?: ExchangeGrantMode;
9
- }
10
- export type ExchangeGrantMode = 'caller' | 'union';
11
- interface AcceptedExchangeOptions {
12
- readonly maximumTtlSeconds: number;
13
- readonly grantMode: ExchangeGrantMode;
14
7
  }
15
8
  export declare function maximumExchangeTtl(input: ExchangeOptions | undefined): number;
16
- export declare function tokenExchange(issuer: IssuerId, subject: string, signing: SigningMaterial, policy: IdentityNetworkPolicy, verification: VerificationBasisResolver, maximumTtlSeconds: number, grantMode?: ExchangeGrantMode): exchange.Provider;
17
- export declare function exchangeOptions(input: ExchangeOptions | undefined): AcceptedExchangeOptions;
18
- export {};
9
+ export declare function tokenExchange(issuer: IssuerId, subject: string, signing: SigningMaterial, policy: IdentityNetworkPolicy, verification: VerificationBasisResolver, maximumTtlSeconds: number): exchange.Provider;
@@ -8,27 +8,21 @@ import { CredentialRejected, IssuerUnavailable } from './errors.js';
8
8
  import { verifyCredential, verifyCredentialAgainst } from './jwks.js';
9
9
  const DEFAULT_MAXIMUM_TTL_SECONDS = 300;
10
10
  export function maximumExchangeTtl(input) {
11
- return acceptExchangeOptions(input).maximumTtlSeconds;
12
- }
13
- function acceptExchangeOptions(input) {
14
- if (input === undefined) {
15
- return Object.freeze({ maximumTtlSeconds: DEFAULT_MAXIMUM_TTL_SECONDS, grantMode: 'caller' });
16
- }
11
+ if (input === undefined)
12
+ return DEFAULT_MAXIMUM_TTL_SECONDS;
17
13
  if (input === null ||
18
14
  typeof input !== 'object' ||
19
15
  Array.isArray(input) ||
20
- Reflect.ownKeys(input).some((key) => key !== 'maximumTtlSeconds' && key !== 'grantMode')) {
16
+ Reflect.ownKeys(input).some((key) => key !== 'maximumTtlSeconds')) {
21
17
  throw new TypeError('Execution identity tokenExchange options are invalid.');
22
18
  }
23
19
  const value = input.maximumTtlSeconds ?? DEFAULT_MAXIMUM_TTL_SECONDS;
24
20
  if (!Number.isSafeInteger(value) || value < 1) {
25
21
  throw new TypeError('tokenExchange.maximumTtlSeconds must be a positive safe integer.');
26
22
  }
27
- const grantMode = input.grantMode ?? 'caller';
28
- return Object.freeze({ maximumTtlSeconds: value, grantMode: acceptExchangeGrantMode(grantMode) });
23
+ return value;
29
24
  }
30
- export function tokenExchange(issuer, subject, signing, policy, verification, maximumTtlSeconds, grantMode = 'caller') {
31
- const acceptedGrantMode = acceptExchangeGrantMode(grantMode);
25
+ export function tokenExchange(issuer, subject, signing, policy, verification, maximumTtlSeconds) {
32
26
  return Object.freeze({
33
27
  async exchange(request, context) {
34
28
  try {
@@ -51,7 +45,7 @@ export function tokenExchange(issuer, subject, signing, policy, verification, ma
51
45
  const token = await new SignJWT({
52
46
  grant: {
53
47
  v: 1,
54
- expr: exchangeGrant(acceptedGrantMode, carried.credential),
48
+ expr: { kind: 'identity', credential: carried.credential },
55
49
  },
56
50
  })
57
51
  .setProtectedHeader({
@@ -87,27 +81,6 @@ export function tokenExchange(issuer, subject, signing, policy, verification, ma
87
81
  },
88
82
  });
89
83
  }
90
- export function exchangeOptions(input) {
91
- return acceptExchangeOptions(input);
92
- }
93
- function exchangeGrant(mode, credential) {
94
- const caller = Object.freeze({ kind: 'identity', credential });
95
- if (mode === 'caller')
96
- return caller;
97
- return Object.freeze({
98
- kind: 'union',
99
- operands: Object.freeze([
100
- Object.freeze({ kind: 'identity', self: true }),
101
- caller,
102
- ]),
103
- });
104
- }
105
- function acceptExchangeGrantMode(input) {
106
- if (input !== 'caller' && input !== 'union') {
107
- throw new TypeError('tokenExchange.grantMode must be caller or union.');
108
- }
109
- return input;
110
- }
111
84
  function requireProfile(input, profile) {
112
85
  const expected = ['aud', 'delegation', 'exp', 'iat', 'iss', 'sub'];
113
86
  const actual = Object.keys(input.claims).sort();
@@ -1,7 +1,7 @@
1
1
  import { admit, authenticate } from './authentication.js';
2
2
  import { publicationDelivery } from './delivery.js';
3
3
  import { httpIssuer } from './errors.js';
4
- import { exchangeOptions, tokenExchange } from './exchange.js';
4
+ import { maximumExchangeTtl, tokenExchange } from './exchange.js';
5
5
  import { createVerificationBasisResolver } from './jwks.js';
6
6
  import { signing } from './signing.js';
7
7
  /** Create one stateless execution identity backed by an asymmetric private JWK. */
@@ -37,10 +37,7 @@ export function createIdentity(input) {
37
37
  });
38
38
  const exchangeProvider = input.tokenExchange === false
39
39
  ? undefined
40
- : (() => {
41
- const options = exchangeOptions(input.tokenExchange);
42
- return tokenExchange(issuer, input.subject, material, policy, verification, options.maximumTtlSeconds, options.grantMode);
43
- })();
40
+ : tokenExchange(issuer, input.subject, material, policy, verification, maximumExchangeTtl(input.tokenExchange));
44
41
  return identity({
45
42
  issuer,
46
43
  subject: input.subject,
@@ -1,2 +1,2 @@
1
1
  export { createIdentity, identity, type Identity, type IdentityInput } from './identity.js';
2
- export { maximumExchangeTtl, tokenExchange, type ExchangeGrantMode, type ExchangeOptions, } from './exchange.js';
2
+ export { maximumExchangeTtl, tokenExchange, type ExchangeOptions } from './exchange.js';
@@ -1,2 +1,2 @@
1
1
  export { createIdentity, identity } from './identity.js';
2
- export { maximumExchangeTtl, tokenExchange, } from './exchange.js';
2
+ export { maximumExchangeTtl, tokenExchange } from './exchange.js';
@@ -1,5 +1,5 @@
1
1
  export { serve } from './serve.js';
2
2
  export type { ExecutionContext, ServedApplication, ServeInput } from './serve.js';
3
- export { createIdentity, type ExchangeGrantMode, type ExchangeOptions, type Identity, type IdentityInput, } from './identity/index.js';
3
+ export { createIdentity, type ExchangeOptions, type Identity, type IdentityInput, } from './identity/index.js';
4
4
  export { InvocationError } from './invocation/index.js';
5
5
  export type { InvocationErrorCode } from './invocation/index.js';
@@ -6,6 +6,7 @@ import type { AuthenticatedInvocation, InvocationAuthority } from './authority.j
6
6
  export interface AnonymousInvocation {
7
7
  readonly caller: AnonymousCaller;
8
8
  readonly client: null;
9
+ readonly kernel: null;
9
10
  readonly graph: null;
10
11
  close(): void;
11
12
  }
@@ -6,6 +6,7 @@ import { requireVia } from './authority.js';
6
6
  const ANONYMOUS = Object.freeze({
7
7
  caller: Object.freeze({ kind: 'anonymous' }),
8
8
  client: null,
9
+ kernel: null,
9
10
  graph: null,
10
11
  close() { },
11
12
  });
@@ -37,14 +38,14 @@ export async function authentication(callable, request, release, domain, authori
37
38
  }
38
39
  const caller = admitCaller(authenticated.caller);
39
40
  const graphInput = admitGraph(authenticated.graph);
40
- if (authenticated.client !== graphInput.union)
41
+ if (authenticated.client !== graphInput.caller)
41
42
  invalid();
42
43
  const graph = Object.freeze({
43
44
  caller: bindSession(graphInput.caller),
44
45
  self: bindSession(graphInput.self),
45
46
  union: bindSession(graphInput.union),
46
47
  });
47
- return Object.freeze({ caller, client: graph.union, graph, close });
48
+ return Object.freeze({ caller, client: graph.caller, graph, close });
48
49
  }
49
50
  catch (cause) {
50
51
  if (close === undefined)
@@ -12,6 +12,7 @@ export interface DispatchContext {
12
12
  readonly input: unknown;
13
13
  readonly caller: Caller;
14
14
  readonly client: BoundClientSession | null;
15
+ readonly kernel: Readonly<Record<'caller' | 'self' | 'union', BoundClientSession>> | null;
15
16
  readonly graph: DomainGraphExecutors | null;
16
17
  readonly query?: QueryExecutor;
17
18
  readonly mutate?: MutationExecutor;
@@ -18,6 +18,7 @@ export function context(request, runtime, authentication, self) {
18
18
  input: request.call.input,
19
19
  caller: authentication.caller,
20
20
  client,
21
+ kernel: authentication.graph,
21
22
  graph: authentication.graph === null ? null : bindGraph(authentication.graph, runtime.loaded.domain),
22
23
  ...(client === null
23
24
  ? {}
@@ -7,7 +7,7 @@ export type StateMachineResolution = Readonly<{
7
7
  }> | Readonly<{
8
8
  kind: 'ambiguous' | 'absent';
9
9
  }>;
10
- export type MachineExportStatus = 'exported' | 'private' | 'not-a-declaration';
10
+ export type MachineExportStatus = 'exported' | 'private' | 'mutable' | 'not-a-declaration';
11
11
  /** Resolve a direct or static local alias of the SDK stateMachine constructor. */
12
12
  export declare function stateMachineConstructorOrigin(file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineOrigin;
13
13
  /** Resolve one local binding to an exported SDK stateMachine authority. */
@@ -42,6 +42,19 @@ export function stateMachineIdentity(project, file, expression, seen = new Set()
42
42
  const identity = `${file.path}\0${value.text}`;
43
43
  if (seen.has(identity))
44
44
  return { kind: 'ambiguous' };
45
+ const initializer = unwrap(declaration.initializer);
46
+ if (ts.isCallExpression(initializer)) {
47
+ const origin = stateMachineConstructorOrigin(file, initializer.expression);
48
+ if (origin === 'ambiguous')
49
+ return { kind: 'ambiguous' };
50
+ if (origin === 'resolved') {
51
+ if (!declaration.constant)
52
+ return { kind: 'ambiguous' };
53
+ return machineExportStatus(file, initializer) === 'exported'
54
+ ? { kind: 'resolved', identity: machineIdentity(file, value.text) }
55
+ : { kind: 'absent' };
56
+ }
57
+ }
45
58
  const next = new Set(seen);
46
59
  next.add(identity);
47
60
  const resolution = stateMachineIdentity(project, file, declaration.initializer, next);
@@ -213,6 +226,8 @@ export function machineExportStatus(file, call) {
213
226
  const directlyExported = ts
214
227
  .getModifiers(statement)
215
228
  ?.some(({ kind }) => kind === ts.SyntaxKind.ExportKeyword);
229
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
230
+ return 'mutable';
216
231
  if (directlyExported || locallyExported(file, declaration.name.text))
217
232
  return 'exported';
218
233
  return 'private';
@@ -356,7 +371,9 @@ function localMachineOrigin(file, name) {
356
371
  declaration.initializer !== undefined &&
357
372
  ts.isCallExpression(unwrap(declaration.initializer))) {
358
373
  const call = unwrap(declaration.initializer);
359
- return stateMachineConstructorOrigin(file, call.expression);
374
+ const origin = stateMachineConstructorOrigin(file, call.expression);
375
+ const constant = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0;
376
+ return constant || origin === 'absent' ? origin : 'ambiguous';
360
377
  }
361
378
  }
362
379
  }
@@ -40,7 +40,9 @@ export const stateRules = [
40
40
  if (exportStatus !== 'exported') {
41
41
  evidence.push(violation(machine.file, machine.call, exportStatus === 'private'
42
42
  ? 'State machine relation must be exported as its Schema module StateMachine authority.'
43
- : 'State machine relation must be assigned to one exported top-level binding.'));
43
+ : exportStatus === 'mutable'
44
+ ? 'State machine relation must be declared const as its stable Schema module authority.'
45
+ : 'State machine relation must be assigned to one exported top-level binding.'));
44
46
  }
45
47
  evidence.push(...admitStaticRelation(machine.file, machine.call));
46
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.5.0-beta.95",
3
+ "version": "0.5.0-beta.97",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",
@@ -217,11 +217,11 @@
217
217
  "registry": "https://registry.npmjs.org/"
218
218
  },
219
219
  "dependencies": {
220
- "@astrale-os/kernel-client": "0.6.0-beta.36",
221
- "@astrale-os/kernel-core": "0.9.0-beta.28",
222
- "@astrale-os/kernel-dsl": "0.2.0-beta.21",
223
- "@astrale-os/kernel-protocol": "0.5.0-beta.29",
224
- "@astrale-os/kernel-server": "0.5.0-beta.31",
220
+ "@astrale-os/kernel-client": "0.6.0-beta.39",
221
+ "@astrale-os/kernel-core": "0.9.0-beta.29",
222
+ "@astrale-os/kernel-dsl": "0.2.0-beta.22",
223
+ "@astrale-os/kernel-protocol": "0.5.0-beta.32",
224
+ "@astrale-os/kernel-server": "0.5.0-beta.34",
225
225
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
226
226
  "hono": "^4.13.2",
227
227
  "jose": "^6.2.9",