space-data-module-sdk 0.5.10 → 0.5.14

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 CHANGED
@@ -14,28 +14,11 @@ This repository is the source of truth for module-level concerns:
14
14
  - the `sds.bundle` single-file container
15
15
  - deployment authorization plus SDS publication records (`REC`, `PNM`, `ENC`)
16
16
  - the first canonical module hostcall/import ABI surface
17
- - shared module-level testing/runtime harnesses, including the WasmEdge
18
- process runner used by package-level validation suites
19
17
 
20
18
  <p align="center">
21
19
  <img src="docs/architecture.svg" alt="Module architecture overview" width="820" />
22
20
  </p>
23
21
 
24
- ## Shared Harness Ownership
25
-
26
- This repo owns the generic module-side runtime harnesses used across the stack:
27
-
28
- - `createModuleHarness(...)` for process and WasmEdge-backed module invocation
29
- - `resolveModuleHarnessLaunchPlan(...)` for portable launch planning
30
- - `buildWasmEdgeEmscriptenPthreadRunner(...)` plus the shared native runner
31
- source and runner-level pthread smoke test
32
-
33
- Flow-specific standalone harnessing lives in `sdn-flow`, which layers flow
34
- enqueue/drain behavior on top of its compiled standalone runtime host. Package-
35
- specific validation suites, including conjunction replay/V&V, are expected to
36
- sit on top of these shared harnesses instead of defining their own runtime
37
- process model.
38
-
39
22
  ## Module Artifact Model
40
23
 
41
24
  A compliant module built with this SDK is always a valid `.wasm` artifact with:
@@ -195,11 +178,6 @@ semantics.
195
178
  This repo now exposes a manifest-driven harness generator from
196
179
  `space-data-module-sdk/testing` and two complementary integration suites:
197
180
 
198
- - shared process-level helpers for command-surface runtimes:
199
- - `createPluginInvokeProcessClient(...)`
200
- - `resolveWasmEdgePluginLaunchPlan(...)`
201
- - `buildWasmEdgeEmscriptenPthreadRunner(...)`
202
-
203
181
  - `npm run test:runtime-matrix`
204
182
  - cross-language runtime smoke across the same WASM in Node.js, Go, Python,
205
183
  Rust, Java, C#, and Swift
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.5.10",
3
+ "version": "0.5.14",
4
4
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
5
5
  "type": "module",
6
6
  "types": "./src/index.d.ts",
@@ -167,6 +167,134 @@ function normalizePayloadWireFormatName(value) {
167
167
  return null;
168
168
  }
169
169
 
