space-data-module-sdk 0.8.6 → 0.8.8
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/README.md +13 -0
- package/docs/AGENTS.md +5 -0
- package/docs/browser-wasmedge-isomorphic.md +65 -1
- package/docs/language-runtime-matrix.md +2 -2
- package/docs/provider-access-abi.md +600 -0
- package/package.json +15 -6
- package/schemas/orbpro/Propagator.fbs +19 -3
- package/src/browser.js +14 -4
- package/src/bundle/artifactBytes.js +44 -0
- package/src/bundle/index.js +1 -0
- package/src/compiler/compileModule.js +24 -1
- package/src/flow/flowCompiler.js +141 -2
- package/src/flow/flowRuntimeHost.js +7 -1
- package/src/flow/isomorphicFlowHost.js +1 -1
- package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
- package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
- package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
- package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
- package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
- package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
- package/src/{testing → host}/browserModuleHarness.js +69 -34
- package/src/host/index.js +6 -0
- package/src/host/isomorphicLoader.js +17 -45
- package/src/host/isomorphicLoaderBrowser.js +47 -0
- package/src/host/isomorphicLoaderCore.js +58 -0
- package/src/host/nodeBuiltinSpecifier.js +43 -0
- package/src/host/providerAccess.js +727 -0
- package/src/host/providerAccessAbi.js +403 -0
- package/src/host/providerAccessEngineAdapter.js +338 -0
- package/src/host/providerAccessFixtureAdapter.js +444 -0
- package/src/host/providerAccessTileStoreAdapter.js +366 -0
- package/src/host/sabHostcallChannel.js +1 -1
- package/src/host/terrainSourceSeam.js +205 -0
- package/src/host/wasiThreadHost.js +11 -1
- package/src/{testing → host}/workerModuleHarness.js +45 -12
- package/src/{testing → host}/workerModuleHarnessWorker.js +6 -5
- package/src/index.d.ts +29 -2
- package/src/standards/browser.js +95 -0
- package/src/standards/catalogCore.js +290 -0
- package/src/standards/index.js +27 -251
- package/src/testing/AGENTS.md +25 -2
- package/src/testing/browser.js +32 -0
- package/src/testing/index.d.ts +12 -1
- package/src/testing/index.js +3 -3
- package/src/testing/parityBrowserRunner.js +9 -2
- package/src/testing/parityHarness.js +1 -1
- package/templates/provider-access-module/include/space_data_provider_abi.h +227 -0
- /package/src/{testing → host}/moduleFlatbufferStreamPump.js +0 -0
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
* cross-origin isolated (COOP+COEP). In Node, worker_threads is used.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { createBrowserHost } from "
|
|
22
|
-
import { createAsyncHostDispatcher } from "
|
|
21
|
+
import { createBrowserHost } from "./browserHost.js";
|
|
22
|
+
import { createAsyncHostDispatcher } from "./abi.js";
|
|
23
23
|
import {
|
|
24
24
|
createSabHostcallBuffer,
|
|
25
25
|
createSabHostcallServer,
|
|
26
26
|
DEFAULT_SAB_HOSTCALL_RESPONSE_BYTES,
|
|
27
|
-
} from "
|
|
28
|
-
import { WASI_THREAD_HOSTCALL_MESSAGE } from "
|
|
27
|
+
} from "./sabHostcallChannel.js";
|
|
28
|
+
import { WASI_THREAD_HOSTCALL_MESSAGE } from "./wasiThreadWorkerRuntime.js";
|
|
29
|
+
import { importNodeBuiltin } from "./nodeBuiltinSpecifier.js";
|
|
29
30
|
|
|
30
31
|
const WORKER_URL = new URL("./workerModuleHarnessWorker.js", import.meta.url);
|
|
31
32
|
|
|
@@ -51,7 +52,10 @@ function isNodeRuntime() {
|
|
|
51
52
|
|
|
52
53
|
async function spawnWorker(workerUrl) {
|
|
53
54
|
if (isNodeRuntime()) {
|
|
54
|
-
|
|
55
|
+
// Specifier assembled at runtime on purpose — see nodeBuiltinSpecifier.js.
|
|
56
|
+
// A literal here is statically resolved by every browser bundler even
|
|
57
|
+
// though this branch is dead in a browser.
|
|
58
|
+
const { Worker: NodeWorker } = await importNodeBuiltin("worker_threads");
|
|
55
59
|
const worker = new NodeWorker(workerUrl);
|
|
56
60
|
return {
|
|
57
61
|
post: (message) => worker.postMessage(message),
|
|
@@ -75,17 +79,46 @@ async function spawnWorker(workerUrl) {
|
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Compile `source` to a `WebAssembly.Module` on THIS (controlling) thread
|
|
84
|
+
* before ever touching a worker.
|
|
85
|
+
*
|
|
86
|
+
* Plaintext-hygiene contract (mirrors OrbPro's `wasmModuleHygiene.js`,
|
|
87
|
+
* `wasm-plaintext-memory-hygiene`): a compiled `WebAssembly.Module` is
|
|
88
|
+
* structured-cloneable to a Worker (browser) or `node:worker_threads`
|
|
89
|
+
* (Node) — proven by `createWasiThreadSpawn` in `src/host/wasiThreadHost.js`,
|
|
90
|
+
* which hands the same compiled module to every pooled worker in both
|
|
91
|
+
* lanes. Shipping the MODULE instead of raw bytes means there is no second
|
|
92
|
+
* plaintext copy alive in the worker, and no re-compile once it arrives —
|
|
93
|
+
* `createBrowserModuleHarness`'s own `compileWasmModule` already accepts a
|
|
94
|
+
* `WebAssembly.Module` directly and returns it unchanged.
|
|
95
|
+
*
|
|
96
|
+
* `source` is never retained past this call and never zeroed here: a
|
|
97
|
+
* caller-supplied `Uint8Array`/`ArrayBuffer` is not this harness's buffer to
|
|
98
|
+
* mutate (compile does not retain a reference to it either — the caller is
|
|
99
|
+
* free to reuse or scrub it immediately after this resolves).
|
|
100
|
+
*
|
|
101
|
+
* @param {WebAssembly.Module|Uint8Array|ArrayBuffer} source
|
|
102
|
+
* @param {string} label diagnostic label
|
|
103
|
+
* @returns {Promise<WebAssembly.Module>}
|
|
104
|
+
*/
|
|
105
|
+
async function toWasmModule(source, label) {
|
|
106
|
+
if (source instanceof WebAssembly.Module) {
|
|
107
|
+
return source;
|
|
108
|
+
}
|
|
109
|
+
if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
|
|
110
|
+
return WebAssembly.compile(source);
|
|
111
|
+
}
|
|
81
112
|
throw new TypeError(
|
|
82
|
-
`${label} must be a Uint8Array or ArrayBuffer
|
|
113
|
+
`${label} must be a WebAssembly.Module, Uint8Array, or ArrayBuffer.`,
|
|
83
114
|
);
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
/**
|
|
87
118
|
* @param {object} options
|
|
88
|
-
* @param {Uint8Array|ArrayBuffer} options.wasmSource module
|
|
119
|
+
* @param {WebAssembly.Module|Uint8Array|ArrayBuffer} options.wasmSource module
|
|
120
|
+
* bytes OR an already-compiled module (preferred — skips a redundant
|
|
121
|
+
* compile and avoids ever materializing plaintext bytes in this harness)
|
|
89
122
|
* @param {object} [options.host] host servicing guest hostcalls (default createBrowserHost(hostOptions))
|
|
90
123
|
* @param {object} [options.hostOptions] options for the default host
|
|
91
124
|
* @param {(operation: string, params: any) => Promise<any>} [options.dispatchHost] dispatch override
|
|
@@ -96,7 +129,7 @@ function toBytes(source, label) {
|
|
|
96
129
|
* createBrowserModuleHarness (surface, args, env, allowRawInvoke, logOutput, ...)
|
|
97
130
|
*/
|
|
98
131
|
export async function createWorkerModuleHarness(options = {}) {
|
|
99
|
-
const
|
|
132
|
+
const wasmModule = await toWasmModule(options.wasmSource, "wasmSource");
|
|
100
133
|
const host = options.host ?? createBrowserHost(options.hostOptions);
|
|
101
134
|
const dispatch = options.dispatchHost ?? createAsyncHostDispatcher(host);
|
|
102
135
|
const buffer = createSabHostcallBuffer({
|
|
@@ -150,7 +183,7 @@ export async function createWorkerModuleHarness(options = {}) {
|
|
|
150
183
|
case "worker-online":
|
|
151
184
|
port.post({
|
|
152
185
|
type: "init",
|
|
153
|
-
|
|
186
|
+
wasmModule,
|
|
154
187
|
buffer,
|
|
155
188
|
hostcallTimeoutMs: options.hostcallTimeoutMs,
|
|
156
189
|
harnessOptions: {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createBrowserModuleHarness } from "./browserModuleHarness.js";
|
|
13
|
-
import { createSabHostcallClientDispatch } from "
|
|
13
|
+
import { createSabHostcallClientDispatch } from "./sabHostcallChannel.js";
|
|
14
14
|
|
|
15
15
|
async function resolvePort() {
|
|
16
16
|
if (
|
|
@@ -90,12 +90,13 @@ async function handleInit(message) {
|
|
|
90
90
|
timeoutMs: message.hostcallTimeoutMs,
|
|
91
91
|
postRequest: (request) => port.post({ type: "hostcall", ...request }),
|
|
92
92
|
});
|
|
93
|
+
// Plaintext-hygiene contract: the controlling thread compiles once and
|
|
94
|
+
// ships the WebAssembly.Module (never raw bytes) — see toWasmModule() in
|
|
95
|
+
// workerModuleHarness.js. createBrowserModuleHarness's compileWasmModule
|
|
96
|
+
// already returns a Module input unchanged, so this never re-compiles.
|
|
93
97
|
const harnessOptions = {
|
|
94
98
|
...(message.harnessOptions ?? {}),
|
|
95
|
-
wasmSource:
|
|
96
|
-
message.wasmBytes instanceof Uint8Array
|
|
97
|
-
? message.wasmBytes
|
|
98
|
-
: new Uint8Array(message.wasmBytes),
|
|
99
|
+
wasmSource: message.wasmModule,
|
|
99
100
|
hostcallDispatch: dispatch,
|
|
100
101
|
};
|
|
101
102
|
harness = await createBrowserModuleHarness(harnessOptions);
|
package/src/index.d.ts
CHANGED
|
@@ -1962,7 +1962,11 @@ export interface BrowserModuleHarness {
|
|
|
1962
1962
|
}
|
|
1963
1963
|
export function detectArtifactProfile(wasmModule: WebAssembly.Module): string;
|
|
1964
1964
|
export function createBrowserModuleHarness(options?: {
|
|
1965
|
-
wasmSource: Uint8Array | ArrayBuffer | string | WebAssembly.Module | unknown;
|
|
1965
|
+
wasmSource: Uint8Array | ArrayBuffer | string | Response | WebAssembly.Module | unknown;
|
|
1966
|
+
// `true` uses env/global-configured trust; an object sets it explicitly;
|
|
1967
|
+
// `false` disables verification. Ignored for a precompiled WebAssembly.Module
|
|
1968
|
+
// (unverifiable — pass bytes/URL/Response when verification is required).
|
|
1969
|
+
verifySignature?: boolean | { trustedPublicKeys?: string[]; requireSignature?: boolean };
|
|
1966
1970
|
host?: BrowserHost | RuntimeHost | Record<string, unknown>;
|
|
1967
1971
|
hostOptions?: BrowserHostOptions;
|
|
1968
1972
|
args?: string[];
|
|
@@ -1979,6 +1983,25 @@ export function createBrowserModuleHarness(options?: {
|
|
|
1979
1983
|
allowRawInvoke?: boolean;
|
|
1980
1984
|
initialMemoryBytes?: number;
|
|
1981
1985
|
maximumMemoryBytes?: number;
|
|
1986
|
+
// Upper bound on guest wasi-threads spawns; sizes the browser warm worker
|
|
1987
|
+
// pool (defaults to hardware concurrency). No effect on a non-threaded guest.
|
|
1988
|
+
maxThreads?: number;
|
|
1989
|
+
// Request-isolated BroadcastChannel descriptor a nested pthread hostcall
|
|
1990
|
+
// dispatches over; supplied by createWorkerModuleHarness for the in-worker
|
|
1991
|
+
// harness instance, not something a top-level caller usually sets by hand.
|
|
1992
|
+
threadHostcallChannel?: {
|
|
1993
|
+
channelName: string;
|
|
1994
|
+
token: string;
|
|
1995
|
+
maxResponseBytes?: number;
|
|
1996
|
+
timeoutMs?: number;
|
|
1997
|
+
};
|
|
1998
|
+
// Explicit successful cross-origin-isolation negotiation for wasi-threads
|
|
1999
|
+
// guests; defaults to true. See docs/browser-wasmedge-isomorphic.md's
|
|
2000
|
+
// cross-origin-isolation section for when this is required.
|
|
2001
|
+
enableBrowserWasiThreads?: boolean;
|
|
2002
|
+
// Capacity of the module-memory arena a direct-invoke request is written
|
|
2003
|
+
// into (bytes). Defaults to 64 KiB.
|
|
2004
|
+
directInvokeRequestArenaBytes?: number;
|
|
1982
2005
|
logOutput?: boolean;
|
|
1983
2006
|
hostcallDispatch?: (operation: string, params: unknown) => unknown;
|
|
1984
2007
|
}): Promise<BrowserModuleHarness>;
|
|
@@ -1997,7 +2020,11 @@ export interface WorkerModuleHarness {
|
|
|
1997
2020
|
destroy(): Promise<void>;
|
|
1998
2021
|
}
|
|
1999
2022
|
export function createWorkerModuleHarness(options: {
|
|
2000
|
-
|
|
2023
|
+
// A precompiled WebAssembly.Module is preferred: it skips a redundant
|
|
2024
|
+
// compile and is structured-cloned to the worker directly (no plaintext
|
|
2025
|
+
// bytes cross the postMessage boundary at all). Uint8Array/ArrayBuffer is
|
|
2026
|
+
// compiled once on the controlling thread before the worker is spawned.
|
|
2027
|
+
wasmSource: WebAssembly.Module | Uint8Array | ArrayBuffer;
|
|
2001
2028
|
host?: unknown;
|
|
2002
2029
|
hostOptions?: BrowserHostOptions;
|
|
2003
2030
|
dispatchHost?: (operation: string, params: unknown) => Promise<unknown>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standards catalog — BROWSER leg (the `browser` condition of `./standards`).
|
|
3
|
+
*
|
|
4
|
+
* Same catalog, same validation verdicts as the Node leg; only acquisition
|
|
5
|
+
* differs, and that difference is absorbed here rather than by a runtime check
|
|
6
|
+
* inside shared code. A browser has no `require.resolve` and no readFile, so
|
|
7
|
+
* the manifest must be handed in or fetched:
|
|
8
|
+
*
|
|
9
|
+
* loadStandardsCatalog({ manifest }) // already-parsed manifest.json
|
|
10
|
+
* loadStandardsCatalog({ manifestUrl }) // fetched, then parsed
|
|
11
|
+
*
|
|
12
|
+
* Neither one supplied is a caller defect, not a fallback: it throws. Silently
|
|
13
|
+
* returning an empty catalog would let a browser "validate" a manifest against
|
|
14
|
+
* nothing and agree with a Node run that actually checked it — exactly the kind
|
|
15
|
+
* of quiet divergence the tri-runtime contract exists to forbid.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
buildStandardsCatalog,
|
|
20
|
+
validateManifestAgainstCatalog,
|
|
21
|
+
withSharedModuleCatalog,
|
|
22
|
+
} from "./catalogCore.js";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
buildStandardsCatalog,
|
|
26
|
+
resolveStandardsTypeRef,
|
|
27
|
+
validateManifestAgainstCatalog,
|
|
28
|
+
withSharedModuleCatalog,
|
|
29
|
+
} from "./catalogCore.js";
|
|
30
|
+
|
|
31
|
+
const standardsCatalogPromises = new Map();
|
|
32
|
+
const knownTypeCatalogPromises = new Map();
|
|
33
|
+
|
|
34
|
+
function catalogCacheKey(options) {
|
|
35
|
+
if (typeof options?.manifestUrl === "string" && options.manifestUrl) {
|
|
36
|
+
return `url:${options.manifestUrl}`;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readManifest(options) {
|
|
42
|
+
if (options?.manifest && typeof options.manifest === "object") {
|
|
43
|
+
return options.manifest;
|
|
44
|
+
}
|
|
45
|
+
if (typeof options?.manifestUrl === "string" && options.manifestUrl) {
|
|
46
|
+
const response = await fetch(options.manifestUrl);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Failed to fetch the spacedatastandards.org manifest from ${options.manifestUrl}: ${response.status}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return response.json();
|
|
53
|
+
}
|
|
54
|
+
throw new Error(
|
|
55
|
+
"The browser standards catalog needs the spacedatastandards.org manifest: " +
|
|
56
|
+
"pass { manifest } (already parsed) or { manifestUrl } (fetched). " +
|
|
57
|
+
"There is no on-disk manifest to read in a browser.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function loadStandardsCatalog(options = {}) {
|
|
62
|
+
const key = catalogCacheKey(options);
|
|
63
|
+
if (key === null) {
|
|
64
|
+
return buildStandardsCatalog(await readManifest(options));
|
|
65
|
+
}
|
|
66
|
+
if (!standardsCatalogPromises.has(key)) {
|
|
67
|
+
standardsCatalogPromises.set(
|
|
68
|
+
key,
|
|
69
|
+
(async () => buildStandardsCatalog(await readManifest(options)))(),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return standardsCatalogPromises.get(key);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadKnownTypeCatalog(options = {}) {
|
|
76
|
+
const key = catalogCacheKey(options);
|
|
77
|
+
if (key === null) {
|
|
78
|
+
return withSharedModuleCatalog(await loadStandardsCatalog(options));
|
|
79
|
+
}
|
|
80
|
+
if (!knownTypeCatalogPromises.has(key)) {
|
|
81
|
+
knownTypeCatalogPromises.set(
|
|
82
|
+
key,
|
|
83
|
+
loadStandardsCatalog(options).then(withSharedModuleCatalog),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return knownTypeCatalogPromises.get(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function validateManifestAgainstStandardsCatalog(
|
|
90
|
+
manifest,
|
|
91
|
+
options = {},
|
|
92
|
+
) {
|
|
93
|
+
const catalog = options.catalog ?? (await loadKnownTypeCatalog(options));
|
|
94
|
+
return validateManifestAgainstCatalog(catalog, manifest, options);
|
|
95
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standards catalog — the runtime-independent half.
|
|
3
|
+
*
|
|
4
|
+
* Parsing, freshness validation, type-ref resolution and manifest checking are
|
|
5
|
+
* identical everywhere. Only HOW the `spacedatastandards.org` manifest is
|
|
6
|
+
* OBTAINED differs: Node reads it off disk (`./index.js`), a browser is handed
|
|
7
|
+
* it or fetches it (`./browser.js`). The package's `browser` export condition
|
|
8
|
+
* picks the leg, so a browser bundle never statically resolves
|
|
9
|
+
* node:fs/promises, node:path or node:module
|
|
10
|
+
* (`module-sdk-browser-entry-node-builtins`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { sharedModuleCatalog } from "./sharedCatalog.js";
|
|
14
|
+
import { normalizePayloadSchemaHash } from "../manifest/typeRefs.js";
|
|
15
|
+
const requiredCurrentScvTokens = Object.freeze([
|
|
16
|
+
"SENSOR_LOCAL",
|
|
17
|
+
"table SCVResult",
|
|
18
|
+
"table SCVSensorShapeContract",
|
|
19
|
+
"SAR_ANNULAR_SECTOR",
|
|
20
|
+
"enum scvSensorRangeBoundaryKind",
|
|
21
|
+
"table SCVAggregateStatistics",
|
|
22
|
+
"AGGREGATE_STATISTICS:SCVAggregateStatistics",
|
|
23
|
+
"table SCVPackedRasterProducts",
|
|
24
|
+
"table SCVPackedRasterBand",
|
|
25
|
+
"RASTER_PRODUCTS:SCVPackedRasterProducts",
|
|
26
|
+
"enum scvRasterProductKind",
|
|
27
|
+
]);
|
|
28
|
+
const currentScvResultFields = Object.freeze(
|
|
29
|
+
new Set([
|
|
30
|
+
"JOB_ID",
|
|
31
|
+
"TRACE_ID",
|
|
32
|
+
"STATUS",
|
|
33
|
+
"TIME_GRID",
|
|
34
|
+
"TARGET_BODY",
|
|
35
|
+
"TOTAL_SENSORS",
|
|
36
|
+
"TOTAL_WINDOWS",
|
|
37
|
+
"CELL_STATS",
|
|
38
|
+
"INTERVALS",
|
|
39
|
+
"LATITUDE_BANDS",
|
|
40
|
+
"TIME_SERIES",
|
|
41
|
+
"HISTOGRAMS",
|
|
42
|
+
"CONTRIBUTIONS",
|
|
43
|
+
"GEOMETRY",
|
|
44
|
+
"RASTER_PRODUCTS",
|
|
45
|
+
"MESSAGE",
|
|
46
|
+
"AGGREGATE_STATISTICS",
|
|
47
|
+
"TARGET_RESULTS",
|
|
48
|
+
]),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function collectFlatbufferTableFields(idl, tableName) {
|
|
52
|
+
const match = String(idl ?? "").match(
|
|
53
|
+
new RegExp(`\\btable\\s+${tableName}\\s*\\{([\\s\\S]*?)\\n\\}`, "m"),
|
|
54
|
+
);
|
|
55
|
+
if (!match) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return match[1]
|
|
59
|
+
.split("\n")
|
|
60
|
+
.map((line) => line.replace(/\/\/.*$/, "").trim())
|
|
61
|
+
.map((line) => line.match(/^([A-Z0-9_]+)\s*:/)?.[1] ?? null)
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeSchemaName(value) {
|
|
66
|
+
return value === undefined || value === null ? "" : String(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeFileIdentifier(value) {
|
|
70
|
+
return value === undefined || value === null ? "" : String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseStandardsEntry([schemaCode, entry]) {
|
|
74
|
+
const idl = String(entry?.IDL ?? "");
|
|
75
|
+
const fileIdentifierMatch = idl.match(/file_identifier\s+"([^"]+)"/i);
|
|
76
|
+
const hashMatch = idl.match(/\/\/ Hash:\s*([a-f0-9]+)/i);
|
|
77
|
+
const versionMatch = idl.match(/\/\/ Version:\s*([^\n]+)/i);
|
|
78
|
+
const rootTypeMatch = idl.match(/root_type\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/i);
|
|
79
|
+
return {
|
|
80
|
+
schemaCode: schemaCode.toUpperCase(),
|
|
81
|
+
schemaName: `${schemaCode.toUpperCase()}.fbs`,
|
|
82
|
+
fileIdentifier: fileIdentifierMatch
|
|
83
|
+
? normalizeFileIdentifier(fileIdentifierMatch[1])
|
|
84
|
+
: null,
|
|
85
|
+
hash: hashMatch ? hashMatch[1].toLowerCase() : null,
|
|
86
|
+
rootTypeName: rootTypeMatch ? rootTypeMatch[1] : null,
|
|
87
|
+
idl,
|
|
88
|
+
version: versionMatch ? versionMatch[1].trim() : null,
|
|
89
|
+
files: Array.isArray(entry?.files) ? entry.files : [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateStandardsCatalogFreshness(catalog, sourceName) {
|
|
94
|
+
const issues = [];
|
|
95
|
+
const scvEntry = catalog.find((entry) => entry.schemaCode === "SCV");
|
|
96
|
+
if (!scvEntry) {
|
|
97
|
+
return issues;
|
|
98
|
+
}
|
|
99
|
+
const missingTokens = requiredCurrentScvTokens.filter(
|
|
100
|
+
(token) => !scvEntry.idl.includes(token),
|
|
101
|
+
);
|
|
102
|
+
if (missingTokens.length > 0) {
|
|
103
|
+
issues.push({
|
|
104
|
+
severity: "error",
|
|
105
|
+
code: "stale-scv-contract",
|
|
106
|
+
message:
|
|
107
|
+
"The loaded spacedatastandards.org SCV catalog is stale and does not " +
|
|
108
|
+
`include required current coverage fields: ${missingTokens.join(", ")}.`,
|
|
109
|
+
location: `${sourceName}.standards.SCV`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const unsupportedResultFields = collectFlatbufferTableFields(
|
|
113
|
+
scvEntry.idl,
|
|
114
|
+
"SCVResult",
|
|
115
|
+
).filter((field) => !currentScvResultFields.has(field));
|
|
116
|
+
if (unsupportedResultFields.length > 0) {
|
|
117
|
+
issues.push({
|
|
118
|
+
severity: "error",
|
|
119
|
+
code: "stale-scv-contract",
|
|
120
|
+
message:
|
|
121
|
+
"The loaded spacedatastandards.org SCV catalog contains unsupported " +
|
|
122
|
+
`SCVResult fields that are not part of the current contract: ${unsupportedResultFields.join(", ")}.`,
|
|
123
|
+
location: `${sourceName}.standards.SCV`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return issues;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the standards catalog from an already-parsed manifest object. Same
|
|
131
|
+
* bytes in, same catalog out, in every runtime.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} manifest - the parsed `spacedatastandards.org` manifest.json
|
|
134
|
+
* @returns {Array} sorted standards catalog
|
|
135
|
+
*/
|
|
136
|
+
export function buildStandardsCatalog(manifest) {
|
|
137
|
+
return Object.entries(manifest?.STANDARDS ?? {})
|
|
138
|
+
.map(parseStandardsEntry)
|
|
139
|
+
.sort((left, right) => left.schemaCode.localeCompare(right.schemaCode));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Append the SDK's own shared-module catalog to a standards catalog.
|
|
144
|
+
*
|
|
145
|
+
* @param {Array} catalog
|
|
146
|
+
* @returns {Array}
|
|
147
|
+
*/
|
|
148
|
+
export function withSharedModuleCatalog(catalog) {
|
|
149
|
+
return [...catalog, ...sharedModuleCatalog];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function resolveStandardsTypeRef(typeRef, catalog = []) {
|
|
153
|
+
const schemaName = normalizeSchemaName(typeRef?.schemaName);
|
|
154
|
+
const fileIdentifier = normalizeFileIdentifier(typeRef?.fileIdentifier);
|
|
155
|
+
if (!schemaName && !fileIdentifier) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return (
|
|
159
|
+
catalog.find(
|
|
160
|
+
(entry) =>
|
|
161
|
+
(!schemaName || normalizeSchemaName(entry.schemaName) === schemaName) &&
|
|
162
|
+
(!fileIdentifier ||
|
|
163
|
+
normalizeFileIdentifier(entry.fileIdentifier) === fileIdentifier),
|
|
164
|
+
) ?? null
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function schemaHashMatchesCatalog(typeRefHash, catalogHash) {
|
|
169
|
+
if (typeRefHash === undefined || typeRefHash === null) return true;
|
|
170
|
+
const declared = normalizePayloadSchemaHash(typeRefHash);
|
|
171
|
+
if (!declared) return true;
|
|
172
|
+
const canonical = normalizePayloadSchemaHash(catalogHash);
|
|
173
|
+
if (!canonical || declared.length !== canonical.length) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return declared.every((byte, index) => byte === canonical[index]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectTypeRefs(manifest) {
|
|
180
|
+
const refs = [];
|
|
181
|
+
for (const method of Array.isArray(manifest?.methods) ? manifest.methods : []) {
|
|
182
|
+
for (const portsKey of ["inputPorts", "outputPorts"]) {
|
|
183
|
+
for (const port of Array.isArray(method?.[portsKey]) ? method[portsKey] : []) {
|
|
184
|
+
for (const typeSet of Array.isArray(port?.acceptedTypeSets)
|
|
185
|
+
? port.acceptedTypeSets
|
|
186
|
+
: []) {
|
|
187
|
+
for (const allowedType of Array.isArray(typeSet?.allowedTypes)
|
|
188
|
+
? typeSet.allowedTypes
|
|
189
|
+
: []) {
|
|
190
|
+
refs.push({
|
|
191
|
+
source: `${method?.methodId ?? "method"}.${port?.portId ?? "port"}`,
|
|
192
|
+
typeRef: allowedType,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const typeRef of Array.isArray(manifest?.schemasUsed)
|
|
200
|
+
? manifest.schemasUsed
|
|
201
|
+
: []) {
|
|
202
|
+
refs.push({
|
|
203
|
+
source: "schemasUsed",
|
|
204
|
+
typeRef,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return refs;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate a module manifest against an ALREADY-RESOLVED catalog.
|
|
212
|
+
*
|
|
213
|
+
* Both legs of `./standards` funnel into this, so the verdicts cannot diverge
|
|
214
|
+
* between runtimes; only catalog acquisition differs.
|
|
215
|
+
*
|
|
216
|
+
* @param {Array} catalog
|
|
217
|
+
* @param {object} manifest
|
|
218
|
+
* @param {object} [options]
|
|
219
|
+
* @returns {{catalog: Array, issues: Array}}
|
|
220
|
+
*/
|
|
221
|
+
export function validateManifestAgainstCatalog(catalog, manifest, options = {}) {
|
|
222
|
+
const sourceName = options.sourceName ?? "manifest";
|
|
223
|
+
const issues = validateStandardsCatalogFreshness(catalog, sourceName);
|
|
224
|
+
for (const { source, typeRef } of collectTypeRefs(manifest)) {
|
|
225
|
+
if (typeRef?.acceptsAnyFlatbuffer === true) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const resolved = resolveStandardsTypeRef(typeRef, catalog);
|
|
229
|
+
if (!resolved) {
|
|
230
|
+
const schemaName = normalizeSchemaName(typeRef?.schemaName);
|
|
231
|
+
const fileIdentifier = normalizeFileIdentifier(typeRef?.fileIdentifier);
|
|
232
|
+
const partiallyKnown = catalog.some(
|
|
233
|
+
(entry) =>
|
|
234
|
+
(schemaName && normalizeSchemaName(entry.schemaName) === schemaName) ||
|
|
235
|
+
(fileIdentifier &&
|
|
236
|
+
normalizeFileIdentifier(entry.fileIdentifier) === fileIdentifier),
|
|
237
|
+
);
|
|
238
|
+
issues.push({
|
|
239
|
+
severity: "error",
|
|
240
|
+
code: partiallyKnown
|
|
241
|
+
? "standards-type-identity-mismatch"
|
|
242
|
+
: "unresolved-standards-type",
|
|
243
|
+
message: partiallyKnown
|
|
244
|
+
? `Type reference from ${source} mixes a known schemaName or exact four-byte fileIdentifier with a different standards entry.`
|
|
245
|
+
: `Type reference from ${source} does not resolve to a known shared-module or \`spacedatastandards.org\` schema by its exact schemaName and fileIdentifier.`,
|
|
246
|
+
location: `${sourceName}.${source}`,
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (
|
|
251
|
+
typeRef?.rootTypeName &&
|
|
252
|
+
resolved.rootTypeName &&
|
|
253
|
+
typeRef.rootTypeName !== resolved.rootTypeName
|
|
254
|
+
) {
|
|
255
|
+
issues.push({
|
|
256
|
+
severity: "error",
|
|
257
|
+
code: "standards-root-type-mismatch",
|
|
258
|
+
message: `Type reference from ${source} declares rootTypeName ${JSON.stringify(typeRef.rootTypeName)} but the canonical SDS root is ${JSON.stringify(resolved.rootTypeName)}.`,
|
|
259
|
+
location: `${sourceName}.${source}`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
typeRef?.schemaVersion &&
|
|
264
|
+
resolved.version &&
|
|
265
|
+
typeRef.schemaVersion !== resolved.version
|
|
266
|
+
) {
|
|
267
|
+
issues.push({
|
|
268
|
+
severity: "error",
|
|
269
|
+
code: "standards-schema-version-mismatch",
|
|
270
|
+
message: `Type reference from ${source} declares schemaVersion ${JSON.stringify(typeRef.schemaVersion)} but the canonical SDS version is ${JSON.stringify(resolved.version)}.`,
|
|
271
|
+
location: `${sourceName}.${source}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
resolved.hash &&
|
|
276
|
+
!schemaHashMatchesCatalog(typeRef?.schemaHash, resolved.hash)
|
|
277
|
+
) {
|
|
278
|
+
issues.push({
|
|
279
|
+
severity: "error",
|
|
280
|
+
code: "standards-schema-hash-mismatch",
|
|
281
|
+
message: `Type reference from ${source} declares a schemaHash that differs from the canonical SDS schema hash.`,
|
|
282
|
+
location: `${sourceName}.${source}`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
catalog,
|
|
288
|
+
issues,
|
|
289
|
+
};
|
|
290
|
+
}
|