Module publication standard
This document defines how a compliant Space Data module is published through the language package ecosystems used around the SDK runtime surface:
- npm
- PyPI
- Maven Central
- NuGet
- Go modules
- crates.io
- Swift Package Manager
The goal is simple: a loader should be able to inspect a package, locate the module artifact, and determine whether signatures or encrypted transport metadata are appended as SDS publication records in the module delivery file or shipped as sidecar FlatBuffers.
Scope
This publication standard covers two release modes:
standalone: the package's primary deliverable is the SDN module itselfattached: the package primarily exists for another language runtime, but it also ships one compliant SDN module build artifact
In both cases the module artifact remains the same canonical format already defined by this repo:
- a runtime payload that is valid WebAssembly bytes once any SDS publication trailer has been stripped and any encrypted delivery payload has been decrypted
- embedded
PluginManifest.fbs - manifest accessors
plugin_get_manifest_flatbufferplugin_get_manifest_flatbuffer_size- optional appended SDS
RECtrailer carryingMBL,PNM, and optionalENC
Core Rules
- A signed-only or unencrypted artifact payload before any publication trailer MUST remain valid
.wasm. - An encrypted binary delivery file MUST be encoded as encrypted payload bytes followed by an appended SDS
RECtrailer. The bytes before the trailer are ciphertext and are not required to validate as wasm until decrypted. - If bundle, signature, or encrypted-delivery metadata are carried in the same file, they MUST be appended as an SDS
RECtrailer at the end of that file. RECtrailers MUST carry standards-sourcedMBLmetadata plusPNMand optionalENCrecords where applicable.- Single-file bundle metadata MUST be read from the appended
RECtrailer, not from an in-wasm custom section. - Sidecar FlatBuffers are allowed when a package chooses not to append those metadata payloads to the module artifact.
- Paths in publication metadata are package-relative, never absolute.
Publication Record Extensions
Publication protection metadata is expressed as standards-backed FlatBuffer extensions layered on top of the canonical module artifact.
REC.fbsis the trailing collection wrapper with file identifier$RECMBL.fbsis the module bundle metadata recordPNM.fbsis the signature/publication notice recordENC.fbsis the encrypted-delivery record
These records are not arbitrary bytes. Loaders and publishers are expected to use the generated message classes from spacedatastandards.org and preserve the standard file identifiers:
REC=>$RECMBL=>$MBLPNM=>$PNMENC=>$ENC
The runtime-facing rule stays strict:
- Strip or decrypt the publication layer first.
- Instantiate the remaining raw wasm module.
- Read the embedded
PluginManifest.fbs.
MBL, PNM, and ENC extend publication and transport handling only. They do not change the canonical module ABI or manifest exports.
Protected Binary Layout
The official same-file protection layout is:
protected-payload-bytes || REC-flatbuffer-bytes || uint32le(REC length) || "$REC"For signed-only delivery, protected-payload-bytes are the wasm bytes. For encrypted delivery, protected-payload-bytes are ciphertext and the appended REC MUST contain an ENC record with the decryption parameters. Loaders MUST decrypt those ciphertext bytes before attempting wasm validation, manifest inspection, or bundle metadata parsing.
The PNM content identity applies to the protected payload bytes as stored in the file. For encrypted delivery this means the PNM.CID identifies the ciphertext payload, while the decrypted bytes remain the canonical wasm module that is passed to the runtime.
PNM digital-signature extension
PNM carries the publication notice for the module:
- file identity (
FILE_NAME,FILE_ID) - content identity (
CID) - publish timestamp
- signature and signature-type metadata
In practice this is the record a host inspects to determine what artifact was published and which signer attested to it.
MBL signature entry — domain separation (statementDomain)
The detached Ed25519 signature carried as the MBL bundle's signature-role entry has two forms, and the ARTIFACT chooses which one applies — never the verifier, never a policy:
statementDomain | signature covers | ||||
|---|---|---|---|---|---|
SDN-MODULE-PUBLICATION-V1 | `"SDN-MODULE-PUBLICATION-V1" \ | \ | 0x00 \ | \ | sha256(portable)` |
| absent | the bare canonical module (or bundle) digest — legacy |
portable is the trailer-stripped payload: the exact bytes the runtime instantiates and the node's capability policy identifies by content hash.
The domain prefix exists because the node's publisher key is the NODE key, and that one bonded key signs several unrelated kinds of statement (dataset publications, module artifacts, update SIGNALS — the advisory pub/sub nudge a publisher pushes so every install upgrades itself in place, which has a live producer — and, still reserved, update manifests). If every statement were a bare SHA-256 digest, a caller who could reach the node's signing endpoint could submit the bytes of one kind of document and staple the returned signature onto another. An ASCII domain label, a NUL that cannot occur inside the label, and a fixed-width 32-byte tail put each statement kind in a disjoint message space with no length ambiguity anywhere in the preimage.
The domain registry is closed: a verifier refuses any label that is not registered, and a module verifier additionally refuses any registered label other than SDN-MODULE-PUBLICATION-V1 — so a signature minted for an update manifest or an update signal can never be replayed into a module trailer, which the shared vectors pin as foreign-registered-domain and update-signal-domain-is-refused-for-a-module. Adding a domain is a reviewed change in every implementation at once (src/bundle/sigdomain.js here; internal/sigdomain and its kubo twin in the node), never a request parameter.
statementDomain and algorithm are compared after trimming Go's whitespace set (unicode.IsSpace / Unicode White_Space), which is not JavaScript's: Go trims U+0085 NEL and JS does not, JS trims U+FEFF BOM and Go does not. Either mismatch is a "loads on the node, dies in the browser" split, so both are pinned by vectors rather than left to each language's default.
Legacy artifacts that predate the field keep verifying unchanged. The forms, the preimage bytes and every refusal reason are pinned as shared cross-language vectors in test/support/statement-domain-vectors.json, byte-identical to the node's copies, and asserted by the JS suite and both Go suites.
ENC encrypted-delivery extension
ENC carries the decryption parameters for a transport-protected module:
- key exchange algorithm (
X25519) - symmetric algorithm (
AES_256_GCM; wire enum value1) - key-derivation function (
HKDF_SHA256) - ephemeral public key
- nonce start (the 12-byte AES-GCM IV)
- optional context, schema hash, recipient key id, and root type
Encrypted delivery is authenticated:
- The protected payload bytes are laid out as
ciphertext || 16-byte GCM tag(theENCschema has no dedicated tag field). - The encoded
ENCFlatBuffer record itself is the GCM additional authenticated data (AAD), so the context, schema hash, root type, recipient key id, nonce, and ephemeral key are all bound to the ciphertext. Tampering with either the payload or theENCrecord MUST cause decryption to fail. - Records labelled
AES_256_CTRare rejected; the SDK neither produces nor decrypts unauthenticated CTR payloads.
It describes how to decrypt the protected delivery payload. It does not imply a different module file format after decryption.
Aligned-Binary Type Refs
Aligned-binary payloads use the same schema identity as the canonical FlatBuffer payload. They are advertised through the FlatBufferTypeRef extension fields:
wireFormat: "aligned-binary"rootTypeNamebyteLengthrequiredAlignment
Every aligned-binary declaration must be paired with the regular wireFormat: "flatbuffer" type for the same schema and file identifier in the same accepted type set. Publication protection applies to the artifact as a whole; aligned-binary is an invoke/payload optimization layered inside the manifest contract.
Canonical Descriptor
The canonical publication descriptor is named sdn-module.
When represented as a standalone JSON file, the filename is sdn-module.json.
The full object form is:
{
"specVersion": 1,
"publicationMode": "attached",
"module": {
"path": "./dist/orbit-lib.module.wasm",
"packaging": "sds-bundled-wasm",
"mediaType": "application/wasm",
"manifestExportSymbol": "plugin_get_manifest_flatbuffer",
"manifestSizeSymbol": "plugin_get_manifest_flatbuffer_size",
"pluginId": "com.example.orbit-lib",
"version": "1.2.3"
},
"artifacts": {
"signature": {
"storage": "module-trailer",
"schemaName": "PNM.fbs",
"fileIdentifier": "$PNM"
},
"transport": {
"storage": "module-trailer",
"schemaName": "ENC.fbs",
"fileIdentifier": "$ENC"
}
},
"integrity": {
"moduleSha256": "2bff0d3d8f4f5aa1d0c7be3e54e15f9f8c9f3a62d5f0a4e8a8d80de28f4f0b31"
}
}Required Fields
specVersion: publication spec version, currently1publicationMode:standaloneorattachedmodule.path: relative path to the.wasmartifact
Recommended Module Repo Build Layout
When authoring a module repo before publication packaging, keep the shared compiled artifact at a stable runtime path:
- required:
dist/isomorphic/module.wasm
If the repo also ships a browser-specific adapter or wrapper, place it beside the browser runtime path:
- optional:
dist/browser/module.js - optional:
dist/browser/module.wasm
The publication descriptor can still point anywhere, but the SDK standard for checked-in module repos is that the exact shared browser/WasmEdge build lands at dist/isomorphic/module.wasm.
Recommended Fields
module.packaging:plain-wasmorsds-bundled-wasmmodule.mediaType: normallyapplication/wasmmodule.manifestExportSymbolmodule.manifestSizeSymbolmodule.pluginIdmodule.versionintegrity.moduleSha256
Artifact Descriptors
Each optional entry inside artifacts describes one metadata payload related to the module:
authorizationsignaturetransportattestation
An artifact descriptor has this shape:
{
"storage": "module-trailer",
"path": "./dist/orbit-lib.signature.fb",
"schemaName": "PNM.fbs",
"fileIdentifier": "$PNM"
}Rules:
storageMUST bemodule-trailerorpackage-filemodule-trailermeans the loader scans the end ofmodule.pathfor an appendedRECtrailer and resolves the matching record from therepathMUST be present whenstorageispackage-fileschemaNameSHOULD name the SDS FlatBuffer schemafileIdentifierSHOULD name the FlatBuffer file identifier
Minimal Shorthand
Package manifests that support arbitrary metadata MAY use a shorthand string when all of the following are true:
- the package only needs to point to one module file
- the module uses the default manifest accessor exports
- all signature and transport metadata are either absent or appended through a
RECtrailer
Example:
{
"name": "@example/orbit-lib",
"version": "1.2.3",
"sdn-module": "./dist/orbit-lib.module.wasm"
}sdn-module.json files MUST use the full object form, not the string shorthand.
Resolution Rules
A package consumer resolves publication metadata in this order:
- Read the ecosystem-specific
sdn-modulecarrier if one exists. - If that carrier is a string, treat it as
module.path. - If that carrier is an object, use it directly.
- If the package has no inline carrier, look for
sdn-module.json. - For JVM artifacts, also look for
META-INF/sdn-module.json. - Resolve
module.pathand anypackage-filesidecars relative to the package root or archive root.
Packaging By Ecosystem
| Ecosystem | Recommended carrier |
|---|---|
| npm | package.json["sdn-module"] |
| PyPI | pyproject.toml [tool."sdn-module"] |
| crates.io | Cargo.toml [package.metadata."sdn-module"] |
| Maven Central | META-INF/sdn-module.json inside the JAR, optionally mirrored by a POM property |
| NuGet | sdn-module.json at package root, optionally surfaced through build metadata |
| Go modules | sdn-module.json at module root |
| Swift Package Manager | sdn-module.json at package root |
npm
Minimal attached example:
{
"name": "@example/orbit-lib",
"version": "1.2.3",
"sdn-module": "./dist/orbit-lib.module.wasm"
}Full example:
{
"name": "@example/orbit-lib",
"version": "1.2.3",
"sdn-module": {
"specVersion": 1,
"publicationMode": "attached",
"module": {
"path": "./dist/orbit-lib.module.wasm",
"packaging": "sds-bundled-wasm",
"mediaType": "application/wasm",
"manifestExportSymbol": "plugin_get_manifest_flatbuffer",
"manifestSizeSymbol": "plugin_get_manifest_flatbuffer_size"
}
}
}PyPI
[tool."sdn-module"]
specVersion = 1
publicationMode = "attached"
[tool."sdn-module".module]
path = "./dist/orbit-lib.module.wasm"
packaging = "sds-bundled-wasm"
mediaType = "application/wasm"
manifestExportSymbol = "plugin_get_manifest_flatbuffer"
manifestSizeSymbol = "plugin_get_manifest_flatbuffer_size"crates.io
[package.metadata."sdn-module"]
specVersion = 1
publicationMode = "attached"
[package.metadata."sdn-module".module]
path = "./dist/orbit-lib.module.wasm"
packaging = "sds-bundled-wasm"
mediaType = "application/wasm"
manifestExportSymbol = "plugin_get_manifest_flatbuffer"
manifestSizeSymbol = "plugin_get_manifest_flatbuffer_size"Maven Central And Kotlin
Ship a JSON descriptor in the archive:
src/main/resources/META-INF/sdn-module.jsonOptional POM property:
<properties>
<sdn.module.descriptor>META-INF/sdn-module.json</sdn.module.descriptor>
</properties>NuGet
Ship the descriptor at package root:
sdn-module.jsonThe .nupkg should also contain the module artifact at the relative path named by module.path.
Go Modules
Ship sdn-module.json at the module root next to go.mod.
Swift Package Manager
Ship sdn-module.json at the package root. If the module artifact must be available through a target at runtime, include the .wasm and sidecar files as resources or through a binary-target wrapper.
Standalone Publication Example
sdn-module.json
{
"specVersion": 1,
"publicationMode": "standalone",
"module": {
"path": "./dist/catalog-query.bundle.wasm",
"packaging": "sds-bundled-wasm",
"mediaType": "application/wasm",
"manifestExportSymbol": "plugin_get_manifest_flatbuffer",
"manifestSizeSymbol": "plugin_get_manifest_flatbuffer_size",
"pluginId": "org.example.catalog-query",
"version": "0.2.0"
},
"artifacts": {
"signature": {
"storage": "module-trailer",
"schemaName": "PNM.fbs",
"fileIdentifier": "$PNM"
}
}
}Attached Publication Example
sdn-module.json
{
"specVersion": 1,
"publicationMode": "attached",
"module": {
"path": "./dist/orbit-lib.module.wasm",
"packaging": "plain-wasm",
"mediaType": "application/wasm",
"manifestExportSymbol": "plugin_get_manifest_flatbuffer",
"manifestSizeSymbol": "plugin_get_manifest_flatbuffer_size",
"pluginId": "com.example.orbit-lib",
"version": "1.2.3"
},
"artifacts": {
"signature": {
"storage": "package-file",
"path": "./dist/orbit-lib.signature.fb",
"schemaName": "PNM.fbs",
"fileIdentifier": "$PNM"
},
"transport": {
"storage": "package-file",
"path": "./dist/orbit-lib.transport.fb",
"schemaName": "ENC.fbs",
"fileIdentifier": "$ENC"
}
}
}Loader Expectations
A loader consuming this standard SHOULD:
- locate the publication descriptor
- read
module.path - scan the artifact from the end for an appended SDS
RECtrailer - resolve
PNM/ENCfrom that trailer before runtime startup - if
ENCis present, decrypt the protected payload bytes before passing bytes to WasmEdge or any other runtime - if
ENCis absent, strip the trailer and use the remaining wasm payload - inspect the parsed
RECtrailer forMBL - resolve any
package-filemetadata through relative paths - validate manifest exports and any declared integrity hashes
If module.packaging is sds-bundled-wasm, loaders SHOULD treat the decrypted or stripped wasm payload as the runtime artifact and the appended REC trailer as the single-file bundle/publication metadata container.
Relationship To Existing Bundle Format
This standard uses the appended REC trailer as the single-file publication container. It explains how a package publishes and points to the module artifact plus any appended SDS publication trailer.
RECstays the appended single-file/publication record containerMBLcarries bundle metadata inside that trailersdn-moduleis the package-discovery descriptor
Use the appended REC trailer when you want one self-describing protected artifact. Use sdn-module when you want package managers and loaders to discover that file reliably.