@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zixcel/view-delivery-contracts",
3
- "version": "0.10.2",
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.3",
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 {Action, Json, ProjectionIdentity, SceneProjection} from '@zixcel/projection-contracts/client'
2
-
3
- export interface Submission {
4
- readonly projectionKey:string
5
- readonly projectionRevision:string
6
- readonly sceneId:string
7
- readonly actionId:string
8
- readonly interactionRef:string
9
- readonly generation:string
10
- readonly inputContractRef:string
11
- readonly values:Readonly<Record<string,string|boolean|number>>
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 interface OwnerFailure {
14
- readonly owner:string
15
- readonly operation:string
16
- readonly code:string
17
- readonly requirementRef:string
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 {constructor(code){super(code);this.code=code}}
7
- const fail=code=>{throw new DeliveryError(code)}
8
- const list=(v,maximum)=>{if(!Array.isArray(v)||v.length>maximum)fail('DeliveryLimitExceeded');return v}
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
- export function inputAction(snapshot,submission){
17
- validateSubmission(submission);validateSnapshot(snapshot)
18
- if(snapshot.kind!=='Scene'||snapshot.key!==submission.sceneId||snapshot.revision!==submission.projectionRevision)fail('StaleSceneAction')
19
- const action=snapshot.data.actions.find(a=>a.id===submission.actionId),binding=action?.interaction
20
- if(!binding)fail('InteractionUnavailable')
21
- if(binding.ref!==submission.interactionRef||binding.generation!==submission.generation||binding.inputContractRef!==submission.inputContractRef)fail('StaleSceneAction')
22
- if(validateInputValues(binding.input,submission.values).length)fail('InvalidInput')
23
- return action
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 requirement(v){
26
- shape(v,['ref','owner','state','failure','setup']);text(v.ref);text(v.owner)
27
- if(!['Ready','SetupRequired','Unavailable'].includes(v.state))fail('InvalidReadiness')
28
- if(v.state==='Ready'){if(v.failure!==null||v.setup!==null)fail('InvalidReadiness')}
29
- else{
30
- shape(v.failure,['owner','operation','code','requirementRef','detail'])
31
- for(const k of ['owner','operation','code','requirementRef'])text(v.failure[k])
32
- if(v.failure.owner!==v.owner||v.failure.requirementRef!==v.ref)fail('InvalidReadiness')
33
- canonical(v.failure.detail,32768)
34
- if(v.setup!==null){shape(v.setup,['id','owner','operation']);for(const x of Object.values(v.setup))text(x)}
35
- }
36
- return frozen(copy(v))
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 readiness(refs,owners){
39
- list(refs,bounds.requirements);list(owners,bounds.requirements)
40
- const inventory=owners.map(requirement)
41
- if(new Set(refs).size!==refs.length||new Set(inventory.map(v=>v.ref)).size!==inventory.length)fail('InvalidReadiness')
42
- const required=refs.map(ref=>{text(ref);const found=inventory.find(v=>v.ref===ref);if(!found)fail('UndeclaredRequirement');return found})
43
- return frozen({state:required.some(v=>v.state==='Unavailable')?'Unavailable':required.some(v=>v.state==='SetupRequired')?'SetupRequired':'Ready',requirements:required})
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 validateDelivery(v,expectedContract='zixcel/view-delivery/1'){
46
- canonical(v,bounds.documentBytes);shape(v,['contract','site','initial','snapshot','readiness','assets','transport'])
47
- if(typeof expectedContract!=='string'||!expectedContract||expectedContract.length>128||v.contract!==expectedContract)fail('InvalidDelivery')
48
- shape(v.site,['id','revision']);text(v.site.id);text(v.site.revision)
49
- if(!/^[a-f0-9]{64}$/.test(v.site.revision))fail('InvalidDelivery')
50
- if(v.snapshot===null){if(v.initial!==null)fail('InitialSnapshotMismatch')}
51
- else{
52
- validateSnapshot(v.snapshot)
53
- if(v.snapshot.kind!=='Scene'||canonical(v.initial)!==canonical({key:v.snapshot.key,revision:v.snapshot.revision,lineage:v.snapshot.lineage}))fail('InitialSnapshotMismatch')
54
- }
55
- shape(v.readiness,['state','requirements'])
56
- if(canonical(readiness(v.readiness.requirements.map(r=>r.ref),v.readiness.requirements))!==canonical(v.readiness))fail('InvalidReadiness')
57
- list(v.assets,bounds.assets)
58
- if(new Set(v.assets.map(a=>a.path)).size!==v.assets.length)fail('InvalidAsset')
59
- for(const a of v.assets){
60
- shape(a,['path','digest','bytes','kind'])
61
- if(!/^\/assets\/[a-zA-Z0-9._-]{1,100}$/.test(a.path)||!['script','style'].includes(a.kind)
62
- ||!Number.isSafeInteger(a.bytes)||a.bytes<1||a.bytes>bounds.assetBytes||!/^[a-f0-9]{64}$/.test(a.digest))fail('InvalidAsset')
63
- }
64
- if(canonical(v.transport)!==canonical({path:'/api/projection-live',classes:['STATE']}))fail('InvalidDelivery')
65
- return frozen(copy(v))
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
  }
@@ -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 {};