space-data-module-sdk 0.8.11 → 0.8.13
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 +92 -0
- package/bin/space-data-module.js +85 -1
- package/docs/module-publication-standard.md +7 -3
- package/docs/propagator-abi.md +477 -0
- package/include/orbpro/orbpro_propagator_abi.h +312 -0
- package/package.json +7 -1
- package/schemas/PluginManifest.fbs +46 -1
- package/schemas/orbpro/Propagator.fbs +161 -3
- package/src/browser.js +11 -0
- package/src/bundle/index.js +1 -0
- package/src/bundle/sigdomain.js +22 -0
- package/src/capabilities.js +91 -0
- package/src/compliance/index.js +8 -0
- package/src/compliance/pluginCompliance.js +76 -32
- package/src/flow/flowCompiler.js +231 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
- package/src/generated/orbpro/manifest/plugin-family.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
- package/src/generated/orbpro/propagator-abi.js +118 -0
- package/src/generated/orbpro/propagator-abi.ts +199 -0
- package/src/host/browserModuleHarness.js +26 -0
- package/src/host/isomorphicLoader.js +57 -11
- package/src/host/runtimeTargetGate.js +256 -0
- package/src/host/workerModuleHarness.js +7 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +11 -0
- package/src/manifest/normalize.js +113 -4
- package/src/scaffold/copyTemplate.js +71 -0
- package/src/scaffold/index.js +150 -0
- package/src/scaffold/tokens.js +90 -0
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +287 -27
- package/templates/propagator-module/README.md +99 -0
- package/templates/propagator-module/build.js +103 -0
- package/templates/propagator-module/package.json +19 -0
- package/templates/propagator-module/plugin-manifest.json +66 -0
- package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
- package/templates/propagator-module/tests/module.build.test.mjs +103 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RUNTIME-TARGET GATE — one assert, called by every loader that stands for
|
|
3
|
+
* a runtime leg.
|
|
4
|
+
*
|
|
5
|
+
* Composed flows derive their `runtimeTargets` from their parts (see
|
|
6
|
+
* `collectFlowRuntimeTargets` in `src/flow/flowCompiler.js`), so an artifact
|
|
7
|
+
* that legitimately runs on only ONE leg now exists. That artifact must never
|
|
8
|
+
* be loaded QUIETLY on a leg it does not declare: the failure would surface as
|
|
9
|
+
* a hostcall trap several frames deep in a capability that leg cannot serve,
|
|
10
|
+
* which is indistinguishable from the cross-runtime divergence the isomorphism
|
|
11
|
+
* invariant exists to catch. Refuse at the door, by name, quoting the
|
|
12
|
+
* declaration that made it ineligible.
|
|
13
|
+
*
|
|
14
|
+
* The gate lives here rather than inside one harness because there are two
|
|
15
|
+
* doors into a composed artifact — `createBrowserModuleHarness` (a single
|
|
16
|
+
* module) and `createFlowRuntimeHost` (the composed flow runtime) — and a gate
|
|
17
|
+
* on only one of them is not a gate.
|
|
18
|
+
*
|
|
19
|
+
* Browser-safe: no node builtins, no bundle/signing surface, and the embedded
|
|
20
|
+
* declaration is read with `WebAssembly.Module.customSections`, which needs no
|
|
21
|
+
* copy of the source bytes (they are scrubbed by the time this runs).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { SDS_MANIFEST_SECTION_NAME } from "../bundle/constants.js";
|
|
25
|
+
import { decodePlgManifest, isPlgManifestBuffer } from "../manifest/plgCodec.js";
|
|
26
|
+
import { LegIncompatibleCapabilityIds } from "../capabilities.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `wasi` is the STRICT PORTABILITY BASELINE, not a fourth runtime: compliance
|
|
30
|
+
* confines a `wasi`-declaring artifact to the pure WASI capability subset and
|
|
31
|
+
* the `command` invoke surface. So a declaration of `wasi` admits either leg —
|
|
32
|
+
* WITH ONE EXCEPTION, which is why capabilities are part of this question.
|
|
33
|
+
* `pipe` is in both the standalone-WASI subset and the browser-incompatible
|
|
34
|
+
* set, so `runtimeTargets:["wasi"] + capabilities:["pipe"]` passes compliance
|
|
35
|
+
* (it names no browser target for the rule to fire on) and would otherwise be
|
|
36
|
+
* admitted to the browser leg against the SDK's own policy. The baseline
|
|
37
|
+
* admits a leg only when the artifact carries nothing that leg cannot serve.
|
|
38
|
+
*
|
|
39
|
+
* DO NOT "UNIFY" THIS WITH THE LITERAL PATH. A declaration that names the leg
|
|
40
|
+
* outright is NOT capability-checked here, and that asymmetry is deliberate:
|
|
41
|
+
*
|
|
42
|
+
* declaration -> trust the author; inference -> prove it.
|
|
43
|
+
*
|
|
44
|
+
* An explicit `browser` target is the author's statement and the embedder's
|
|
45
|
+
* business — five of the browser-incompatible capabilities (`network`, `ipfs`,
|
|
46
|
+
* `protocol_dial`, `protocol_handle`, `wallet_sign`) ARE served by
|
|
47
|
+
* `BrowserHost` when the embedder injects an adapter, so refusing them at load
|
|
48
|
+
* time would break legitimate embedders. Publish-time compliance already
|
|
49
|
+
* refuses the incoherent declaration. The `wasi` path, by contrast, is an
|
|
50
|
+
* inference the SDK makes on the author's behalf, and an inference must be
|
|
51
|
+
* conservative.
|
|
52
|
+
*/
|
|
53
|
+
const LEG_SATISFYING_TARGETS = Object.freeze({
|
|
54
|
+
browser: Object.freeze(["browser", "wasi"]),
|
|
55
|
+
wasmedge: Object.freeze(["wasmedge", "wasi"]),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const WASI_PORTABILITY_TARGET = "wasi";
|
|
59
|
+
|
|
60
|
+
function legIncompatibleCapabilities(capabilities, leg) {
|
|
61
|
+
const incompatible = LegIncompatibleCapabilityIds[leg];
|
|
62
|
+
if (!incompatible || !Array.isArray(capabilities)) return [];
|
|
63
|
+
return capabilities
|
|
64
|
+
.map((capability) =>
|
|
65
|
+
typeof capability === "string"
|
|
66
|
+
? capability
|
|
67
|
+
: (capability?.capabilityId ?? capability?.name ?? ""),
|
|
68
|
+
)
|
|
69
|
+
.filter((capability) => incompatible.includes(capability))
|
|
70
|
+
.sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Does a declared target set admit this leg?
|
|
75
|
+
*
|
|
76
|
+
* ONE RULE, used by the loaders AND by the flow compiler's derivation. Two
|
|
77
|
+
* rules for one field is how a manifest comes to mean different things to the
|
|
78
|
+
* compiler and to the door: the compiler stripped the browser leg from a
|
|
79
|
+
* `["wasi","wasmedge"]` part while the gate happily admitted the same
|
|
80
|
+
* declaration to the browser.
|
|
81
|
+
*
|
|
82
|
+
* An empty/absent declaration admits everything, matching compliance, which
|
|
83
|
+
* skips the runtime-target rule on an absent field.
|
|
84
|
+
*
|
|
85
|
+
* @param {string[]|undefined} targets declared runtimeTargets
|
|
86
|
+
* @param {string} leg the runtime target being asked about
|
|
87
|
+
* @param {Array<string|{capabilityId?: string, name?: string}>} [capabilities]
|
|
88
|
+
* the artifact's declared capabilities; consulted only to keep the `wasi`
|
|
89
|
+
* baseline from admitting a leg that cannot serve them
|
|
90
|
+
*/
|
|
91
|
+
export function runtimeTargetSatisfies(targets, leg, capabilities) {
|
|
92
|
+
if (!Array.isArray(targets) || targets.length === 0) return true;
|
|
93
|
+
const accepted = LEG_SATISFYING_TARGETS[leg] ?? [leg];
|
|
94
|
+
if (targets.includes(leg)) return true;
|
|
95
|
+
if (!targets.some((target) => accepted.includes(target))) return false;
|
|
96
|
+
// Admitted only via the portability baseline — prove it, do not assume it.
|
|
97
|
+
return (
|
|
98
|
+
targets.includes(WASI_PORTABILITY_TARGET) &&
|
|
99
|
+
legIncompatibleCapabilities(capabilities, leg).length === 0
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class RuntimeTargetError extends Error {
|
|
104
|
+
constructor(message, { declaredTargets, source, leg }) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = "RuntimeTargetError";
|
|
107
|
+
this.code = "runtime-target-out-of-scope";
|
|
108
|
+
this.declaredTargets = declaredTargets;
|
|
109
|
+
this.leg = leg;
|
|
110
|
+
// "embedded" (the artifact's own signed record) or "caller" (the manifest
|
|
111
|
+
// the loader was handed). Which one refused is evidence, not trivia.
|
|
112
|
+
this.declarationSource = source;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function normalizedRuntimeTargets(manifest) {
|
|
117
|
+
return Array.isArray(manifest?.runtimeTargets)
|
|
118
|
+
? manifest.runtimeTargets
|
|
119
|
+
.map((target) => String(target ?? "").trim().toLowerCase())
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
: [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The artifact's OWN declaration, read straight out of the embedded `$PLG`
|
|
126
|
+
* custom section, so a caller that hands a loader nothing but bytes still gets
|
|
127
|
+
* the refusal.
|
|
128
|
+
*
|
|
129
|
+
* @param {WebAssembly.Module} wasmModule
|
|
130
|
+
* @returns {string[]} declared targets, or [] when the artifact declares none
|
|
131
|
+
*/
|
|
132
|
+
export function embeddedPlgManifest(wasmModule) {
|
|
133
|
+
let sections;
|
|
134
|
+
try {
|
|
135
|
+
sections = WebAssembly.Module.customSections(
|
|
136
|
+
wasmModule,
|
|
137
|
+
SDS_MANIFEST_SECTION_NAME,
|
|
138
|
+
);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
for (const section of sections ?? []) {
|
|
143
|
+
try {
|
|
144
|
+
const bytes = new Uint8Array(section);
|
|
145
|
+
if (!isPlgManifestBuffer(bytes)) continue;
|
|
146
|
+
// FIRST DECODABLE MANIFEST WINS — the same rule the node-side locator
|
|
147
|
+
// (`locateEmbeddedPlgManifest`) applies. Skipping a manifest merely
|
|
148
|
+
// because its runtimeTargets are empty made this reader disagree with
|
|
149
|
+
// that one on the very same bytes, so an artifact could be scoped by one
|
|
150
|
+
// declaration and enforced against another.
|
|
151
|
+
return decodePlgManifest(bytes);
|
|
152
|
+
} catch {
|
|
153
|
+
// An UNDECODABLE section is not a declaration. Keep looking.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function embeddedRuntimeTargets(wasmModule) {
|
|
160
|
+
return normalizedRuntimeTargets(embeddedPlgManifest(wasmModule));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve which declaration refuses this leg, if either does.
|
|
165
|
+
*
|
|
166
|
+
* EITHER source can refuse, and the EMBEDDED one wins on conflict. The
|
|
167
|
+
* embedded `$PLG` is what the artifact's signature covers; `manifest` is
|
|
168
|
+
* caller input. Trusting the caller first would make
|
|
169
|
+
* `{manifest: {runtimeTargets: ["browser"]}}` a bypass for a signed
|
|
170
|
+
* WasmEdge-only artifact — and the isomorphic flow host passes exactly such a
|
|
171
|
+
* caller-side manifest for every child it mounts.
|
|
172
|
+
*
|
|
173
|
+
* An artifact that declares NOTHING is unconstrained, matching compliance,
|
|
174
|
+
* which skips the runtime-target rule on an absent field.
|
|
175
|
+
*
|
|
176
|
+
* @returns {{targets: string[], source: "embedded"|"caller"}|null}
|
|
177
|
+
*/
|
|
178
|
+
export function resolveRuntimeTargetRefusal({
|
|
179
|
+
wasmModule,
|
|
180
|
+
manifest,
|
|
181
|
+
leg,
|
|
182
|
+
embeddedManifest: providedEmbeddedManifest,
|
|
183
|
+
}) {
|
|
184
|
+
// A caller that already located the artifact's own manifest from BYTES (the
|
|
185
|
+
// node leg does, via `locateEmbeddedPlgManifest`) passes it here rather than
|
|
186
|
+
// making this gate compile the module just to read a custom section. The two
|
|
187
|
+
// locators apply the same first-decodable-wins rule; the byte-side one can
|
|
188
|
+
// additionally reach a manifest carried in a bundle entry or a data segment,
|
|
189
|
+
// which only ever makes that leg MORE likely to refuse — the safe direction.
|
|
190
|
+
const embeddedManifest =
|
|
191
|
+
providedEmbeddedManifest ??
|
|
192
|
+
(wasmModule ? embeddedPlgManifest(wasmModule) : null);
|
|
193
|
+
const embedded = normalizedRuntimeTargets(embeddedManifest);
|
|
194
|
+
if (!runtimeTargetSatisfies(embedded, leg, embeddedManifest?.capabilities)) {
|
|
195
|
+
return { targets: embedded, source: "embedded" };
|
|
196
|
+
}
|
|
197
|
+
const caller = normalizedRuntimeTargets(manifest);
|
|
198
|
+
if (!runtimeTargetSatisfies(caller, leg, manifest?.capabilities)) {
|
|
199
|
+
return { targets: caller, source: "caller" };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {object} options
|
|
206
|
+
* @param {WebAssembly.Module} [options.wasmModule] compiled artifact
|
|
207
|
+
* @param {object} [options.manifest] caller-supplied manifest
|
|
208
|
+
* @param {string} options.leg the runtime target this loader stands for
|
|
209
|
+
* @param {string} [options.what] noun used in the message ("module" by default)
|
|
210
|
+
*/
|
|
211
|
+
export function assertArtifactRuntimeTarget({
|
|
212
|
+
wasmModule,
|
|
213
|
+
manifest,
|
|
214
|
+
embeddedManifest,
|
|
215
|
+
leg,
|
|
216
|
+
what = "module",
|
|
217
|
+
}) {
|
|
218
|
+
const refusal = resolveRuntimeTargetRefusal({
|
|
219
|
+
wasmModule,
|
|
220
|
+
manifest,
|
|
221
|
+
embeddedManifest,
|
|
222
|
+
leg,
|
|
223
|
+
});
|
|
224
|
+
if (!refusal) return;
|
|
225
|
+
const { targets, source } = refusal;
|
|
226
|
+
throw new RuntimeTargetError(
|
|
227
|
+
`Refusing to load a ${what} that does not target "${leg}": its ` +
|
|
228
|
+
`${source === "embedded" ? "embedded manifest" : "supplied manifest"} declares ` +
|
|
229
|
+
`runtimeTargets [${targets.join(", ")}], which does not admit this leg. Loading it ` +
|
|
230
|
+
`here would trap on the first capability the "${leg}" host cannot serve. Run it on ` +
|
|
231
|
+
"a host that provides one of its declared targets, or split the out-of-scope " +
|
|
232
|
+
"surface into its own module.",
|
|
233
|
+
{ declaredTargets: targets, source, leg },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Is this JavaScript running in a browser (window or a Worker)? Used only to
|
|
239
|
+
* DEFAULT the leg for loaders that are genuinely runtime-agnostic; every
|
|
240
|
+
* caller may state its leg explicitly and that always wins.
|
|
241
|
+
*/
|
|
242
|
+
export function detectBrowserLeg() {
|
|
243
|
+
try {
|
|
244
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
const workerScope = globalThis.WorkerGlobalScope;
|
|
248
|
+
return (
|
|
249
|
+
typeof workerScope === "function" &&
|
|
250
|
+
typeof self !== "undefined" &&
|
|
251
|
+
self instanceof workerScope
|
|
252
|
+
);
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./sabHostcallChannel.js";
|
|
28
28
|
import { WASI_THREAD_HOSTCALL_MESSAGE } from "./wasiThreadWorkerRuntime.js";
|
|
29
29
|
import { importNodeBuiltin } from "./nodeBuiltinSpecifier.js";
|
|
30
|
+
import { assertBrowserRuntimeTarget } from "./browserModuleHarness.js";
|
|
30
31
|
|
|
31
32
|
const WORKER_URL = new URL("./workerModuleHarnessWorker.js", import.meta.url);
|
|
32
33
|
|
|
@@ -130,6 +131,12 @@ async function toWasmModule(source, label) {
|
|
|
130
131
|
*/
|
|
131
132
|
export async function createWorkerModuleHarness(options = {}) {
|
|
132
133
|
const wasmModule = await toWasmModule(options.wasmSource, "wasmSource");
|
|
134
|
+
// Fail on THIS thread, before a worker, a SAB channel and a BroadcastChannel
|
|
135
|
+
// are stood up for an artifact the browser leg must not run. The in-worker
|
|
136
|
+
// createBrowserModuleHarness would refuse it too, but a rejection that
|
|
137
|
+
// arrives after the whole rig exists reads like a worker fault rather than
|
|
138
|
+
// the declaration it actually is.
|
|
139
|
+
assertBrowserRuntimeTarget(wasmModule, options.harnessOptions?.manifest);
|
|
133
140
|
const host = options.host ?? createBrowserHost(options.hostOptions);
|
|
134
141
|
const dispatch = options.dispatchHost ?? createAsyncHostDispatcher(host);
|
|
135
142
|
const buffer = createSabHostcallBuffer({
|
package/src/index.d.ts
CHANGED
|
@@ -2337,8 +2337,55 @@ export interface FlowRuntimeHostOptions {
|
|
|
2337
2337
|
engineLink?: {
|
|
2338
2338
|
exports: WebAssembly.Exports & { memory: WebAssembly.Memory };
|
|
2339
2339
|
} | null;
|
|
2340
|
+
/**
|
|
2341
|
+
* The runtime leg this host stands for. A composed flow DERIVES its
|
|
2342
|
+
* runtimeTargets from its parts, so a single-leg artifact exists and must be
|
|
2343
|
+
* refused by name on any other leg. Omit and a real browser is detected and
|
|
2344
|
+
* gated; pass `null` to opt out explicitly.
|
|
2345
|
+
*/
|
|
2346
|
+
runtimeTarget?: string | null;
|
|
2347
|
+
/** Caller-side manifest; the artifact's embedded record wins on conflict. */
|
|
2348
|
+
manifest?: PluginManifest | Record<string, unknown>;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
export declare class RuntimeTargetError extends Error {
|
|
2352
|
+
code: "runtime-target-out-of-scope";
|
|
2353
|
+
declaredTargets: string[];
|
|
2354
|
+
leg: string;
|
|
2355
|
+
declarationSource: "embedded" | "caller";
|
|
2340
2356
|
}
|
|
2341
2357
|
|
|
2358
|
+
/**
|
|
2359
|
+
* Does a declared target set admit this leg? `wasi` is the portability
|
|
2360
|
+
* baseline and admits either leg, but only when `capabilities` carries nothing
|
|
2361
|
+
* that leg cannot serve.
|
|
2362
|
+
*/
|
|
2363
|
+
export function runtimeTargetSatisfies(
|
|
2364
|
+
targets: string[] | undefined,
|
|
2365
|
+
leg: string,
|
|
2366
|
+
capabilities?: Array<string | { capabilityId?: string; name?: string }>,
|
|
2367
|
+
): boolean;
|
|
2368
|
+
|
|
2369
|
+
/** The artifact's own declared targets, read from its embedded `$PLG`. */
|
|
2370
|
+
export function embeddedRuntimeTargets(wasmModule: WebAssembly.Module): string[];
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Which declaration refuses this leg, if either does. The artifact's embedded
|
|
2374
|
+
* record wins over a caller-supplied manifest.
|
|
2375
|
+
*/
|
|
2376
|
+
export function resolveRuntimeTargetRefusal(options: {
|
|
2377
|
+
wasmModule?: WebAssembly.Module;
|
|
2378
|
+
manifest?: PluginManifest | Record<string, unknown>;
|
|
2379
|
+
embeddedManifest?: PluginManifest | Record<string, unknown> | null;
|
|
2380
|
+
leg: string;
|
|
2381
|
+
}): { targets: string[]; source: "embedded" | "caller" } | null;
|
|
2382
|
+
|
|
2383
|
+
/** Throws a RuntimeTargetError when the artifact does not target the browser. */
|
|
2384
|
+
export function assertBrowserRuntimeTarget(
|
|
2385
|
+
wasmModule: WebAssembly.Module,
|
|
2386
|
+
manifest?: PluginManifest | Record<string, unknown>,
|
|
2387
|
+
): void;
|
|
2388
|
+
|
|
2342
2389
|
export const FLOW_INVALID_INDEX: number;
|
|
2343
2390
|
export function createFlowRuntimeHost(
|
|
2344
2391
|
options: FlowRuntimeHostOptions,
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,17 @@ export * from "./deployment/index.js";
|
|
|
16
16
|
export * from "./app/index.js";
|
|
17
17
|
export { FLOW_INVALID_INDEX, createFlowRuntimeHost } from "./flow/flowRuntimeHost.js";
|
|
18
18
|
export { createIsomorphicFlowRuntimeHost } from "./flow/isomorphicFlowHost.js";
|
|
19
|
+
// The runtime-target gate: a composed flow derives its runtimeTargets, so a
|
|
20
|
+
// single-leg artifact exists and every loader refuses one that is not its own.
|
|
21
|
+
// Exported so a consumer can catch the refusal by CLASS rather than by
|
|
22
|
+
// matching a message string, and can ask the same question before offering a
|
|
23
|
+
// module to a leg.
|
|
24
|
+
export {
|
|
25
|
+
RuntimeTargetError,
|
|
26
|
+
runtimeTargetSatisfies,
|
|
27
|
+
resolveRuntimeTargetRefusal,
|
|
28
|
+
embeddedRuntimeTargets,
|
|
29
|
+
} from "./host/runtimeTargetGate.js";
|
|
19
30
|
export {
|
|
20
31
|
DefaultInvokeExports,
|
|
21
32
|
DefaultManifestExports,
|
|
@@ -15,7 +15,16 @@ import {
|
|
|
15
15
|
import { FlatBufferTypeRefT } from "../generated/orbpro/stream/flat-buffer-type-ref.js";
|
|
16
16
|
import { ProtocolRole, ProtocolTransportKind } from "../runtime/constants.js";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The manifest-string → PluginFamily vocabulary. This is the WHOLE vocabulary:
|
|
20
|
+
* anything not in here is refused by name, never coerced.
|
|
21
|
+
*
|
|
22
|
+
* `datasource` is the ONE alias, kept because manifests in the field spell it
|
|
23
|
+
* both ways. Do not add aliases casually — an alias is a second spelling of a
|
|
24
|
+
* family, and every one of them is a place two modules can disagree about what
|
|
25
|
+
* they are.
|
|
26
|
+
*/
|
|
27
|
+
export const pluginFamilyByName = Object.freeze({
|
|
19
28
|
sensor: PluginFamily.SENSOR,
|
|
20
29
|
propagator: PluginFamily.PROPAGATOR,
|
|
21
30
|
renderer: PluginFamily.RENDERER,
|
|
@@ -28,8 +37,89 @@ const pluginFamilyByName = Object.freeze({
|
|
|
28
37
|
infrastructure: PluginFamily.INFRASTRUCTURE,
|
|
29
38
|
flow: PluginFamily.FLOW,
|
|
30
39
|
bridge: PluginFamily.BRIDGE,
|
|
40
|
+
maneuver: PluginFamily.MANEUVER,
|
|
41
|
+
orbit_determination: PluginFamily.ORBIT_DETERMINATION,
|
|
42
|
+
foundation: PluginFamily.FOUNDATION,
|
|
43
|
+
parser: PluginFamily.PARSER,
|
|
44
|
+
validator: PluginFamily.VALIDATOR,
|
|
45
|
+
exporter: PluginFamily.EXPORTER,
|
|
46
|
+
publisher: PluginFamily.PUBLISHER,
|
|
47
|
+
basilisk: PluginFamily.BASILISK,
|
|
31
48
|
});
|
|
32
49
|
|
|
50
|
+
/** Every accepted family string, sorted — the vocabulary a refusal names. */
|
|
51
|
+
export const PluginFamilyNames = Object.freeze(
|
|
52
|
+
Object.keys(pluginFamilyByName).sort(),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SDK family → authoritative SDS `pluginCategory` member name.
|
|
57
|
+
*
|
|
58
|
+
* SDS (spacedatastandards.org `schema/PLG/main.fbs`) is the source of truth for
|
|
59
|
+
* what families exist; this SDK enum is a projection of it. The two DO NOT
|
|
60
|
+
* share ordinals — they diverge from index 5 (SDK `COMMS = 5`, SDS `EW = 5`) —
|
|
61
|
+
* so this mapping is BY NAME and must stay explicit. Never convert one enum to
|
|
62
|
+
* the other numerically.
|
|
63
|
+
*
|
|
64
|
+
* Two entries have no 1:1 SDS member and are recorded honestly rather than
|
|
65
|
+
* hidden. Both are filed in graph/tasks/sds-plugin-category-projection-gaps.md:
|
|
66
|
+
* - ORBIT_DETERMINATION → Analysis (SDS has no OD category yet)
|
|
67
|
+
* - SDF, BRIDGE → Analysis / Infrastructure (SDK-local concepts)
|
|
68
|
+
*/
|
|
69
|
+
export const sdsPluginCategoryByFamily = Object.freeze({
|
|
70
|
+
[PluginFamily.SENSOR]: "Sensor",
|
|
71
|
+
[PluginFamily.PROPAGATOR]: "Propagator",
|
|
72
|
+
[PluginFamily.RENDERER]: "Renderer",
|
|
73
|
+
[PluginFamily.ANALYSIS]: "Analysis",
|
|
74
|
+
[PluginFamily.DATA_SOURCE]: "DataSource",
|
|
75
|
+
[PluginFamily.COMMS]: "Comms",
|
|
76
|
+
[PluginFamily.SHADER]: "Shader",
|
|
77
|
+
[PluginFamily.SDF]: "Analysis",
|
|
78
|
+
[PluginFamily.INFRASTRUCTURE]: "Infrastructure",
|
|
79
|
+
[PluginFamily.FLOW]: "Flow",
|
|
80
|
+
[PluginFamily.BRIDGE]: "Infrastructure",
|
|
81
|
+
[PluginFamily.MANEUVER]: "Maneuver",
|
|
82
|
+
[PluginFamily.ORBIT_DETERMINATION]: "Analysis",
|
|
83
|
+
[PluginFamily.FOUNDATION]: "Foundation",
|
|
84
|
+
[PluginFamily.PARSER]: "Parser",
|
|
85
|
+
[PluginFamily.VALIDATOR]: "Validator",
|
|
86
|
+
[PluginFamily.EXPORTER]: "Exporter",
|
|
87
|
+
[PluginFamily.PUBLISHER]: "Publisher",
|
|
88
|
+
[PluginFamily.BASILISK]: "Basilisk",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const PluginFamilyValues = Object.freeze(
|
|
92
|
+
new Set(Object.values(pluginFamilyByName)),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Thrown when a manifest declares a family the SDK does not know.
|
|
97
|
+
*
|
|
98
|
+
* It is a THROW and not a fallback on purpose. The old code returned
|
|
99
|
+
* `PluginFamily.ANALYSIS` for anything unrecognized, which is why 19
|
|
100
|
+
* first-party modules shipped mislabelled and family-typed resolution only
|
|
101
|
+
* ever worked for propagators: a typo and a deliberate new family were
|
|
102
|
+
* indistinguishable, and both were silent.
|
|
103
|
+
*
|
|
104
|
+
* Ruling: graph/findings/official-harness-shapes.md §4.7 / §8.3
|
|
105
|
+
*/
|
|
106
|
+
export class UnknownPluginFamilyError extends Error {
|
|
107
|
+
constructor(value) {
|
|
108
|
+
super(
|
|
109
|
+
`Unknown pluginFamily ${JSON.stringify(value)}. ` +
|
|
110
|
+
`The manifest vocabulary is: ${PluginFamilyNames.join(", ")}. ` +
|
|
111
|
+
`Families are NOT invented in a manifest — the SDK enum ` +
|
|
112
|
+
`(schemas/PluginManifest.fbs PluginFamily) is a projection of the ` +
|
|
113
|
+
`authoritative SDS pluginCategory vocabulary, and a new family is ` +
|
|
114
|
+
`appended there first.`,
|
|
115
|
+
);
|
|
116
|
+
this.name = "UnknownPluginFamilyError";
|
|
117
|
+
this.code = "unknown-plugin-family";
|
|
118
|
+
this.value = value;
|
|
119
|
+
this.validFamilies = PluginFamilyNames;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
33
123
|
const drainPolicyByName = Object.freeze({
|
|
34
124
|
"single-shot": ManifestDrainPolicy.SINGLE_SHOT,
|
|
35
125
|
"drain-until-yield": ManifestDrainPolicy.DRAIN_UNTIL_YIELD,
|
|
@@ -145,14 +235,33 @@ function normalizeUnsignedInteger(value, fallback = 0) {
|
|
|
145
235
|
return Math.max(0, Math.trunc(normalized));
|
|
146
236
|
}
|
|
147
237
|
|
|
148
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Resolve a manifest `pluginFamily` to its enum value, or REFUSE.
|
|
240
|
+
*
|
|
241
|
+
* There is no fallback. See {@link UnknownPluginFamilyError} for why.
|
|
242
|
+
*
|
|
243
|
+
* @param {string|number} value
|
|
244
|
+
* @returns {number} a PluginFamily member
|
|
245
|
+
* @throws {UnknownPluginFamilyError} on an unknown string, an out-of-range
|
|
246
|
+
* number, or a missing/blank value.
|
|
247
|
+
*/
|
|
248
|
+
export function normalizePluginFamily(value) {
|
|
149
249
|
if (typeof value === "number") {
|
|
250
|
+
// A numeric family still has to BE one. An out-of-range ordinal is the
|
|
251
|
+
// same defect as an unknown string, and used to sail straight through.
|
|
252
|
+
if (!PluginFamilyValues.has(value)) {
|
|
253
|
+
throw new UnknownPluginFamilyError(value);
|
|
254
|
+
}
|
|
150
255
|
return value;
|
|
151
256
|
}
|
|
152
|
-
const normalized = String(value ?? "
|
|
257
|
+
const normalized = String(value ?? "")
|
|
153
258
|
.trim()
|
|
154
259
|
.toLowerCase();
|
|
155
|
-
|
|
260
|
+
const resolved = pluginFamilyByName[normalized];
|
|
261
|
+
if (resolved === undefined) {
|
|
262
|
+
throw new UnknownPluginFamilyError(value);
|
|
263
|
+
}
|
|
264
|
+
return resolved;
|
|
156
265
|
}
|
|
157
266
|
|
|
158
267
|
function normalizeDrainPolicy(value) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { substituteTokens } from "./tokens.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Refuse to scaffold into a non-empty directory unless `force` is set. Never
|
|
8
|
+
* deletes anything — `force` only lifts the refusal, it does not clear the
|
|
9
|
+
* directory first, so pre-existing unrelated files are left alone and
|
|
10
|
+
* template files land on top of (overwrite) any same-named files.
|
|
11
|
+
*/
|
|
12
|
+
export async function ensureWritableOutputDir(outDir, force) {
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = await fs.readdir(outDir);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error && error.code === "ENOENT") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
if (entries.length > 0 && !force) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Refusing to scaffold into non-empty directory ${outDir} ` +
|
|
25
|
+
`(${entries.length} existing ${entries.length === 1 ? "entry" : "entries"}). ` +
|
|
26
|
+
`Pass --force to scaffold into it anyway.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy every file under `templateDir` into `outDir`, applying token
|
|
33
|
+
* substitution to BOTH file contents and file/directory names. Every
|
|
34
|
+
* template file is treated as UTF-8 text — correct for this SDK's templates
|
|
35
|
+
* (JSON/JS/C/C++/Markdown), and deliberate: a template that ever needs a
|
|
36
|
+
* binary asset is a signal to reconsider, not something this copier should
|
|
37
|
+
* silently support.
|
|
38
|
+
*
|
|
39
|
+
* Returns the sorted list of output-relative (posix-style) file paths that
|
|
40
|
+
* were written.
|
|
41
|
+
*/
|
|
42
|
+
export async function copyTemplateTree(templateDir, outDir, tokens) {
|
|
43
|
+
const created = [];
|
|
44
|
+
|
|
45
|
+
async function walk(currentTemplateDir, currentOutDir) {
|
|
46
|
+
const entries = await fs.readdir(currentTemplateDir, {
|
|
47
|
+
withFileTypes: true,
|
|
48
|
+
});
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const destName = substituteTokens(entry.name, tokens);
|
|
51
|
+
const srcPath = path.join(currentTemplateDir, entry.name);
|
|
52
|
+
const destPath = path.join(currentOutDir, destName);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
55
|
+
await walk(srcPath, destPath);
|
|
56
|
+
} else if (entry.isFile()) {
|
|
57
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
58
|
+
const raw = await fs.readFile(srcPath, "utf8");
|
|
59
|
+
await fs.writeFile(destPath, substituteTokens(raw, tokens), "utf8");
|
|
60
|
+
created.push(
|
|
61
|
+
path.relative(outDir, destPath).split(path.sep).join("/"),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
68
|
+
await walk(templateDir, outDir);
|
|
69
|
+
created.sort();
|
|
70
|
+
return created;
|
|
71
|
+
}
|