@astrale-os/sdk 0.5.0-beta.76 → 0.5.0-beta.78

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.78](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.77...sdk-v0.5.0-beta.78) (2026-08-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * **mutation:** support atomic transition deltas ([#331](https://github.com/astrale-os/sdk/issues/331)) ([0bf94fa](https://github.com/astrale-os/sdk/commit/0bf94fa8193cdc52047007f25cbf59e79f1f97b4))
9
+
10
+ ## [0.5.0-beta.77](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.76...sdk-v0.5.0-beta.77) (2026-08-29)
11
+
12
+
13
+ ### Features
14
+
15
+ * **iframe:** declare view iframe requirements ([#327](https://github.com/astrale-os/sdk/issues/327)) ([ca2e131](https://github.com/astrale-os/sdk/commit/ca2e1311291aee3c62a19cddb33a0b4c13a345d4))
16
+
3
17
  ## [0.5.0-beta.76](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.75...sdk-v0.5.0-beta.76) (2026-08-29)
4
18
 
5
19
 
@@ -5,5 +5,5 @@ export type { AdministrationRequirements, AdministrationRequirementsInput, Calla
5
5
  export { route, routes } from './routes/index.js';
6
6
  export type { Route, RouteInput, RouteTarget, Routes } from './routes/index.js';
7
7
  export { defineFrontend, external, vite } from './view/index.js';
8
- export type { Frontend, FrontendInput, FrontendSource } from './view/index.js';
8
+ export type { Frontend, FrontendInput, FrontendSource, IframeRequirements } from './view/index.js';
9
9
  export type { Application, ApplicationIdentity, RuntimeOfApplication, SchemaOfApplication, SchemaOfRuntime, } from './application.js';
@@ -3,7 +3,7 @@ import type { Domain, ResolvedClass } from '../../../platform/schema/index.js';
3
3
  import type { StatePropertiesOfClass } from '../../../platform/schema/state-property/index.js';
4
4
  import type { EventOf, StateMachine, StateOf } from '../../state/machine.js';
5
5
  import type { ExistingEndpoint } from './endpoint.js';
6
- import type { RichPropertyConditions, RichPropertyName } from './properties.js';
6
+ import type { RichPropertyConditions, RichPropertyDelta, RichPropertyName } from './properties.js';
7
7
  export type StatePropertyName<Class extends ResolvedClass<'node'>> = Extract<keyof StatePropertiesOfClass<Class>, RichPropertyName<Class>>;
8
8
  export type StateMachineOf<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>> = Extract<StatePropertiesOfClass<Class>[Property], StateMachine>;
9
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>> {
@@ -13,5 +13,7 @@ export interface StateTransitionInput<Class extends ResolvedClass<'node'>, Prope
13
13
  readonly from: From;
14
14
  readonly event: Event;
15
15
  readonly props?: RichPropertyConditions<Class>;
16
+ /** Auxiliary property changes committed in the same compare-and-set as the state transition. */
17
+ readonly update?: RichPropertyDelta<Class>;
16
18
  }
17
19
  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;
@@ -10,10 +10,15 @@ export function authorStateTransition(core, domain, input) {
10
10
  const fromState = input.from;
11
11
  const event = input.event;
12
12
  const suppliedProps = input.props;
13
+ const suppliedUpdate = input.update;
13
14
  const equals = suppliedProps?.equals;
14
15
  const absent = suppliedProps?.absent;
16
+ const set = suppliedUpdate?.set;
17
+ const unset = suppliedUpdate?.unset;
15
18
  const suppliedEquals = equals === undefined ? undefined : { ...equals };
16
19
  const suppliedAbsent = absent === undefined ? undefined : [...absent];
20
+ const suppliedSet = set === undefined ? undefined : { ...set };
21
+ const suppliedUnset = unset === undefined ? undefined : [...unset];
17
22
  const selected = resolvedProperty(selectedClass, property);
18
23
  const selectedClassKey = ClassKey(selectedClass.key);
19
24
  const machine = stateMachineFor(domain, selected);
@@ -28,7 +33,9 @@ export function authorStateTransition(core, domain, input) {
28
33
  if ((suppliedEquals !== undefined &&
29
34
  Object.hasOwn(suppliedEquals, property) &&
30
35
  suppliedEquals[property] !== fromState) ||
31
- suppliedAbsent?.includes(property) === true) {
36
+ suppliedAbsent?.includes(property) === true ||
37
+ (suppliedSet !== undefined && Object.hasOwn(suppliedSet, property)) ||
38
+ suppliedUnset?.includes(property) === true) {
32
39
  throw new TypeError(`StateMachine transition conditions conflict with property ${String(property)}.`);
33
40
  }
34
41
  core.expect.node({
@@ -42,6 +49,9 @@ export function authorStateTransition(core, domain, input) {
42
49
  core.updateNode({
43
50
  node,
44
51
  class: selectedClassKey,
45
- props: delta(selectedClass, { set: to }),
52
+ props: delta(selectedClass, {
53
+ set: { ...suppliedSet, ...to },
54
+ ...(suppliedUnset === undefined ? {} : { unset: suppliedUnset }),
55
+ }),
46
56
  });
47
57
  }
@@ -1,6 +1,6 @@
1
1
  export { isFrontend } from './admission.js';
2
2
  export { defineFrontend } from './define.js';
3
3
  export type { Frontend, FrontendInput, FrontendRouteInputs, FrontendRoutes, ViewName, } from './frontend.js';
4
- export type { FrontendHandshake, FrontendRoute, FrontendRouteInput } from './route.js';
4
+ export type { FrontendHandshake, FrontendRoute, FrontendRouteInput, IframeRequirements, } from './route.js';
5
5
  export { external, isFrontendSource, vite } from './source.js';
6
6
  export type { ExternalSource, FrontendSource, ViteSource } from './source.js';
@@ -1,13 +1,19 @@
1
1
  export type FrontendHandshake = 'shell' | 'none';
2
+ export interface IframeRequirements {
3
+ readonly sandbox?: readonly string[];
4
+ readonly allow?: readonly string[];
5
+ }
2
6
  export type FrontendRouteInput = string | {
3
7
  readonly path?: string;
4
8
  readonly document?: string;
5
9
  readonly handshake?: FrontendHandshake;
10
+ readonly iframe?: IframeRequirements;
6
11
  };
7
12
  export interface FrontendRoute {
8
13
  readonly path: string;
9
14
  readonly document: string;
10
15
  readonly handshake: FrontendHandshake;
16
+ readonly iframe?: IframeRequirements;
11
17
  }
12
18
  export declare function normalizeRoute(input: FrontendRouteInput | undefined, fallback: string): FrontendRoute;
13
19
  export declare function defaultRoute(name: string): string;
@@ -1,22 +1,102 @@
1
+ const TOKEN = /^[a-z][a-z0-9-]*$/u;
2
+ const BASELINES = Object.freeze({
3
+ none: iframeRequirements(['allow-scripts'], []),
4
+ shell: iframeRequirements(['allow-same-origin', 'allow-scripts'], ['clipboard-write']),
5
+ });
1
6
  export function normalizeRoute(input, fallback) {
2
7
  if (typeof input === 'string') {
3
8
  return Object.freeze({ path: routePath(input), document: 'index.html', handshake: 'shell' });
4
9
  }
5
- const value = input ?? {};
6
- if (value === null || typeof value !== 'object' || Array.isArray(value))
7
- invalid();
8
- if (Reflect.ownKeys(value).some((key) => key !== 'path' && key !== 'document' && key !== 'handshake')) {
10
+ const value = plainRecord(input ?? {});
11
+ if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !['path', 'document', 'handshake', 'iframe'].includes(key))) {
9
12
  invalid();
10
13
  }
11
- if (value.handshake !== undefined && value.handshake !== 'shell' && value.handshake !== 'none') {
14
+ const authoredHandshake = dataValue(value, 'handshake');
15
+ if (authoredHandshake !== undefined &&
16
+ authoredHandshake !== 'shell' &&
17
+ authoredHandshake !== 'none') {
12
18
  invalid();
13
19
  }
20
+ const handshake = authoredHandshake ?? 'shell';
21
+ const iframe = normalizeIframeRequirements(dataValue(value, 'iframe'), handshake);
22
+ return Object.freeze({
23
+ path: routePath(dataValue(value, 'path') ?? fallback),
24
+ document: filePath(dataValue(value, 'document') ?? 'index.html'),
25
+ handshake,
26
+ ...(iframe === undefined ? {} : { iframe }),
27
+ });
28
+ }
29
+ function normalizeIframeRequirements(input, handshake) {
30
+ if (input === undefined)
31
+ return undefined;
32
+ const value = plainRecord(input);
33
+ if (Reflect.ownKeys(value).some((key) => key !== 'sandbox' && key !== 'allow'))
34
+ invalid();
35
+ const authored = iframeRequirements(tokenArray(dataValue(value, 'sandbox')), tokenArray(dataValue(value, 'allow')));
36
+ assertSandboxRelationships(authored.sandbox);
37
+ const baseline = BASELINES[handshake];
38
+ const sandbox = authored.sandbox.filter((token) => !baseline.sandbox.includes(token));
39
+ const allow = authored.allow.filter((feature) => !baseline.allow.includes(feature));
40
+ return sandbox.length === 0 && allow.length === 0 ? undefined : iframeRequirements(sandbox, allow);
41
+ }
42
+ function iframeRequirements(sandbox, allow) {
14
43
  return Object.freeze({
15
- path: routePath(value.path ?? fallback),
16
- document: filePath(value.document ?? 'index.html'),
17
- handshake: value.handshake ?? 'shell',
44
+ sandbox: Object.freeze([...new Set(sandbox)].sort()),
45
+ allow: Object.freeze([...new Set(allow)].sort()),
18
46
  });
19
47
  }
48
+ function tokenArray(input) {
49
+ if (input === undefined)
50
+ return [];
51
+ if (!Array.isArray(input))
52
+ invalid();
53
+ const descriptors = Object.getOwnPropertyDescriptors(input);
54
+ const result = [];
55
+ for (let index = 0; index < input.length; index += 1) {
56
+ const descriptor = descriptors[String(index)];
57
+ if (descriptor === undefined || !('value' in descriptor))
58
+ invalid();
59
+ if (typeof descriptor.value !== 'string' || !TOKEN.test(descriptor.value))
60
+ invalid();
61
+ result.push(descriptor.value);
62
+ }
63
+ for (const key of Reflect.ownKeys(descriptors)) {
64
+ if (key === 'length')
65
+ continue;
66
+ if (typeof key !== 'string' || !/^(0|[1-9][0-9]*)$/u.test(key) || Number(key) >= input.length) {
67
+ invalid();
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ function plainRecord(input) {
73
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
74
+ invalid();
75
+ const prototype = Object.getPrototypeOf(input);
76
+ if (prototype !== Object.prototype && prototype !== null)
77
+ invalid();
78
+ return input;
79
+ }
80
+ function dataValue(input, key) {
81
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
82
+ if (descriptor === undefined)
83
+ return undefined;
84
+ if (!('value' in descriptor))
85
+ invalid();
86
+ return descriptor.value;
87
+ }
88
+ function assertSandboxRelationships(sandbox) {
89
+ const tokens = new Set(sandbox);
90
+ if (tokens.has('allow-popups-to-escape-sandbox') && !tokens.has('allow-popups'))
91
+ invalid();
92
+ if (tokens.has('allow-top-navigation') && tokens.has('allow-top-navigation-by-user-activation')) {
93
+ invalid();
94
+ }
95
+ if (tokens.has('allow-top-navigation-to-custom-protocols') &&
96
+ (tokens.has('allow-top-navigation') || tokens.has('allow-popups'))) {
97
+ invalid();
98
+ }
99
+ }
20
100
  export function defaultRoute(name) {
21
101
  return routePath(`/${name
22
102
  .replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
@@ -1,5 +1,5 @@
1
1
  import type { Frontend, FrontendSource } from '../../../application/view/index.js';
2
- import type { FrontendHandshake } from '../../../application/view/index.js';
2
+ import type { FrontendHandshake, IframeRequirements } from '../../../application/view/index.js';
3
3
  import type { Key, ViewRef } from '../../../platform/schema/index.js';
4
4
  import type { Domain } from '../../../platform/schema/index.js';
5
5
  export interface CompiledFrontendRoute {
@@ -7,6 +7,7 @@ export interface CompiledFrontendRoute {
7
7
  readonly path: string;
8
8
  readonly document: string;
9
9
  readonly handshake: FrontendHandshake;
10
+ readonly iframe?: IframeRequirements;
10
11
  }
11
12
  export interface FrontendCompilation {
12
13
  readonly source: FrontendSource;
@@ -15,6 +15,7 @@ export function compileFrontend(frontend, domain) {
15
15
  path: route.path,
16
16
  document: route.document,
17
17
  handshake: route.handshake,
18
+ ...(route.iframe === undefined ? {} : { iframe: route.iframe }),
18
19
  });
19
20
  });
20
21
  const entrypoint = domain.views[frontend.entrypoint];
@@ -8,5 +8,6 @@ export function views(build, addressing) {
8
8
  view: route.view,
9
9
  href: new URL(route.path.replace(/^\//u, ''), base).href,
10
10
  handshake: route.handshake,
11
+ ...(route.iframe === undefined ? {} : { iframe: route.iframe }),
11
12
  })));
12
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.5.0-beta.76",
3
+ "version": "0.5.0-beta.78",
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.29",
221
- "@astrale-os/kernel-core": "0.9.0-beta.22",
220
+ "@astrale-os/kernel-client": "0.6.0-beta.30",
221
+ "@astrale-os/kernel-core": "0.9.0-beta.23",
222
222
  "@astrale-os/kernel-dsl": "0.2.0-beta.17",
223
- "@astrale-os/kernel-protocol": "0.5.0-beta.23",
224
- "@astrale-os/kernel-server": "0.5.0-beta.25",
223
+ "@astrale-os/kernel-protocol": "0.5.0-beta.24",
224
+ "@astrale-os/kernel-server": "0.5.0-beta.26",
225
225
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
226
226
  "hono": "^4.13.2",
227
227
  "jose": "^6.2.9",