@astrale-os/kernel-protocol 0.5.0-beta.23 → 0.5.0-beta.25

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
@@ -24,6 +24,29 @@
24
24
  * the first supported V2 release is a coordinated `1.0.0`, not a compatible continuation of the
25
25
  current `0.x` line; package and wire versions remain independent as specified in
26
26
  [the protocol architecture](./.spec/architecture.md)
27
+ ## [0.5.0-beta.25](https://github.com/astrale-os/kernel/compare/kernel-protocol-v0.5.0-beta.24...kernel-protocol-v0.5.0-beta.25) (2026-08-29)
28
+
29
+
30
+ ### Dependencies
31
+
32
+ * The following workspace dependencies were updated
33
+ * dependencies
34
+ * @astrale-os/kernel-core bumped to 0.9.0-beta.24
35
+
36
+ ## [0.5.0-beta.24](https://github.com/astrale-os/kernel/compare/kernel-protocol-v0.5.0-beta.23...kernel-protocol-v0.5.0-beta.24) (2026-08-29)
37
+
38
+
39
+ ### Features
40
+
41
+ * **api:** carry view iframe requirements ([#531](https://github.com/astrale-os/kernel/issues/531)) ([4eedb0b](https://github.com/astrale-os/kernel/commit/4eedb0b3430f5f642942e913cf1c5806e09b7cf1))
42
+
43
+
44
+ ### Dependencies
45
+
46
+ * The following workspace dependencies were updated
47
+ * dependencies
48
+ * @astrale-os/kernel-core bumped to 0.9.0-beta.23
49
+
27
50
  ## [0.5.0-beta.23](https://github.com/astrale-os/kernel/compare/kernel-protocol-v0.5.0-beta.22...kernel-protocol-v0.5.0-beta.23) (2026-08-29)
28
51
 
29
52
 
@@ -1,3 +1,3 @@
1
1
  export { admitCallableBinding, admitCallableKey, type CallableBinding } from './callable.js';
2
2
  export { acceptInvocationEndpoints, type InvocationEndpoints } from './invocation.js';
3
- export { admitViewBinding, type ViewBinding } from './view.js';
3
+ export { admitViewBinding, type IframeRequirements, type ViewBinding } from './view.js';
@@ -3,5 +3,12 @@ export interface ViewBinding {
3
3
  readonly view: Key.View;
4
4
  readonly href: string;
5
5
  readonly handshake: 'shell' | 'none';
6
+ readonly iframe?: IframeRequirements;
7
+ }
8
+ export interface IframeRequirements {
9
+ readonly sandbox?: readonly string[];
10
+ readonly allow?: readonly string[];
6
11
  }
7
12
  export declare function admitViewBinding(input: unknown, origin: string): ViewBinding;
13
+ /** Authoring-only normalization used before a Publication receives its canonical ETag. */
14
+ export declare function canonicalizeViewBinding(input: unknown, origin: string): ViewBinding;
@@ -1,9 +1,17 @@
1
1
  import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
2
2
  import { absoluteHttpUrl, exact, record } from '../content.js';
3
3
  import { fail } from '../error.js';
4
+ const TOKEN = /^[a-z][a-z0-9-]*$/u;
4
5
  export function admitViewBinding(input, origin) {
6
+ return readViewBinding(input, origin, false);
7
+ }
8
+ /** Authoring-only normalization used before a Publication receives its canonical ETag. */
9
+ export function canonicalizeViewBinding(input, origin) {
10
+ return readViewBinding(input, origin, true);
11
+ }
12
+ function readViewBinding(input, origin, canonicalize) {
5
13
  const value = record(input, 'PUBLICATION_VIEW_INVALID', 'Published View binding is invalid.');
6
- exact(value, ['view', 'href', 'handshake'], 'PUBLICATION_VIEW_INVALID');
14
+ exact(value, ['view', 'href', 'handshake', 'iframe'], 'PUBLICATION_VIEW_INVALID');
7
15
  let view;
8
16
  try {
9
17
  view = Key(String(value.view));
@@ -20,9 +28,110 @@ export function admitViewBinding(input, origin) {
20
28
  if (value.handshake !== 'shell' && value.handshake !== 'none') {
21
29
  fail('PUBLICATION_VIEW_INVALID', 'View handshake is invalid.');
22
30
  }
31
+ const iframe = admitIframeRequirements(dataValue(value, 'iframe'), value.handshake, canonicalize);
23
32
  return Object.freeze({
24
33
  view,
25
34
  href: absoluteHttpUrl(value.href, true, 'PUBLICATION_VIEW_INVALID'),
26
35
  handshake: value.handshake,
36
+ ...(iframe === undefined ? {} : { iframe }),
37
+ });
38
+ }
39
+ function admitIframeRequirements(input, handshake, canonicalize) {
40
+ if (input === undefined)
41
+ return undefined;
42
+ const value = plainRecord(input);
43
+ exactOwnKeys(value, ['sandbox', 'allow']);
44
+ if (!canonicalize && (!Object.hasOwn(value, 'sandbox') || !Object.hasOwn(value, 'allow'))) {
45
+ invalid();
46
+ }
47
+ const sandbox = tokens(dataValue(value, 'sandbox'), canonicalize);
48
+ const allow = tokens(dataValue(value, 'allow'), canonicalize);
49
+ assertRelationships(sandbox);
50
+ const baselineSandbox = handshake === 'shell' ? ['allow-same-origin', 'allow-scripts'] : ['allow-scripts'];
51
+ const baselineAllow = handshake === 'shell' ? ['clipboard-write'] : [];
52
+ if (!canonicalize &&
53
+ (sandbox.some((token) => baselineSandbox.includes(token)) ||
54
+ allow.some((feature) => baselineAllow.includes(feature)))) {
55
+ invalid();
56
+ }
57
+ const canonicalSandbox = canonicalize
58
+ ? sandbox.filter((token) => !baselineSandbox.includes(token))
59
+ : sandbox;
60
+ const canonicalAllow = canonicalize
61
+ ? allow.filter((feature) => !baselineAllow.includes(feature))
62
+ : allow;
63
+ if (!canonicalize && canonicalSandbox.length === 0 && canonicalAllow.length === 0)
64
+ invalid();
65
+ if (canonicalSandbox.length === 0 && canonicalAllow.length === 0)
66
+ return undefined;
67
+ return Object.freeze({
68
+ sandbox: Object.freeze(canonicalSandbox),
69
+ allow: Object.freeze(canonicalAllow),
27
70
  });
28
71
  }
72
+ function plainRecord(input) {
73
+ if (input === null || typeof input !== 'object' || 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 exactOwnKeys(input, allowed) {
81
+ const keys = new Set(allowed);
82
+ if (Reflect.ownKeys(input).some((key) => typeof key !== 'string' || !keys.has(key)))
83
+ invalid();
84
+ }
85
+ function dataValue(input, key) {
86
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
87
+ if (descriptor === undefined)
88
+ return undefined;
89
+ if (!('value' in descriptor))
90
+ invalid();
91
+ return descriptor.value;
92
+ }
93
+ function tokens(input, canonicalize) {
94
+ if (input === undefined)
95
+ return [];
96
+ if (!Array.isArray(input))
97
+ invalid();
98
+ const descriptors = Object.getOwnPropertyDescriptors(input);
99
+ const values = [];
100
+ for (let index = 0; index < input.length; index += 1) {
101
+ const descriptor = descriptors[String(index)];
102
+ if (descriptor === undefined || !('value' in descriptor))
103
+ invalid();
104
+ if (typeof descriptor.value !== 'string' || !TOKEN.test(descriptor.value))
105
+ invalid();
106
+ values.push(descriptor.value);
107
+ }
108
+ for (const key of Reflect.ownKeys(descriptors)) {
109
+ if (key === 'length')
110
+ continue;
111
+ if (typeof key !== 'string' || !/^(0|[1-9][0-9]*)$/u.test(key) || Number(key) >= input.length) {
112
+ invalid();
113
+ }
114
+ }
115
+ if (canonicalize)
116
+ return [...new Set(values)].sort();
117
+ for (let index = 1; index < values.length; index += 1) {
118
+ if (values[index - 1] >= values[index])
119
+ invalid();
120
+ }
121
+ return values;
122
+ }
123
+ function assertRelationships(sandbox) {
124
+ const tokens = new Set(sandbox);
125
+ if (tokens.has('allow-popups-to-escape-sandbox') && !tokens.has('allow-popups'))
126
+ invalid();
127
+ if (tokens.has('allow-top-navigation') && tokens.has('allow-top-navigation-by-user-activation')) {
128
+ invalid();
129
+ }
130
+ if (tokens.has('allow-top-navigation-to-custom-protocols') &&
131
+ (tokens.has('allow-top-navigation') || tokens.has('allow-popups'))) {
132
+ invalid();
133
+ }
134
+ }
135
+ function invalid() {
136
+ return fail('PUBLICATION_VIEW_INVALID', 'View iframe requirements are invalid.');
137
+ }
@@ -14,4 +14,4 @@ export { ClassOperationOrder, admitRequirements, validateRequirements, type Capa
14
14
  export { acceptReference, compareReference, compareReferences, matchesReference, reference, type PublicationRef, type PublicationReferenceRelation, } from './reference.js';
15
15
  export type { BodyPick, CredentialSource, HttpMethod, HttpRoute, HttpRouteDeclaration, RouteBody, RouteHeaders, SingleCredentialSource, } from './route.js';
16
16
  export type { Schema } from './schema.js';
17
- export { acceptInvocationEndpoints, type CallableBinding, type InvocationEndpoints, type ViewBinding, } from './bindings/index.js';
17
+ export { acceptInvocationEndpoints, type CallableBinding, type IframeRequirements, type InvocationEndpoints, type ViewBinding, } from './bindings/index.js';
@@ -3,6 +3,7 @@ import { accept as acceptIssuerId } from '@astrale-os/kernel-core/auth/issuer';
3
3
  import { Key, patterns } from '@astrale-os/kernel-dsl/v1/addressing';
4
4
  import * as portable from '@astrale-os/kernel-dsl/value';
5
5
  import { admitCallableBinding, admitViewBinding } from './bindings/index.js';
6
+ import { canonicalizeViewBinding } from './bindings/view.js';
6
7
  import { array, deepFreeze, exact, record, unique } from './content.js';
7
8
  import { admitEndpoint as admitDeliveryEndpoint } from './delivery-endpoint.js';
8
9
  import { fail } from './error.js';
@@ -33,7 +34,8 @@ export function url(input) {
33
34
  return `${admitted.replace(/\/+$/u, '')}${PATH}`;
34
35
  }
35
36
  export function seal(input) {
36
- return acceptPublication({ ...input, etag: computeEtag(input) }, false);
37
+ const canonical = withoutEtag(acceptPublication({ ...input, etag: 'canonicalizing' }, false, true));
38
+ return acceptPublication({ ...canonical, etag: computeEtag(canonical) }, false);
37
39
  }
38
40
  export function accept(input) {
39
41
  const publication = acceptPublication(input, true);
@@ -45,7 +47,7 @@ export function accept(input) {
45
47
  export function encode(input) {
46
48
  return portable.json.encode(input);
47
49
  }
48
- function acceptPublication(input, requireEtag) {
50
+ function acceptPublication(input, requireEtag, canonicalizeViews = false) {
49
51
  const value = record(input, 'PUBLICATION_INVALID', 'Expected a Publication object.');
50
52
  exact(value, [
51
53
  'format',
@@ -80,7 +82,7 @@ function acceptPublication(input, requireEtag) {
80
82
  exact(bindingSet, ['callables', 'views'], 'PUBLICATION_BINDING_INVALID');
81
83
  const callables = array(bindingSet.callables, 'PUBLICATION_BINDING_INVALID').map((entry) => admitCallableBinding(entry, origin));
82
84
  unique(callables.map(({ callable }) => callable), 'PUBLICATION_BINDING_INVALID', 'callable binding');
83
- const views = array(bindingSet.views, 'PUBLICATION_VIEW_INVALID').map((entry) => admitViewBinding(entry, origin));
85
+ const views = array(bindingSet.views, 'PUBLICATION_VIEW_INVALID').map((entry) => canonicalizeViews ? canonicalizeViewBinding(entry, origin) : admitViewBinding(entry, origin));
84
86
  unique(views.map(({ view }) => view), 'PUBLICATION_VIEW_INVALID', 'View binding');
85
87
  const routes = array(value.routes, 'PUBLICATION_ROUTE_INVALID').map((entry) => admitRoute(entry, origin));
86
88
  unique(routes.map(({ name }) => name), 'PUBLICATION_ROUTE_INVALID', 'route name');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-protocol",
3
- "version": "0.5.0-beta.23",
3
+ "version": "0.5.0-beta.25",
4
4
  "description": "Portable Astrale invocation, publication, installation, routing, and issuer protocols",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -133,7 +133,7 @@
133
133
  "access": "public"
134
134
  },
135
135
  "dependencies": {
136
- "@astrale-os/kernel-core": "0.9.0-beta.22",
136
+ "@astrale-os/kernel-core": "0.9.0-beta.24",
137
137
  "@astrale-os/kernel-dsl": "0.2.0-beta.17",
138
138
  "@noble/hashes": "2.3.0"
139
139
  },