@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
|
-
|
|
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.
|
|
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.
|
|
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
|
},
|