170
+ function normalizedPluginFamilyName(value) {
171
+ if (!isNonEmptyString(value)) {
172
+ return null;
173
+ }
174
+ return value
175
+ .trim()
176
+ .toLowerCase()
177
+ .replace(/-/g, "_");
178
+ }
179
+
180
+ function typeSetHasDualWireFormats(typeSet) {
181
+ if (!Array.isArray(typeSet?.allowedTypes) || typeSet.allowedTypes.length === 0) {
182
+ return false;
183
+ }
184
+ let hasFlatbuffer = false;
185
+ let hasAlignedBinary = false;
186
+ for (const allowedType of typeSet.allowedTypes) {
187
+ const wireFormat = normalizePayloadWireFormatName(allowedType?.wireFormat);
188
+ if (wireFormat === "flatbuffer") {
189
+ hasFlatbuffer = true;
190
+ } else if (wireFormat === "aligned-binary") {
191
+ hasAlignedBinary = true;
192
+ }
193
+ }
194
+ return hasFlatbuffer && hasAlignedBinary;
195
+ }
196
+
197
+ function validateCanonicalDualFormatPort(port, issues, location, methodId, portId) {
198
+ if (!port) {
199
+ pushIssue(
200
+ issues,
201
+ "error",
202
+ "propagator-missing-canonical-port",
203
+ `Canonical propagator method "${methodId}" must declare port "${portId}".`,
204
+ location,
205
+ );
206
+ return;
207
+ }
208
+ if (!Array.isArray(port.acceptedTypeSets) || port.acceptedTypeSets.length === 0) {
209
+ pushIssue(
210
+ issues,
211
+ "error",
212
+ "propagator-missing-canonical-port",
213
+ `Canonical propagator method "${methodId}" port "${portId}" must declare acceptedTypeSets.`,
214
+ `${location}.acceptedTypeSets`,
215
+ );
216
+ return;
217
+ }
218
+ port.acceptedTypeSets.forEach((typeSet, index) => {
219
+ if (!typeSetHasDualWireFormats(typeSet)) {
220
+ pushIssue(
221
+ issues,
222
+ "error",
223
+ "canonical-port-missing-dual-wire-format",
224
+ `Canonical propagator method "${methodId}" port "${portId}" acceptedTypeSet "${typeSet?.setId ?? index}" must accept both regular flatbuffer and aligned-binary payloads.`,
225
+ `${location}.acceptedTypeSets[${index}]`,
226
+ );
227
+ }
228
+ });
229
+ }
230
+
231
+ function validateCanonicalPropagatorContract(manifest, issues, sourceName) {
232
+ if (normalizedPluginFamilyName(manifest?.pluginFamily) !== "propagator") {
233
+ return;
234
+ }
235
+
236
+ const methods = Array.isArray(manifest?.methods) ? manifest.methods : [];
237
+ const methodRequirements = [
238
+ {
239
+ methodId: "ingest_omm",
240
+ inputPorts: ["omm"],
241
+ outputPorts: [],
242
+ },
243
+ {
244
+ methodId: "describe_sources_batch",
245
+ inputPorts: ["request"],
246
+ outputPorts: ["result"],
247
+ },
248
+ {
249
+ methodId: "propagate_state",
250
+ inputPorts: ["request"],
251
+ outputPorts: ["state"],
252
+ },
253
+ ];
254
+
255
+ for (const requirement of methodRequirements) {
256
+ const methodIndex = methods.findIndex(
257
+ (method) => method?.methodId === requirement.methodId,
258
+ );
259
+ if (methodIndex === -1) {
260
+ pushIssue(
261
+ issues,
262
+ "error",
263
+ "propagator-missing-canonical-method",
264
+ `Propagator plugins must declare canonical method "${requirement.methodId}".`,
265
+ `${sourceName}.methods`,
266
+ );
267
+ continue;
268
+ }
269
+ const method = methods[methodIndex];
270
+ const location = `${sourceName}.methods[${methodIndex}]`;
271
+ for (const portId of requirement.inputPorts) {
272
+ const port = Array.isArray(method.inputPorts)
273
+ ? method.inputPorts.find((entry) => entry?.portId === portId)
274
+ : null;
275
+ validateCanonicalDualFormatPort(
276
+ port,
277
+ issues,
278
+ `${location}.inputPorts`,
279
+ requirement.methodId,
280
+ portId,
281
+ );
282
+ }
283
+ for (const portId of requirement.outputPorts) {
284
+ const port = Array.isArray(method.outputPorts)
285
+ ? method.outputPorts.find((entry) => entry?.portId === portId)
286
+ : null;
287
+ validateCanonicalDualFormatPort(
288
+ port,
289
+ issues,
290
+ `${location}.outputPorts`,
291
+ requirement.methodId,
292
+ portId,
293
+ );
294
+ }
295
+ }
296
+ }
297
+
170
298
  function validateStringField(issues, value, location, label) {
171
299
  if (!isNonEmptyString(value)) {
172
300
  pushIssue(issues, "error", "missing-string", `${label} must be a non-empty string.`, location);
@@ -366,7 +494,7 @@ function validateAllowedType(type, issues, location) {
366
494
  "Allowed type fixedStringLength",
367
495
  { min: 0 },
368
496
  );
369
- validateIntegerField(
497
+ validateOptionalIntegerField(
370
498
  issues,
371
499
  type.byteLength,
372
500
  `${location}.byteLength`,
@@ -1259,6 +1387,8 @@ export function validatePluginManifest(manifest, options = {}) {
1259
1387
  }
1260
1388
  });
1261
1389
 
1390
+ validateCanonicalPropagatorContract(manifest, issues, sourceName);
1391
+
1262
1392
  if (manifest.timers !== undefined && !Array.isArray(manifest.timers)) {
1263
1393
  pushIssue(
1264
1394
  issues,
package/src/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export type ProtocolRoleName = "handle" | "dial" | "both";
11
11
 
12
12
  export interface PayloadTypeRef {
13
13
  schemaName?: string;
14
- fileIdentifier?: string;
14
+ fileIdentifier?: string | null;
15
15
  schemaHash?: string | number[] | Uint8Array;
16
16
  acceptsAnyFlatbuffer?: boolean;
17
17
  wireFormat?: PayloadWireFormat;
@@ -662,25 +662,12 @@ export type {
662
662
  HarnessInvokeScenario,
663
663
  HarnessRawScenario,
664
664
  ManifestHarnessPlan,
665
- ModuleHarness,
666
- ModuleHarnessRuntimeDescriptor,
667
- PluginInvokeProcessClient,
668
- PluginInvokeProcessLaunchPlan,
669
- WasmEdgeRunnerBuildPlan,
670
665
  } from "./testing/index.js";
671
666
 
672
667
  export {
673
- buildWasmEdgeEmscriptenPthreadRunner,
674
- buildWasmEdgeSpawnEnv,
675
- createModuleHarness,
676
- createPluginInvokeProcessClient,
677
668
  describeCapabilityRuntimeSurface,
678
669
  generateManifestHarnessPlan,
679
670
  materializeHarnessScenario,
680
- resolveModuleHarnessLaunchPlan,
681
- resolveWasmEdgeRunnerBuildPlan,
682
- resolveWasmEdgeRunnerSourcePath,
683
- resolveWasmEdgePluginLaunchPlan,
684
671
  serializeHarnessPlan,
685
672
  } from "./testing/index.js";
686
673
 
@@ -12,7 +12,7 @@ import { InvokeSurface } from "../generated/orbpro/manifest/invoke-surface.js";
12
12
  import { BufferMutability } from "../generated/orbpro/stream/buffer-mutability.js";
13
13
  import { BufferOwnership } from "../generated/orbpro/stream/buffer-ownership.js";
14
14
  import { FlatBufferTypeRefT } from "../generated/orbpro/stream/flat-buffer-type-ref.js";
15
- import { TypedArenaBufferT } from "../generated/orbpro/stream/typed-arena-buffer.js";
15
+ import { TypedArenaBuffer, TypedArenaBufferT } from "../generated/orbpro/stream/typed-arena-buffer.js";
16
16
  import { toUint8Array } from "../runtime/bufferLike.js";
17
17
 
18
18
  function toByteBuffer(data) {
@@ -149,22 +149,20 @@ function normalizeArenaFrame(frame = {}, offset) {
149
149
 
150
150
  function packArenaFrames(frames = []) {
151
151
  const packedFrames = [];
152
- const normalizedFrames = [];
152
+ const arena = [];
153
153
  let offset = 0;
154
154
  for (const frame of frames) {
155
155
  const normalized = normalizeArenaFrame(frame, offset);
156
+ for (let index = 0; index < normalized.padding; index += 1) {
157
+ arena.push(0);
158
+ }
159
+ arena.push(...normalized.payload);
156
160
  offset = normalized.buffer.offset + normalized.buffer.size;
157
161
  packedFrames.push(normalized.buffer);
158
- normalizedFrames.push(normalized);
159
- }
160
-
161
- const arena = new Uint8Array(offset);
162
- for (const normalized of normalizedFrames) {
163
- arena.set(normalized.payload, normalized.buffer.offset);
164
162
  }
165
163
  return {
166
164
  frames: packedFrames,
167
- arena,
165
+ arena: Uint8Array.from(arena),
168
166
  };
169
167
  }
170
168
 
@@ -1,31 +1,62 @@
1
1
  function cloneSchemaHash(value) {
2
2
  if (value instanceof Uint8Array) {
3
- return new Uint8Array(value);
3
+ return value.byteLength > 0 ? new Uint8Array(value) : undefined;
4
4
  }
5
5
  if (Array.isArray(value)) {
6
- return [...value];
6
+ return value.length > 0 ? [...value] : undefined;
7
7
  }
8
8
  return value ?? undefined;
9
9
  }
10
10
 
11
+ function normalizeNullableString(value) {
12
+ if (value === undefined || value === null) {
13
+ return null;
14
+ }
15
+ const normalized = String(value).trim();
16
+ return normalized.length > 0 ? normalized : null;
17
+ }
18
+
19
+ function normalizeAlignedMetadataScalar(value) {
20
+ if (value === undefined || value === null || value === "") {
21
+ return undefined;
22
+ }
23
+ const numeric = Number(value);
24
+ if (Number.isFinite(numeric)) {
25
+ return numeric === 0 ? undefined : numeric;
26
+ }
27
+ return value;
28
+ }
29
+
11
30
  export function clonePayloadTypeRef(value = null) {
12
31
  if (!value || typeof value !== "object") {
13
- return { acceptsAnyFlatbuffer: true };
32
+ return { acceptsAnyFlatbuffer: true, fileIdentifier: null };
14
33
  }
15
34
  return {
16
- schemaName: value.schemaName ?? value.schema_name ?? undefined,
17
- fileIdentifier: value.fileIdentifier ?? value.file_identifier ?? undefined,
35
+ schemaName: normalizeNullableString(
36
+ value.schemaName ?? value.schema_name,
37
+ ),
38
+ fileIdentifier: normalizeNullableString(
39
+ value.fileIdentifier ?? value.file_identifier,
40
+ ),
18
41
  schemaHash: cloneSchemaHash(value.schemaHash ?? value.schema_hash),
19
42
  acceptsAnyFlatbuffer: Boolean(
20
43
  value.acceptsAnyFlatbuffer ?? value.accepts_any_flatbuffer ?? false,
21
44
  ),
22
- wireFormat: value.wireFormat ?? value.wire_format ?? undefined,
23
- rootTypeName: value.rootTypeName ?? value.root_type_name ?? undefined,
24
- fixedStringLength:
25
- value.fixedStringLength ?? value.fixed_string_length ?? undefined,
26
- byteLength: value.byteLength ?? value.byte_length ?? undefined,
27
- requiredAlignment:
28
- value.requiredAlignment ?? value.required_alignment ?? undefined,
45
+ wireFormat: normalizePayloadWireFormatName(
46
+ value.wireFormat ?? value.wire_format,
47
+ ),
48
+ rootTypeName: normalizeNullableString(
49
+ value.rootTypeName ?? value.root_type_name,
50
+ ),
51
+ fixedStringLength: normalizeAlignedMetadataScalar(
52
+ value.fixedStringLength ?? value.fixed_string_length,
53
+ ),
54
+ byteLength: normalizeAlignedMetadataScalar(
55
+ value.byteLength ?? value.byte_length,
56
+ ),
57
+ requiredAlignment: normalizeAlignedMetadataScalar(
58
+ value.requiredAlignment ?? value.required_alignment,
59
+ ),
29
60
  };
30
61
  }
31
62
 
@@ -33,6 +64,12 @@ export function normalizePayloadWireFormatName(value) {
33
64
  if (value === undefined || value === null || value === "") {
34
65
  return null;
35
66
  }
67
+ if (value === 1 || value === "1") {
68
+ return "aligned-binary";
69
+ }
70
+ if (value === 0 || value === "0") {
71
+ return "flatbuffer";
72
+ }
36
73
  const normalized = String(value).trim().toLowerCase().replace(/_/g, "-");
37
74
  if (normalized === "aligned-binary") {
38
75
  return "aligned-binary";
@@ -57,69 +57,6 @@ export interface ManifestHarnessPlan {
57
57
  scenarios: Array<HarnessInvokeScenario | HarnessRawScenario>;
58
58
  }
59
59
 
60
- export interface PluginInvokeProcessLaunchPlan {
61
- command: string;
62
- args: string[];
63
- env?: Record<string, string | undefined>;
64
- cwd?: string;
65
- wasmPath?: string;
66
- }
67
-
68
- export interface PluginInvokeProcessClient {
69
- launchPlan: PluginInvokeProcessLaunchPlan;
70
- invokeRaw(requestBytes: Uint8Array | ArrayBuffer | ArrayBufferView): Promise<Uint8Array>;
71
- invoke(request: {
72
- methodId?: string | null;
73
- inputs?: HarnessInputFrame[];
74
- }): Promise<{
75
- statusCode: number;
76
- errorCode?: string | null;
77
- errorMessage?: string | null;
78
- outputs: HarnessInputFrame[];
79
- }>;
80
- destroy(): Promise<void>;
81
- }
82
-
83
- export interface ModuleHarnessRuntimeDescriptor {
84
- kind?: "process" | "wasmedge";
85
- launchPlan?: PluginInvokeProcessLaunchPlan;
86
- command?: string;
87
- args?: string[];
88
- env?: Record<string, string | undefined>;
89
- cwd?: string;
90
- wasmPath?: string;
91
- wasmEdgeBinary?: string;
92
- wasmEdgeRunnerBinary?: string;
93
- enableThreads?: boolean;
94
- }
95
-
96
- export interface ModuleHarness {
97
- runtime: ModuleHarnessRuntimeDescriptor & { kind: "process" | "wasmedge" };
98
- launchPlan: PluginInvokeProcessLaunchPlan;
99
- invokeRaw(requestBytes: Uint8Array | ArrayBuffer | ArrayBufferView): Promise<Uint8Array>;
100
- invoke(request: {
101
- methodId?: string | null;
102
- inputs?: HarnessInputFrame[];
103
- }): Promise<{
104
- statusCode: number;
105
- errorCode?: string | null;
106
- errorMessage?: string | null;
107
- outputs: HarnessInputFrame[];
108
- }>;
109
- destroy(): Promise<void>;
110
- }
111
-
112
- export interface WasmEdgeRunnerBuildPlan {
113
- runnerSourcePath: string;
114
- requestedIncludeDir: string;
115
- wasmedgeIncludeDir: string;
116
- wasmedgeLibDir: string;
117
- wasmedgeSharedLibraryPath: string;
118
- outputPath: string;
119
- compilerCommand: string;
120
- compilerArgs: string[];
121
- }
122
-
123
60
  export function describeCapabilityRuntimeSurface(
124
61
  capability: string,
125
62
  ): CapabilityRuntimeSurface;
@@ -147,49 +84,3 @@ export function materializeHarnessScenario(
147
84
  };
148
85
 
149
86
  export function serializeHarnessPlan(plan: ManifestHarnessPlan): unknown;
150
-
151
- export function buildWasmEdgeSpawnEnv(
152
- baseEnv?: Record<string, string | undefined>,
153
- ): Record<string, string | undefined>;
154
-
155
- export function resolveWasmEdgePluginLaunchPlan(options: {
156
- wasmPath: string;
157
- wasmEdgeBinary?: string;
158
- wasmEdgeRunnerBinary?: string;
159
- enableThreads?: boolean;
160
- invokeArgs?: string[];
161
- env?: Record<string, string | undefined>;
162
- }): PluginInvokeProcessLaunchPlan;
163
-
164
- export function createPluginInvokeProcessClient(options: {
165
- launchPlan?: PluginInvokeProcessLaunchPlan;
166
- command?: string;
167
- args?: string[];
168
- env?: Record<string, string | undefined>;
169
- cwd?: string;
170
- }): Promise<PluginInvokeProcessClient>;
171
-
172
- export function resolveModuleHarnessLaunchPlan(options: {
173
- runtime?: ModuleHarnessRuntimeDescriptor;
174
- } | ModuleHarnessRuntimeDescriptor): PluginInvokeProcessLaunchPlan;
175
-
176
- export function createModuleHarness(options: {
177
- runtime?: ModuleHarnessRuntimeDescriptor;
178
- } | ModuleHarnessRuntimeDescriptor): Promise<ModuleHarness>;
179
-
180
- export function resolveWasmEdgeRunnerSourcePath(): string;
181
-
182
- export function resolveWasmEdgeRunnerBuildPlan(options: {
183
- outputPath: string;
184
- wasmedgeIncludeDir?: string;
185
- wasmedgeLibDir?: string;
186
- output?: string;
187
- }): WasmEdgeRunnerBuildPlan;
188
-
189
- export function buildWasmEdgeEmscriptenPthreadRunner(options: {
190
- outputPath: string;
191
- wasmedgeIncludeDir?: string;
192
- wasmedgeLibDir?: string;
193
- output?: string;
194
- cwd?: string;
195
- }): Promise<string>;
@@ -1,22 +1,6 @@
1
- import { Buffer } from "node:buffer";
2
-
3
1
  import { encodePluginInvokeRequest } from "../invoke/codec.js";
4
2
  import { normalizeInvokeSurfaces } from "../invoke/index.js";
5
3
  import { selectPreferredPayloadTypeRef } from "../manifest/typeRefs.js";
6
- export {
7
- buildWasmEdgeSpawnEnv,
8
- createPluginInvokeProcessClient,
9
- resolveWasmEdgePluginLaunchPlan,
10
- } from "./processInvoke.js";
11
- export {
12
- buildWasmEdgeEmscriptenPthreadRunner,
13
- resolveWasmEdgeRunnerBuildPlan,
14
- resolveWasmEdgeRunnerSourcePath,
15
- } from "./buildWasmEdgeRunner.js";
16
- export {
17
- createModuleHarness,
18
- resolveModuleHarnessLaunchPlan,
19
- } from "./moduleHarness.js";
20
4
 
21
5
  const CapabilitySurfaceMatrix = Object.freeze({
22
6
  logging: Object.freeze({
@@ -277,27 +277,7 @@ function validateEncTable(table, buffer, label) {
277
277
  maxLength: 32,
278
278
  });
279
279
  assertOptionalStringField(buffer, tableMeta, 22, `${label} root type`);
280
- const timestamp = table.TIMESTAMP();
281
- const record = {
282
- version: Number(table.VERSION()),
283
- keyExchange:
284
- KEY_EXCHANGE_NAME_BY_VALUE[table.KEY_EXCHANGE()] ??
285
- String(table.KEY_EXCHANGE()),
286
- symmetric:
287
- SYMMETRIC_ALGO_NAME_BY_VALUE[table.SYMMETRIC()] ??
288
- String(table.SYMMETRIC()),
289
- keyDerivation:
290
- KDF_NAME_BY_VALUE[table.KEY_DERIVATION()] ??
291
- String(table.KEY_DERIVATION()),
292
- ephemeralPublicKey: normalizeByteField(table.ephemeralPublicKeyArray()),
293
- nonceStart: normalizeByteField(table.nonceStartArray()),
294
- recipientKeyId: normalizeByteField(table.recipientKeyIdArray()),
295
- context: normalizeStringField(table.CONTEXT()),
296
- schemaHash: normalizeByteField(table.schemaHashArray()),
297
- rootType: normalizeStringField(table.ROOT_TYPE()),
298
- timestamp:
299
- timestamp === undefined || timestamp === null ? 0 : Number(timestamp),
300
- };
280
+ const record = normalizeEncTable(table.unpack());
301
281
  if (!record.ephemeralPublicKey?.length) {
302
282
  throw new Error(`${label} is missing the ephemeral public key.`);
303
283
  }
@@ -336,17 +316,7 @@ function validatePnmTable(table, buffer, label) {
336
316
  assertOptionalStringField(buffer, tableMeta, 16, `${label} timestamp signature`);
337
317
  assertOptionalStringField(buffer, tableMeta, 18, `${label} signature type`);
338
318
  assertOptionalStringField(buffer, tableMeta, 20, `${label} timestamp signature type`);
339
- const record = {
340
- multiformatAddress: normalizeStringField(table.MULTIFORMAT_ADDRESS()),
341
- publishTimestamp: normalizeStringField(table.PUBLISH_TIMESTAMP()),
342
- cid: normalizeStringField(table.CID()),
343
- fileName: normalizeStringField(table.FILE_NAME()),
344
- fileId: normalizeStringField(table.FILE_ID()),
345
- signature: normalizeStringField(table.SIGNATURE()),
346
- timestampSignature: normalizeStringField(table.TIMESTAMP_SIGNATURE()),
347
- signatureType: normalizeStringField(table.SIGNATURE_TYPE()),
348
- timestampSignatureType: normalizeStringField(table.TIMESTAMP_SIGNATURE_TYPE()),
349
- };
319
+ const record = normalizePnmTable(table.unpack());
350
320
  if (
351
321
  !record.multiformatAddress &&
352
322
  !record.publishTimestamp &&
@@ -456,6 +426,48 @@ function pnmTableFromObject(record = {}) {
456
426
  );
457
427
  }
458
428
 
429
+ function normalizeEncTable(table) {
430
+ if (!table) {
431
+ return null;
432
+ }
433
+ return {
434
+ version: Number(table.VERSION ?? 1),
435
+ keyExchange:
436
+ KEY_EXCHANGE_NAME_BY_VALUE[table.KEY_EXCHANGE] ?? String(table.KEY_EXCHANGE),
437
+ symmetric:
438
+ SYMMETRIC_ALGO_NAME_BY_VALUE[table.SYMMETRIC] ?? String(table.SYMMETRIC),
439
+ keyDerivation:
440
+ KDF_NAME_BY_VALUE[table.KEY_DERIVATION] ?? String(table.KEY_DERIVATION),
441
+ ephemeralPublicKey: normalizeByteField(table.EPHEMERAL_PUBLIC_KEY),
442
+ nonceStart: normalizeByteField(table.NONCE_START),
443
+ recipientKeyId: normalizeByteField(table.RECIPIENT_KEY_ID),
444
+ context: normalizeStringField(table.CONTEXT),
445
+ schemaHash: normalizeByteField(table.SCHEMA_HASH),
446
+ rootType: normalizeStringField(table.ROOT_TYPE),
447
+ timestamp:
448
+ table.TIMESTAMP === undefined || table.TIMESTAMP === null
449
+ ? 0
450
+ : Number(table.TIMESTAMP),
451
+ };
452
+ }
453
+
454
+ function normalizePnmTable(table) {
455
+ if (!table) {
456
+ return null;
457
+ }
458
+ return {
459
+ multiformatAddress: normalizeStringField(table.MULTIFORMAT_ADDRESS),
460
+ publishTimestamp: normalizeStringField(table.PUBLISH_TIMESTAMP),
461
+ cid: normalizeStringField(table.CID),
462
+ fileName: normalizeStringField(table.FILE_NAME),
463
+ fileId: normalizeStringField(table.FILE_ID),
464
+ signature: normalizeStringField(table.SIGNATURE),
465
+ timestampSignature: normalizeStringField(table.TIMESTAMP_SIGNATURE),
466
+ signatureType: normalizeStringField(table.SIGNATURE_TYPE),
467
+ timestampSignatureType: normalizeStringField(table.TIMESTAMP_SIGNATURE_TYPE),
468
+ };
469
+ }
470
+
459
471
  function readFooterLength(bytes) {
460
472
  const view = toUint8Array(bytes);
461
473
  if (view.length < TRAILER_FOOTER_LENGTH) {
@@ -589,6 +601,7 @@ export function decodePublicationRecordCollection(bytes) {
589
601
  if (recordTables.length === 0) {
590
602
  throw new Error("REC trailer does not contain any records.");
591
603
  }
604
+ const collection = collectionTable.unpack();
592
605
  const records = [];
593
606
  let enc = null;
594
607
  let pnm = null;
@@ -609,9 +622,10 @@ export function decodePublicationRecordCollection(bytes) {
609
622
  8,
610
623
  `REC trailer record ${index} standard`,
611
624
  );
625
+ const unpackedRecord = collection.RECORDS[index];
612
626
  const recordType = recordTable.value_type();
613
627
  const standard =
614
- normalizeStringField(recordTable.standard()) ??
628
+ normalizeStringField(unpackedRecord?.standard) ??
615
629
  STANDARD_BY_RECORD_TYPE[recordType] ??
616
630
  null;
617
631
  const expectedStandard =
@@ -630,7 +644,10 @@ export function decodePublicationRecordCollection(bytes) {
630
644
  if (!valueMeta) {
631
645
  throw new Error(`REC trailer record ${index} is missing a value.`);
632
646
  }
633
- let value = null;
647
+ let value =
648
+ standard === "ENC" || standard === "PNM"
649
+ ? null
650
+ : unpackedRecord?.value ?? null;
634
651
  if (standard === "ENC") {
635
652
  const encTable = recordTable.value(new ENC());
636
653
  if (!encTable) {
@@ -667,9 +684,7 @@ export function decodePublicationRecordCollection(bytes) {
667
684
  });
668
685
  }
669
686
  return {
670
- version:
671
- normalizeStringField(collectionTable.version()) ??
672
- DEFAULT_RECORD_COLLECTION_VERSION,
687
+ version: normalizeStringField(collection.version) ?? DEFAULT_RECORD_COLLECTION_VERSION,
673
688
  records,
674
689
  enc,
675
690
  pnm,