@zixcel/view-delivery-contracts 0.10.2 → 0.10.4
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/package.json +14 -6
- package/src/index.d.mts +20 -43
- package/src/index.mjs +107 -60
- package/src/types.d.mts +61 -0
- package/src/types.mjs +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zixcel/view-delivery-contracts",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Bounded site delivery and per-source readiness over exact Projection contracts",
|
|
@@ -18,12 +18,9 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@zixcel/projection-contracts": "0.10.
|
|
21
|
+
"@zixcel/projection-contracts": "0.10.4",
|
|
22
22
|
"@zixcel/interaction": "0.10.1"
|
|
23
23
|
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"test": "node --test test/*.test.mjs"
|
|
26
|
-
},
|
|
27
24
|
"repository": {
|
|
28
25
|
"type": "git",
|
|
29
26
|
"url": "git+https://github.com/zixcel/data-presentation.git",
|
|
@@ -33,5 +30,16 @@
|
|
|
33
30
|
"publishConfig": {
|
|
34
31
|
"access": "public",
|
|
35
32
|
"registry": "https://registry.npmjs.org"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"typed-contracts",
|
|
36
|
+
"schema-validation",
|
|
37
|
+
"artifact-distribution",
|
|
38
|
+
"javascript"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test test/*.test.mts",
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit"
|
|
36
44
|
}
|
|
37
|
-
}
|
|
45
|
+
}
|
package/src/index.d.mts
CHANGED
|
@@ -1,44 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { Submission, Requirement, Readiness, Delivery } from './types.mjs';
|
|
2
|
+
import type { SceneProjection } from '@zixcel/projection-contracts/client';
|
|
3
|
+
export type * from './types.mjs';
|
|
4
|
+
export declare const bounds: Readonly<{
|
|
5
|
+
documentBytes: 1048576;
|
|
6
|
+
assets: 8;
|
|
7
|
+
assetBytes: 1048576;
|
|
8
|
+
requirements: 16;
|
|
9
|
+
requestBytes: 32768;
|
|
10
|
+
requests: 16;
|
|
11
|
+
bodyMs: 5000;
|
|
12
|
+
}>;
|
|
13
|
+
export declare class DeliveryError extends Error {
|
|
14
|
+
readonly code: string;
|
|
15
|
+
constructor(code: string);
|
|
12
16
|
}
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
readonly detail:Json
|
|
19
|
-
}
|
|
20
|
-
export interface Setup {readonly id:string;readonly owner:string;readonly operation:string}
|
|
21
|
-
export type Requirement =
|
|
22
|
-
| {readonly ref:string;readonly owner:string;readonly state:'Ready';readonly failure:null;readonly setup:null}
|
|
23
|
-
| {readonly ref:string;readonly owner:string;readonly state:'SetupRequired'|'Unavailable';readonly failure:OwnerFailure;readonly setup:Setup|null}
|
|
24
|
-
export interface Readiness {
|
|
25
|
-
readonly state:'Ready'|'SetupRequired'|'Unavailable'
|
|
26
|
-
readonly requirements:readonly Requirement[]
|
|
27
|
-
}
|
|
28
|
-
export interface Asset {readonly path:string;readonly digest:string;readonly bytes:number;readonly kind:'script'|'style'}
|
|
29
|
-
export interface Delivery {
|
|
30
|
-
readonly contract:string
|
|
31
|
-
readonly site:{readonly id:string;readonly revision:string}
|
|
32
|
-
readonly initial:ProjectionIdentity|null
|
|
33
|
-
readonly snapshot:SceneProjection|null
|
|
34
|
-
readonly readiness:Readiness
|
|
35
|
-
readonly assets:readonly Asset[]
|
|
36
|
-
readonly transport:{readonly path:'/api/projection-live';readonly classes:readonly ['STATE']}
|
|
37
|
-
}
|
|
38
|
-
export const bounds:Readonly<{documentBytes:1048576;assets:8;assetBytes:1048576;requirements:16;requestBytes:32768;requests:16;bodyMs:5000}>
|
|
39
|
-
export class DeliveryError extends Error {readonly code:string;constructor(code:string)}
|
|
40
|
-
export function validateSubmission(value:unknown):Submission
|
|
41
|
-
export function inputAction(snapshot:SceneProjection,submission:Submission):Action
|
|
42
|
-
export function requirement(value:unknown):Requirement
|
|
43
|
-
export function readiness(refs:readonly string[],owners:readonly Requirement[]):Readiness
|
|
44
|
-
export function validateDelivery(value:unknown,expectedContract?:string):Delivery
|
|
17
|
+
export declare function validateSubmission(value: unknown): Submission;
|
|
18
|
+
export declare function inputAction(snapshot: SceneProjection, submission: Submission): import("@zixcel/projection-contracts/client").Action;
|
|
19
|
+
export declare function requirement(value: unknown): Requirement;
|
|
20
|
+
export declare function readiness(refs: readonly string[], owners: readonly unknown[]): Readiness;
|
|
21
|
+
export declare function validateDelivery(value: unknown, expectedContract?: string): Delivery;
|
package/src/index.mjs
CHANGED
|
@@ -1,66 +1,113 @@
|
|
|
1
1
|
// Snapshot delivery contracts: no framework, business policy, transport or renderer.
|
|
2
|
-
import {canonical,shape,text,copy,frozen} from '@zixcel/projection-contracts/client'
|
|
3
|
-
import {validateSnapshot} from '@zixcel/projection-contracts/client'
|
|
4
|
-
import {validateInputValues} from '@zixcel/interaction/input'
|
|
5
|
-
export const bounds=Object.freeze({documentBytes:1048576,assets:8,assetBytes:1048576,requirements:16,requestBytes:32768,requests:16,bodyMs:5000})
|
|
6
|
-
export class DeliveryError extends Error {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export function validateSubmission(v){
|
|
10
|
-
const fields=['projectionKey','projectionRevision','sceneId','actionId','interactionRef','generation','inputContractRef','values']
|
|
11
|
-
if(!v||Object.getPrototypeOf(v)!==Object.prototype||Object.keys(v).length!==fields.length||fields.some(k=>!Object.hasOwn(v,k)))fail('InvalidInput')
|
|
12
|
-
for(const k of fields.filter(k=>k!=='values'))if(typeof v[k]!=='string'||!v[k]||v[k].length>512)fail('InvalidInput')
|
|
13
|
-
if(!v.values||Object.getPrototypeOf(v.values)!==Object.prototype||Object.keys(v.values).length>32||Object.values(v.values).some(x=>!['string','boolean','number'].includes(typeof x)||typeof x==='number'&&!Number.isFinite(x)))fail('InvalidInput')
|
|
14
|
-
canonical(v,bounds.requestBytes);return v
|
|
2
|
+
import { canonical, shape, text, copy, frozen } from '@zixcel/projection-contracts/client';
|
|
3
|
+
import { validateSnapshot } from '@zixcel/projection-contracts/client';
|
|
4
|
+
import { validateInputValues } from '@zixcel/interaction/input';
|
|
5
|
+
export const bounds = Object.freeze({ documentBytes: 1048576, assets: 8, assetBytes: 1048576, requirements: 16, requestBytes: 32768, requests: 16, bodyMs: 5000 });
|
|
6
|
+
export class DeliveryError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code) { super(code); this.code = code; }
|
|
15
9
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
function fail(code) { throw new DeliveryError(code); }
|
|
11
|
+
const list = (v, maximum) => { if (!Array.isArray(v) || v.length > maximum)
|
|
12
|
+
fail('DeliveryLimitExceeded'); return v; };
|
|
13
|
+
const object = (v, fields) => { shape(v, fields); return v; };
|
|
14
|
+
export function validateSubmission(value) {
|
|
15
|
+
const fields = ['projectionKey', 'projectionRevision', 'sceneId', 'actionId', 'interactionRef', 'generation', 'inputContractRef', 'values'];
|
|
16
|
+
// Preserve the existing input refusal code rather than exposing field contents.
|
|
17
|
+
if (!value || Object.getPrototypeOf(value) !== Object.prototype || Object.keys(value).length !== fields.length || fields.some(k => !Object.hasOwn(value, k)))
|
|
18
|
+
fail('InvalidInput');
|
|
19
|
+
const v = value;
|
|
20
|
+
canonical(v, bounds.requestBytes); // Refuse accessor-backed input before reading fields.
|
|
21
|
+
for (const k of fields.filter(k => k !== 'values'))
|
|
22
|
+
if (typeof v[k] !== 'string' || !v[k] || v[k].length > 512)
|
|
23
|
+
fail('InvalidInput');
|
|
24
|
+
const values = v.values;
|
|
25
|
+
if (!values || Object.getPrototypeOf(values) !== Object.prototype || Object.keys(values).length > 32 || Object.values(values).some(x => !['string', 'boolean', 'number'].includes(typeof x) || typeof x === 'number' && !Number.isFinite(x)))
|
|
26
|
+
fail('InvalidInput');
|
|
27
|
+
return value;
|
|
24
28
|
}
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
export function inputAction(snapshot, submission) {
|
|
30
|
+
validateSubmission(submission);
|
|
31
|
+
validateSnapshot(snapshot);
|
|
32
|
+
if (snapshot.kind !== 'Scene' || snapshot.key !== submission.sceneId || snapshot.revision !== submission.projectionRevision)
|
|
33
|
+
fail('StaleSceneAction');
|
|
34
|
+
const action = snapshot.data.actions.find(a => a.id === submission.actionId), binding = action?.interaction;
|
|
35
|
+
if (!binding)
|
|
36
|
+
fail('InteractionUnavailable');
|
|
37
|
+
if (binding.ref !== submission.interactionRef || binding.generation !== submission.generation || binding.inputContractRef !== submission.inputContractRef)
|
|
38
|
+
fail('StaleSceneAction');
|
|
39
|
+
if (validateInputValues(binding.input, submission.values).length)
|
|
40
|
+
fail('InvalidInput');
|
|
41
|
+
return action;
|
|
37
42
|
}
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
export function requirement(value) {
|
|
44
|
+
const v = object(value, ['ref', 'owner', 'state', 'failure', 'setup']);
|
|
45
|
+
text(v.ref);
|
|
46
|
+
text(v.owner);
|
|
47
|
+
if (typeof v.state !== 'string' || !['Ready', 'SetupRequired', 'Unavailable'].includes(v.state))
|
|
48
|
+
fail('InvalidReadiness');
|
|
49
|
+
if (v.state === 'Ready') {
|
|
50
|
+
if (v.failure !== null || v.setup !== null)
|
|
51
|
+
fail('InvalidReadiness');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const failure = object(v.failure, ['owner', 'operation', 'code', 'requirementRef', 'detail']);
|
|
55
|
+
for (const k of ['owner', 'operation', 'code', 'requirementRef'])
|
|
56
|
+
text(failure[k]);
|
|
57
|
+
if (failure.owner !== v.owner || failure.requirementRef !== v.ref)
|
|
58
|
+
fail('InvalidReadiness');
|
|
59
|
+
canonical(failure.detail, 32768);
|
|
60
|
+
if (v.setup !== null) {
|
|
61
|
+
const setup = object(v.setup, ['id', 'owner', 'operation']);
|
|
62
|
+
for (const x of Object.values(setup))
|
|
63
|
+
text(x);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return frozen(copy(v));
|
|
44
67
|
}
|
|
45
|
-
export function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
export function readiness(refs, owners) {
|
|
69
|
+
list(refs, bounds.requirements);
|
|
70
|
+
list(owners, bounds.requirements);
|
|
71
|
+
const inventory = owners.map(requirement);
|
|
72
|
+
if (new Set(refs).size !== refs.length || new Set(inventory.map(v => v.ref)).size !== inventory.length)
|
|
73
|
+
fail('InvalidReadiness');
|
|
74
|
+
const required = refs.map(ref => { text(ref); const found = inventory.find(v => v.ref === ref); if (!found)
|
|
75
|
+
fail('UndeclaredRequirement'); return found; });
|
|
76
|
+
return frozen({ state: required.some(v => v.state === 'Unavailable') ? 'Unavailable' : required.some(v => v.state === 'SetupRequired') ? 'SetupRequired' : 'Ready', requirements: required });
|
|
77
|
+
}
|
|
78
|
+
export function validateDelivery(value, expectedContract = 'zixcel/view-delivery/1') {
|
|
79
|
+
canonical(value, bounds.documentBytes);
|
|
80
|
+
const v = object(value, ['contract', 'site', 'initial', 'snapshot', 'readiness', 'assets', 'transport']);
|
|
81
|
+
if (typeof expectedContract !== 'string' || !expectedContract || expectedContract.length > 128 || v.contract !== expectedContract)
|
|
82
|
+
fail('InvalidDelivery');
|
|
83
|
+
const site = object(v.site, ['id', 'revision']);
|
|
84
|
+
text(site.id);
|
|
85
|
+
text(site.revision);
|
|
86
|
+
if (!/^[a-f0-9]{64}$/.test(text(site.revision)))
|
|
87
|
+
fail('InvalidDelivery');
|
|
88
|
+
if (v.snapshot === null) {
|
|
89
|
+
if (v.initial !== null)
|
|
90
|
+
fail('InitialSnapshotMismatch');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const snapshot = validateSnapshot(v.snapshot);
|
|
94
|
+
if (snapshot.kind !== 'Scene' || canonical(v.initial) !== canonical({ key: snapshot.key, revision: snapshot.revision, lineage: snapshot.lineage }))
|
|
95
|
+
fail('InitialSnapshotMismatch');
|
|
96
|
+
}
|
|
97
|
+
const status = object(v.readiness, ['state', 'requirements']);
|
|
98
|
+
const requirements = list(status.requirements, bounds.requirements).map(requirement);
|
|
99
|
+
if (canonical(readiness(requirements.map(r => r.ref), requirements)) !== canonical(v.readiness))
|
|
100
|
+
fail('InvalidReadiness');
|
|
101
|
+
const assets = list(v.assets, bounds.assets).map(a => object(a, ['path', 'digest', 'bytes', 'kind']));
|
|
102
|
+
if (new Set(assets.map(a => a.path)).size !== assets.length)
|
|
103
|
+
fail('InvalidAsset');
|
|
104
|
+
for (const a of assets) {
|
|
105
|
+
shape(a, ['path', 'digest', 'bytes', 'kind']);
|
|
106
|
+
if (typeof a.path !== 'string' || !/^\/assets\/[a-zA-Z0-9._-]{1,100}$/.test(a.path) || typeof a.kind !== 'string' || !['script', 'style'].includes(a.kind)
|
|
107
|
+
|| typeof a.bytes !== 'number' || !Number.isSafeInteger(a.bytes) || a.bytes < 1 || a.bytes > bounds.assetBytes || typeof a.digest !== 'string' || !/^[a-f0-9]{64}$/.test(a.digest))
|
|
108
|
+
fail('InvalidAsset');
|
|
109
|
+
}
|
|
110
|
+
if (canonical(v.transport) !== canonical({ path: '/api/projection-live', classes: ['STATE'] }))
|
|
111
|
+
fail('InvalidDelivery');
|
|
112
|
+
return frozen(copy(v));
|
|
66
113
|
}
|
package/src/types.d.mts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Json, ProjectionIdentity, SceneProjection } from '@zixcel/projection-contracts/client';
|
|
2
|
+
export interface Submission {
|
|
3
|
+
readonly projectionKey: string;
|
|
4
|
+
readonly projectionRevision: string;
|
|
5
|
+
readonly sceneId: string;
|
|
6
|
+
readonly actionId: string;
|
|
7
|
+
readonly interactionRef: string;
|
|
8
|
+
readonly generation: string;
|
|
9
|
+
readonly inputContractRef: string;
|
|
10
|
+
readonly values: Readonly<Record<string, string | boolean | number>>;
|
|
11
|
+
}
|
|
12
|
+
export interface OwnerFailure {
|
|
13
|
+
readonly owner: string;
|
|
14
|
+
readonly operation: string;
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly requirementRef: string;
|
|
17
|
+
readonly detail: Json;
|
|
18
|
+
}
|
|
19
|
+
export interface Setup {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly owner: string;
|
|
22
|
+
readonly operation: string;
|
|
23
|
+
}
|
|
24
|
+
export type Requirement = {
|
|
25
|
+
readonly ref: string;
|
|
26
|
+
readonly owner: string;
|
|
27
|
+
readonly state: 'Ready';
|
|
28
|
+
readonly failure: null;
|
|
29
|
+
readonly setup: null;
|
|
30
|
+
} | {
|
|
31
|
+
readonly ref: string;
|
|
32
|
+
readonly owner: string;
|
|
33
|
+
readonly state: 'SetupRequired' | 'Unavailable';
|
|
34
|
+
readonly failure: OwnerFailure;
|
|
35
|
+
readonly setup: Setup | null;
|
|
36
|
+
};
|
|
37
|
+
export interface Readiness {
|
|
38
|
+
readonly state: 'Ready' | 'SetupRequired' | 'Unavailable';
|
|
39
|
+
readonly requirements: readonly Requirement[];
|
|
40
|
+
}
|
|
41
|
+
export interface Asset {
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly digest: string;
|
|
44
|
+
readonly bytes: number;
|
|
45
|
+
readonly kind: 'script' | 'style';
|
|
46
|
+
}
|
|
47
|
+
export interface Delivery {
|
|
48
|
+
readonly contract: string;
|
|
49
|
+
readonly site: {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly revision: string;
|
|
52
|
+
};
|
|
53
|
+
readonly initial: ProjectionIdentity | null;
|
|
54
|
+
readonly snapshot: SceneProjection | null;
|
|
55
|
+
readonly readiness: Readiness;
|
|
56
|
+
readonly assets: readonly Asset[];
|
|
57
|
+
readonly transport: {
|
|
58
|
+
readonly path: '/api/projection-live';
|
|
59
|
+
readonly classes: readonly ['STATE'];
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|