@moldesk/visualizer 0.0.0-stage → 0.0.5

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.
@@ -0,0 +1,26 @@
1
+ export declare const TEMPLATE_VERSION = 1;
2
+ export declare const ASSET_NAMES: readonly ["viewer.css", "viewer.js"];
3
+ export interface BundleMetadata {
4
+ viewerVersion: string;
5
+ molstarVersion: string;
6
+ templateVersion: number;
7
+ compatible: {
8
+ envelopeVersions: number[];
9
+ schemaVersions: number[];
10
+ };
11
+ assets: Record<(typeof ASSET_NAMES)[number], string>;
12
+ bundleSha256: string;
13
+ }
14
+ export interface ViewerBundle {
15
+ metadata: BundleMetadata;
16
+ js: string;
17
+ css: string;
18
+ }
19
+ export declare class BundleError extends Error {
20
+ readonly code = "VISUALIZER_BUNDLE_INVALID";
21
+ constructor(message: string);
22
+ }
23
+ export declare function sha256Hex(data: string | Uint8Array): string;
24
+ export declare function computeBundleSha256(assets: Record<string, string>): string;
25
+ export declare function defaultAssetsDir(): string;
26
+ export declare function loadBundle(assetsDir?: string): ViewerBundle;
package/dist/bundle.js ADDED
@@ -0,0 +1,48 @@
1
+ // Locates and verifies the installed viewer bundle (built once at package build time).
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import path from "node:path";
6
+ export const TEMPLATE_VERSION = 1;
7
+ export const ASSET_NAMES = ["viewer.css", "viewer.js"]; // explicit digest order
8
+ export class BundleError extends Error {
9
+ code = "VISUALIZER_BUNDLE_INVALID";
10
+ constructor(message) {
11
+ super(`${message}. The installed MoleculeDesk viewer is missing or damaged; reinstall it with "npm install -g @moldesk/cli".`);
12
+ this.name = "BundleError";
13
+ }
14
+ }
15
+ export function sha256Hex(data) {
16
+ return createHash("sha256").update(data).digest("hex");
17
+ }
18
+ export function computeBundleSha256(assets) {
19
+ const lines = ASSET_NAMES.map((name) => `${name}:${assets[name]}\n`).join("");
20
+ return sha256Hex(lines);
21
+ }
22
+ export function defaultAssetsDir() {
23
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "assets");
24
+ }
25
+ export function loadBundle(assetsDir = defaultAssetsDir()) {
26
+ let metadata;
27
+ let js;
28
+ let css;
29
+ try {
30
+ metadata = JSON.parse(readFileSync(path.join(assetsDir, "bundle.json"), "utf8"));
31
+ js = readFileSync(path.join(assetsDir, "viewer.js"), "utf8");
32
+ css = readFileSync(path.join(assetsDir, "viewer.css"), "utf8");
33
+ }
34
+ catch {
35
+ throw new BundleError("Could not read viewer assets");
36
+ }
37
+ const actual = { "viewer.js": sha256Hex(js), "viewer.css": sha256Hex(css) };
38
+ if (!metadata?.assets ||
39
+ metadata.assets["viewer.js"] !== actual["viewer.js"] ||
40
+ metadata.assets["viewer.css"] !== actual["viewer.css"] ||
41
+ metadata.bundleSha256 !== computeBundleSha256(actual)) {
42
+ throw new BundleError("Viewer assets do not match their recorded digests");
43
+ }
44
+ if (metadata.templateVersion !== TEMPLATE_VERSION) {
45
+ throw new BundleError("Viewer assets were built for a different report template");
46
+ }
47
+ return { metadata, js, css };
48
+ }
@@ -0,0 +1,146 @@
1
+ export { REPORT_LIMITS } from "./limits.js";
2
+ export type MolecularFormat = "pdb" | "mmcif" | "sdf";
3
+ export type DataFormat = "json" | "csv";
4
+ export type ResourceFormat = MolecularFormat | DataFormat;
5
+ export type SourceRef = {
6
+ kind: "output";
7
+ path: string;
8
+ } | {
9
+ kind: "input";
10
+ path: string;
11
+ };
12
+ export type MolecularSelection = {
13
+ kind: "all";
14
+ } | {
15
+ kind: "molecule";
16
+ type: "protein" | "dna" | "rna" | "nucleic" | "ligand" | "ion" | "water";
17
+ } | {
18
+ kind: "chains";
19
+ namespace: "label" | "auth";
20
+ ids: string[];
21
+ } | {
22
+ kind: "entities";
23
+ ids: string[];
24
+ } | {
25
+ kind: "residues";
26
+ residues: ResidueRef[];
27
+ };
28
+ export interface ResidueRef {
29
+ chainNamespace: "label" | "auth";
30
+ chainId: string;
31
+ residueNamespace: "label" | "auth";
32
+ sequenceId: number;
33
+ insertionCode?: string;
34
+ }
35
+ export interface SceneStructure {
36
+ id: string;
37
+ resourceId: string;
38
+ /** Zero-based model index within the source file. */
39
+ modelIndex?: number;
40
+ assemblyId?: string;
41
+ /** 16 finite values, column-major (Mol* Mat4 / WebGL convention). */
42
+ transform?: number[];
43
+ }
44
+ export interface SceneComponent {
45
+ id: string;
46
+ structureId: string;
47
+ selection: MolecularSelection;
48
+ label?: string;
49
+ role?: "target" | "binder" | "receptor" | "ligand";
50
+ representation?: "cartoon" | "sticks" | "surface";
51
+ /** `#rrggbb` only. */
52
+ color?: string;
53
+ }
54
+ export interface Scene {
55
+ id: string;
56
+ title: string;
57
+ /** Only set from verified ranking information. */
58
+ rank?: number;
59
+ structures: SceneStructure[];
60
+ components?: SceneComponent[];
61
+ }
62
+ export interface DatasetRef {
63
+ resourceId: string;
64
+ /** Own array property of a JSON root object. */
65
+ rowsKey?: string;
66
+ }
67
+ export interface Column {
68
+ field: string;
69
+ label: string;
70
+ unit?: string;
71
+ }
72
+ export type ChartSpec = {
73
+ type: "line" | "scatter" | "bar";
74
+ x: Column;
75
+ y: Column;
76
+ seriesField?: string;
77
+ } | {
78
+ type: "heatmap";
79
+ x: Column;
80
+ y: Column;
81
+ value: Column;
82
+ };
83
+ export type Panel = {
84
+ id: string;
85
+ kind: "structure";
86
+ sceneIds: string[];
87
+ } | {
88
+ id: string;
89
+ kind: "table";
90
+ title?: string;
91
+ dataset: DatasetRef;
92
+ columns: Column[];
93
+ } | {
94
+ id: string;
95
+ kind: "chart";
96
+ title?: string;
97
+ dataset: DatasetRef;
98
+ chart: ChartSpec;
99
+ };
100
+ /** What an adapter returns: references to files, resolved by the exporter. */
101
+ export interface ReportDescription {
102
+ title: string;
103
+ resources: Array<{
104
+ id: string;
105
+ source: SourceRef;
106
+ format: ResourceFormat;
107
+ label: string;
108
+ }>;
109
+ scenes: Scene[];
110
+ panels: Panel[];
111
+ }
112
+ export interface ReportResource {
113
+ id: string;
114
+ label: string;
115
+ format: ResourceFormat;
116
+ /** SHA-256 (hex) of the original bytes. */
117
+ sha256: string;
118
+ sizeBytes: number;
119
+ encoding: "base64";
120
+ /** Original bytes, base64. Never regenerated coordinates. */
121
+ data: string;
122
+ }
123
+ export interface MolecularReportV1 {
124
+ schemaVersion: 1;
125
+ id: string;
126
+ title: string;
127
+ resources: ReportResource[];
128
+ scenes: Scene[];
129
+ panels: Panel[];
130
+ provenance: {
131
+ createdAt: string;
132
+ runId?: string;
133
+ model?: string;
134
+ modelVersion?: string;
135
+ };
136
+ }
137
+ export declare class ContractError extends Error {
138
+ readonly code = "INVALID_REPORT";
139
+ constructor(message: string);
140
+ }
141
+ export declare function isMolecularFormat(format: string): format is MolecularFormat;
142
+ export declare function isDataFormat(format: string): format is DataFormat;
143
+ /** Validates an adapter-provided description. Sources are checked for shape only. */
144
+ export declare function validateDescription(value: unknown): ReportDescription;
145
+ /** Structural validation of an exported report. Hash/size integrity is checked by the decoder. */
146
+ export declare function validateReport(value: unknown): MolecularReportV1;
@@ -0,0 +1,433 @@
1
+ // Browser-neutral report contract: types plus runtime validation.
2
+ // No Node or DOM imports; used by adapters (types), the exporter and the viewer.
3
+ import { REPORT_LIMITS as L } from "./limits.js";
4
+ export { REPORT_LIMITS } from "./limits.js";
5
+ export class ContractError extends Error {
6
+ code = "INVALID_REPORT";
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ContractError";
10
+ }
11
+ }
12
+ const MOLECULAR_FORMATS = ["pdb", "mmcif", "sdf"];
13
+ const DATA_FORMATS = ["json", "csv"];
14
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
15
+ const COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
16
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
17
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
18
+ const URL_LIKE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
19
+ export function isMolecularFormat(format) {
20
+ return MOLECULAR_FORMATS.includes(format);
21
+ }
22
+ export function isDataFormat(format) {
23
+ return DATA_FORMATS.includes(format);
24
+ }
25
+ function fail(where, message) {
26
+ throw new ContractError(`${where}: ${message}`);
27
+ }
28
+ function isRecord(value) {
29
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30
+ }
31
+ /** Reads only own, non-prototype-sensitive properties; rejects unknown keys. */
32
+ function object(value, where, allowed) {
33
+ if (!isRecord(value))
34
+ fail(where, "expected an object");
35
+ for (const key of Object.keys(value)) {
36
+ if (FORBIDDEN_KEYS.has(key))
37
+ fail(where, `forbidden key "${key}"`);
38
+ if (!allowed.includes(key))
39
+ fail(where, `unknown key "${key}"`);
40
+ }
41
+ return value;
42
+ }
43
+ function own(obj, key) {
44
+ return Object.hasOwn(obj, key) ? obj[key] : undefined;
45
+ }
46
+ function str(value, where, max, opts = {}) {
47
+ if (typeof value !== "string")
48
+ fail(where, "expected a string");
49
+ if (!opts.empty && value.length === 0)
50
+ fail(where, "must not be empty");
51
+ if (value.length > max)
52
+ fail(where, `longer than ${max} characters`);
53
+ return value;
54
+ }
55
+ function id(value, where) {
56
+ const s = str(value, where, L.maxIdLength);
57
+ if (!ID_PATTERN.test(s))
58
+ fail(where, "invalid identifier");
59
+ return s;
60
+ }
61
+ function list(value, where, max) {
62
+ if (!Array.isArray(value))
63
+ fail(where, "expected an array");
64
+ if (value.length > max)
65
+ fail(where, `more than ${max} entries`);
66
+ return value;
67
+ }
68
+ function finite(value, where) {
69
+ if (typeof value !== "number" || !Number.isFinite(value))
70
+ fail(where, "expected a finite number");
71
+ return value;
72
+ }
73
+ function int(value, where, min = 0) {
74
+ const n = finite(value, where);
75
+ if (!Number.isInteger(n) || n < min)
76
+ fail(where, `expected an integer >= ${min}`);
77
+ return n;
78
+ }
79
+ function oneOf(value, where, allowed) {
80
+ if (typeof value !== "string" || !allowed.includes(value)) {
81
+ fail(where, `expected one of ${allowed.join(", ")}`);
82
+ }
83
+ return value;
84
+ }
85
+ function unique(set, value, where) {
86
+ if (set.has(value))
87
+ fail(where, `duplicate id "${value}"`);
88
+ set.add(value);
89
+ }
90
+ function validateColumn(value, where) {
91
+ const o = object(value, where, ["field", "label", "unit"]);
92
+ const unit = own(o, "unit");
93
+ return {
94
+ field: str(own(o, "field"), `${where}.field`, L.maxFieldLength),
95
+ label: str(own(o, "label"), `${where}.label`, L.maxLabelLength),
96
+ ...(unit === undefined ? {} : { unit: str(unit, `${where}.unit`, 32) }),
97
+ };
98
+ }
99
+ function validateSelection(value, where) {
100
+ if (!isRecord(value))
101
+ fail(where, "expected an object");
102
+ const kind = oneOf(own(value, "kind"), `${where}.kind`, [
103
+ "all",
104
+ "molecule",
105
+ "chains",
106
+ "entities",
107
+ "residues",
108
+ ]);
109
+ switch (kind) {
110
+ case "all":
111
+ object(value, where, ["kind"]);
112
+ return { kind };
113
+ case "molecule": {
114
+ const o = object(value, where, ["kind", "type"]);
115
+ const type = oneOf(own(o, "type"), `${where}.type`, [
116
+ "protein",
117
+ "dna",
118
+ "rna",
119
+ "nucleic",
120
+ "ligand",
121
+ "ion",
122
+ "water",
123
+ ]);
124
+ return { kind, type };
125
+ }
126
+ case "chains": {
127
+ const o = object(value, where, ["kind", "namespace", "ids"]);
128
+ return {
129
+ kind,
130
+ namespace: oneOf(own(o, "namespace"), `${where}.namespace`, ["label", "auth"]),
131
+ ids: list(own(o, "ids"), `${where}.ids`, L.maxSelectionIds).map((v, i) => str(v, `${where}.ids[${i}]`, 16)),
132
+ };
133
+ }
134
+ case "entities": {
135
+ const o = object(value, where, ["kind", "ids"]);
136
+ return {
137
+ kind,
138
+ ids: list(own(o, "ids"), `${where}.ids`, L.maxSelectionIds).map((v, i) => str(v, `${where}.ids[${i}]`, 16)),
139
+ };
140
+ }
141
+ case "residues": {
142
+ const o = object(value, where, ["kind", "residues"]);
143
+ const residues = list(own(o, "residues"), `${where}.residues`, L.maxResidueRefs).map((v, i) => {
144
+ const w = `${where}.residues[${i}]`;
145
+ const r = object(v, w, [
146
+ "chainNamespace",
147
+ "chainId",
148
+ "residueNamespace",
149
+ "sequenceId",
150
+ "insertionCode",
151
+ ]);
152
+ const ins = own(r, "insertionCode");
153
+ return {
154
+ chainNamespace: oneOf(own(r, "chainNamespace"), `${w}.chainNamespace`, ["label", "auth"]),
155
+ chainId: str(own(r, "chainId"), `${w}.chainId`, 16),
156
+ residueNamespace: oneOf(own(r, "residueNamespace"), `${w}.residueNamespace`, [
157
+ "label",
158
+ "auth",
159
+ ]),
160
+ sequenceId: int(own(r, "sequenceId"), `${w}.sequenceId`, -1_000_000),
161
+ ...(ins === undefined ? {} : { insertionCode: str(ins, `${w}.insertionCode`, 1) }),
162
+ };
163
+ });
164
+ return { kind, residues };
165
+ }
166
+ }
167
+ }
168
+ function validateScene(value, where, resourceFormats) {
169
+ const o = object(value, where, ["id", "title", "rank", "structures", "components"]);
170
+ const sceneId = id(own(o, "id"), `${where}.id`);
171
+ const rank = own(o, "rank");
172
+ const structureIds = new Set();
173
+ const structures = list(own(o, "structures"), `${where}.structures`, L.maxStructuresPerScene).map((v, i) => {
174
+ const w = `${where}.structures[${i}]`;
175
+ const s = object(v, w, ["id", "resourceId", "modelIndex", "assemblyId", "transform"]);
176
+ const structureId = id(own(s, "id"), `${w}.id`);
177
+ unique(structureIds, structureId, w);
178
+ const resourceId = id(own(s, "resourceId"), `${w}.resourceId`);
179
+ const format = resourceFormats.get(resourceId);
180
+ if (format === undefined)
181
+ fail(w, `unknown resource "${resourceId}"`);
182
+ if (!isMolecularFormat(format))
183
+ fail(w, `resource "${resourceId}" is not a molecular format`);
184
+ const modelIndex = own(s, "modelIndex");
185
+ const assemblyId = own(s, "assemblyId");
186
+ const transform = own(s, "transform");
187
+ return {
188
+ id: structureId,
189
+ resourceId,
190
+ ...(modelIndex === undefined ? {} : { modelIndex: int(modelIndex, `${w}.modelIndex`) }),
191
+ ...(assemblyId === undefined ? {} : { assemblyId: str(assemblyId, `${w}.assemblyId`, 32) }),
192
+ ...(transform === undefined
193
+ ? {}
194
+ : {
195
+ transform: (() => {
196
+ const t = list(transform, `${w}.transform`, 16);
197
+ if (t.length !== 16)
198
+ fail(`${w}.transform`, "expected exactly 16 values");
199
+ return t.map((n, k) => finite(n, `${w}.transform[${k}]`));
200
+ })(),
201
+ }),
202
+ };
203
+ });
204
+ if (structures.length === 0)
205
+ fail(where, "scene needs at least one structure");
206
+ const componentIds = new Set();
207
+ const rawComponents = own(o, "components");
208
+ const components = rawComponents === undefined
209
+ ? undefined
210
+ : list(rawComponents, `${where}.components`, L.maxComponentsPerScene).map((v, i) => {
211
+ const w = `${where}.components[${i}]`;
212
+ const c = object(v, w, ["id", "structureId", "selection", "label", "role", "representation", "color"]);
213
+ const componentId = id(own(c, "id"), `${w}.id`);
214
+ unique(componentIds, componentId, w);
215
+ const structureId = id(own(c, "structureId"), `${w}.structureId`);
216
+ if (!structureIds.has(structureId))
217
+ fail(w, `unknown structure "${structureId}"`);
218
+ const label = own(c, "label");
219
+ const role = own(c, "role");
220
+ const representation = own(c, "representation");
221
+ const color = own(c, "color");
222
+ if (color !== undefined && (typeof color !== "string" || !COLOR_PATTERN.test(color))) {
223
+ fail(`${w}.color`, "expected #rrggbb");
224
+ }
225
+ return {
226
+ id: componentId,
227
+ structureId,
228
+ selection: validateSelection(own(c, "selection"), `${w}.selection`),
229
+ ...(label === undefined ? {} : { label: str(label, `${w}.label`, L.maxLabelLength) }),
230
+ ...(role === undefined
231
+ ? {}
232
+ : { role: oneOf(role, `${w}.role`, ["target", "binder", "receptor", "ligand"]) }),
233
+ ...(representation === undefined
234
+ ? {}
235
+ : {
236
+ representation: oneOf(representation, `${w}.representation`, [
237
+ "cartoon",
238
+ "sticks",
239
+ "surface",
240
+ ]),
241
+ }),
242
+ ...(color === undefined ? {} : { color: color }),
243
+ };
244
+ });
245
+ return {
246
+ id: sceneId,
247
+ title: str(own(o, "title"), `${where}.title`, L.maxLabelLength),
248
+ ...(rank === undefined ? {} : { rank: int(rank, `${where}.rank`, 1) }),
249
+ structures,
250
+ ...(components === undefined ? {} : { components }),
251
+ };
252
+ }
253
+ function validateDataset(value, where, resourceFormats) {
254
+ const o = object(value, where, ["resourceId", "rowsKey"]);
255
+ const resourceId = id(own(o, "resourceId"), `${where}.resourceId`);
256
+ const format = resourceFormats.get(resourceId);
257
+ if (format === undefined)
258
+ fail(where, `unknown resource "${resourceId}"`);
259
+ if (!isDataFormat(format))
260
+ fail(where, `resource "${resourceId}" is not a data format`);
261
+ const rowsKey = own(o, "rowsKey");
262
+ if (rowsKey !== undefined) {
263
+ const key = str(rowsKey, `${where}.rowsKey`, L.maxFieldLength);
264
+ if (FORBIDDEN_KEYS.has(key))
265
+ fail(`${where}.rowsKey`, "forbidden key");
266
+ if (format !== "json")
267
+ fail(`${where}.rowsKey`, "only valid for json resources");
268
+ return { resourceId, rowsKey: key };
269
+ }
270
+ return { resourceId };
271
+ }
272
+ function validatePanel(value, where, resourceFormats, sceneIds) {
273
+ if (!isRecord(value))
274
+ fail(where, "expected an object");
275
+ const kind = oneOf(own(value, "kind"), `${where}.kind`, ["structure", "table", "chart"]);
276
+ if (kind === "structure") {
277
+ const o = object(value, where, ["id", "kind", "sceneIds"]);
278
+ const ids = list(own(o, "sceneIds"), `${where}.sceneIds`, L.maxScenes).map((v, i) => {
279
+ const sid = id(v, `${where}.sceneIds[${i}]`);
280
+ if (!sceneIds.has(sid))
281
+ fail(where, `unknown scene "${sid}"`);
282
+ return sid;
283
+ });
284
+ return { id: id(own(o, "id"), `${where}.id`), kind, sceneIds: ids };
285
+ }
286
+ if (kind === "table") {
287
+ const o = object(value, where, ["id", "kind", "title", "dataset", "columns"]);
288
+ const title = own(o, "title");
289
+ return {
290
+ id: id(own(o, "id"), `${where}.id`),
291
+ kind,
292
+ ...(title === undefined ? {} : { title: str(title, `${where}.title`, L.maxLabelLength) }),
293
+ dataset: validateDataset(own(o, "dataset"), `${where}.dataset`, resourceFormats),
294
+ columns: list(own(o, "columns"), `${where}.columns`, L.maxColumns).map((c, i) => validateColumn(c, `${where}.columns[${i}]`)),
295
+ };
296
+ }
297
+ const o = object(value, where, ["id", "kind", "title", "dataset", "chart"]);
298
+ const title = own(o, "title");
299
+ const chartRaw = own(o, "chart");
300
+ if (!isRecord(chartRaw))
301
+ fail(`${where}.chart`, "expected an object");
302
+ const type = oneOf(own(chartRaw, "type"), `${where}.chart.type`, [
303
+ "line",
304
+ "scatter",
305
+ "bar",
306
+ "heatmap",
307
+ ]);
308
+ let chart;
309
+ if (type === "heatmap") {
310
+ const c = object(chartRaw, `${where}.chart`, ["type", "x", "y", "value"]);
311
+ chart = {
312
+ type,
313
+ x: validateColumn(own(c, "x"), `${where}.chart.x`),
314
+ y: validateColumn(own(c, "y"), `${where}.chart.y`),
315
+ value: validateColumn(own(c, "value"), `${where}.chart.value`),
316
+ };
317
+ }
318
+ else {
319
+ const c = object(chartRaw, `${where}.chart`, ["type", "x", "y", "seriesField"]);
320
+ const seriesField = own(c, "seriesField");
321
+ chart = {
322
+ type,
323
+ x: validateColumn(own(c, "x"), `${where}.chart.x`),
324
+ y: validateColumn(own(c, "y"), `${where}.chart.y`),
325
+ ...(seriesField === undefined
326
+ ? {}
327
+ : { seriesField: str(seriesField, `${where}.chart.seriesField`, L.maxFieldLength) }),
328
+ };
329
+ }
330
+ return {
331
+ id: id(own(o, "id"), `${where}.id`),
332
+ kind,
333
+ ...(title === undefined ? {} : { title: str(title, `${where}.title`, L.maxLabelLength) }),
334
+ dataset: validateDataset(own(o, "dataset"), `${where}.dataset`, resourceFormats),
335
+ chart,
336
+ };
337
+ }
338
+ function validateScenesAndPanels(scenesRaw, panelsRaw, resourceFormats) {
339
+ const sceneIds = new Set();
340
+ const scenes = list(scenesRaw, "scenes", L.maxScenes).map((s, i) => {
341
+ const scene = validateScene(s, `scenes[${i}]`, resourceFormats);
342
+ unique(sceneIds, scene.id, `scenes[${i}]`);
343
+ return scene;
344
+ });
345
+ const panelIds = new Set();
346
+ const panels = list(panelsRaw, "panels", L.maxPanels).map((p, i) => {
347
+ const panel = validatePanel(p, `panels[${i}]`, resourceFormats, sceneIds);
348
+ unique(panelIds, panel.id, `panels[${i}]`);
349
+ return panel;
350
+ });
351
+ return { scenes, panels };
352
+ }
353
+ function validateResourceHeader(o, where) {
354
+ return {
355
+ id: id(own(o, "id"), `${where}.id`),
356
+ label: str(own(o, "label"), `${where}.label`, L.maxLabelLength),
357
+ format: oneOf(own(o, "format"), `${where}.format`, [...MOLECULAR_FORMATS, ...DATA_FORMATS]),
358
+ };
359
+ }
360
+ /** Validates an adapter-provided description. Sources are checked for shape only. */
361
+ export function validateDescription(value) {
362
+ const o = object(value, "description", ["title", "resources", "scenes", "panels"]);
363
+ const formats = new Map();
364
+ const resources = list(own(o, "resources"), "resources", L.maxResources).map((r, i) => {
365
+ const w = `resources[${i}]`;
366
+ const ro = object(r, w, ["id", "source", "format", "label"]);
367
+ const header = validateResourceHeader(ro, w);
368
+ unique(new Set(formats.keys()), header.id, w);
369
+ formats.set(header.id, header.format);
370
+ const so = object(own(ro, "source"), `${w}.source`, ["kind", "path"]);
371
+ const path = str(own(so, "path"), `${w}.source.path`, 4096);
372
+ if (URL_LIKE.test(path) && !/^[a-zA-Z]:[\\/]/.test(path))
373
+ fail(`${w}.source.path`, "URLs are not allowed");
374
+ return {
375
+ id: header.id,
376
+ source: { kind: oneOf(own(so, "kind"), `${w}.source.kind`, ["output", "input"]), path },
377
+ format: header.format,
378
+ label: header.label,
379
+ };
380
+ });
381
+ const { scenes, panels } = validateScenesAndPanels(own(o, "scenes"), own(o, "panels"), formats);
382
+ return { title: str(own(o, "title"), "title", L.maxTitleLength), resources, scenes, panels };
383
+ }
384
+ /** Structural validation of an exported report. Hash/size integrity is checked by the decoder. */
385
+ export function validateReport(value) {
386
+ const o = object(value, "report", ["schemaVersion", "id", "title", "resources", "scenes", "panels", "provenance"]);
387
+ if (own(o, "schemaVersion") !== 1)
388
+ fail("report.schemaVersion", "unsupported schema version");
389
+ const formats = new Map();
390
+ let total = 0;
391
+ const resources = list(own(o, "resources"), "resources", L.maxResources).map((r, i) => {
392
+ const w = `resources[${i}]`;
393
+ const ro = object(r, w, ["id", "label", "format", "sha256", "sizeBytes", "encoding", "data"]);
394
+ const header = validateResourceHeader(ro, w);
395
+ if (formats.has(header.id))
396
+ fail(w, `duplicate id "${header.id}"`);
397
+ formats.set(header.id, header.format);
398
+ const sha256 = own(ro, "sha256");
399
+ if (typeof sha256 !== "string" || !SHA256_PATTERN.test(sha256))
400
+ fail(`${w}.sha256`, "expected lowercase hex SHA-256");
401
+ const sizeBytes = int(own(ro, "sizeBytes"), `${w}.sizeBytes`);
402
+ if (sizeBytes > L.maxResourceBytes)
403
+ fail(w, "resource exceeds size limit");
404
+ total += sizeBytes;
405
+ if (total > L.maxTotalResourceBytes)
406
+ fail("resources", "total size exceeds limit");
407
+ if (own(ro, "encoding") !== "base64")
408
+ fail(`${w}.encoding`, "expected base64");
409
+ const data = str(own(ro, "data"), `${w}.data`, Math.ceil((L.maxResourceBytes * 4) / 3) + 8, { empty: true });
410
+ if (Math.ceil(sizeBytes / 3) * 4 !== data.length)
411
+ fail(w, "data length does not match sizeBytes");
412
+ return { ...header, sha256, sizeBytes, encoding: "base64", data };
413
+ });
414
+ const { scenes, panels } = validateScenesAndPanels(own(o, "scenes"), own(o, "panels"), formats);
415
+ const p = object(own(o, "provenance"), "provenance", ["createdAt", "runId", "model", "modelVersion"]);
416
+ const runId = own(p, "runId");
417
+ const model = own(p, "model");
418
+ const modelVersion = own(p, "modelVersion");
419
+ return {
420
+ schemaVersion: 1,
421
+ id: id(own(o, "id"), "id"),
422
+ title: str(own(o, "title"), "title", L.maxTitleLength),
423
+ resources,
424
+ scenes,
425
+ panels,
426
+ provenance: {
427
+ createdAt: str(own(p, "createdAt"), "provenance.createdAt", 64),
428
+ ...(runId === undefined ? {} : { runId: str(runId, "provenance.runId", 128) }),
429
+ ...(model === undefined ? {} : { model: str(model, "provenance.model", 128) }),
430
+ ...(modelVersion === undefined ? {} : { modelVersion: str(modelVersion, "provenance.modelVersion", 128) }),
431
+ },
432
+ };
433
+ }
@@ -0,0 +1 @@
1
+ export {};