space-data-module-sdk 0.5.3 → 0.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
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",
@@ -3,7 +3,7 @@ import * as flatbuffers from "flatbuffers";
3
3
  import { ENC, ENCT, KDF, KeyExchange, SymmetricAlgo } from "spacedatastandards.org/lib/js/ENC/main.js";
4
4
  import { PNM, PNMT } from "spacedatastandards.org/lib/js/PNM/main.js";
5
5
  import { REC, RECT } from "spacedatastandards.org/lib/js/REC/REC.js";
6
- import { RecordT } from "spacedatastandards.org/lib/js/REC/Record.js";
6
+ import { Record, RecordT } from "spacedatastandards.org/lib/js/REC/Record.js";
7
7
  import { RecordType } from "spacedatastandards.org/lib/js/REC/RecordType.js";
8
8
 
9
9
  import {
@@ -58,6 +58,281 @@ const STANDARD_BY_RECORD_TYPE = Object.freeze(
58
58
  );
59
59
  const textEncoder = new TextEncoder();
60
60
 
61
+ function assertBounds(buffer, offset, length, label) {
62
+ if (
63
+ !Number.isSafeInteger(offset) ||
64
+ !Number.isSafeInteger(length) ||
65
+ offset < 0 ||
66
+ length < 0 ||
67
+ offset + length > buffer.length
68
+ ) {
69
+ throw new Error(`${label} is out of bounds.`);
70
+ }
71
+ }
72
+
73
+ function readUint16LE(buffer, offset, label) {
74
+ assertBounds(buffer, offset, 2, label);
75
+ return buffer[offset] | (buffer[offset + 1] << 8);
76
+ }
77
+
78
+ function readUint32LE(buffer, offset, label) {
79
+ assertBounds(buffer, offset, 4, label);
80
+ return (
81
+ buffer[offset] |
82
+ (buffer[offset + 1] << 8) |
83
+ (buffer[offset + 2] << 16) |
84
+ (buffer[offset + 3] << 24)
85
+ ) >>> 0;
86
+ }
87
+
88
+ function readInt32LE(buffer, offset, label) {
89
+ return readUint32LE(buffer, offset, label) | 0;
90
+ }
91
+
92
+ function readTableFieldOffset(buffer, tableMeta, vtableFieldOffset) {
93
+ const fieldEntryOffset = tableMeta.vtableStart + vtableFieldOffset;
94
+ if (fieldEntryOffset + 2 > tableMeta.vtableEnd) {
95
+ return 0;
96
+ }
97
+ return readUint16LE(buffer, fieldEntryOffset, `${tableMeta.label} field offset`);
98
+ }
99
+
100
+ function resolveRelativeOffset(buffer, offset, label) {
101
+ const relativeOffset = readInt32LE(buffer, offset, label);
102
+ const target = offset + relativeOffset;
103
+ if (!Number.isSafeInteger(target) || target < 0 || target > buffer.length - 4) {
104
+ throw new Error(`${label} points outside the FlatBuffer.`);
105
+ }
106
+ return target;
107
+ }
108
+
109
+ function assertFlatbufferIdentifier(buffer, identifier, label) {
110
+ if (identifier.length !== flatbuffers.FILE_IDENTIFIER_LENGTH) {
111
+ throw new Error(`FlatBuffer identifier "${identifier}" must be 4 bytes.`);
112
+ }
113
+ assertBounds(
114
+ buffer,
115
+ flatbuffers.SIZEOF_INT,
116
+ flatbuffers.FILE_IDENTIFIER_LENGTH,
117
+ `${label} identifier`,
118
+ );
119
+ for (let index = 0; index < identifier.length; index += 1) {
120
+ if (buffer[flatbuffers.SIZEOF_INT + index] !== identifier.charCodeAt(index)) {
121
+ throw new Error(`${label} is missing the ${identifier} file identifier.`);
122
+ }
123
+ }
124
+ }
125
+
126
+ function assertFlatbufferTable(buffer, tableStart, label) {
127
+ assertBounds(buffer, tableStart, 4, `${label} table header`);
128
+ const vtableDistance = readInt32LE(buffer, tableStart, `${label} vtable offset`);
129
+ const vtableStart = tableStart - vtableDistance;
130
+ if (
131
+ !Number.isSafeInteger(vtableStart) ||
132
+ vtableStart < 0 ||
133
+ vtableStart > buffer.length - 4
134
+ ) {
135
+ throw new Error(`${label} vtable offset is invalid.`);
136
+ }
137
+ const vtableLength = readUint16LE(buffer, vtableStart, `${label} vtable length`);
138
+ const objectLength = readUint16LE(
139
+ buffer,
140
+ vtableStart + 2,
141
+ `${label} object length`,
142
+ );
143
+ if (vtableLength < 4 || (vtableLength & 1) !== 0) {
144
+ throw new Error(`${label} vtable length is invalid.`);
145
+ }
146
+ if (objectLength < 4) {
147
+ throw new Error(`${label} object length is invalid.`);
148
+ }
149
+ assertBounds(buffer, vtableStart, vtableLength, `${label} vtable`);
150
+ assertBounds(buffer, tableStart, objectLength, `${label} object`);
151
+ for (let entryOffset = vtableStart + 4; entryOffset < vtableStart + vtableLength; entryOffset += 2) {
152
+ const fieldOffset = readUint16LE(buffer, entryOffset, `${label} field entry`);
153
+ if (fieldOffset !== 0 && (fieldOffset < 4 || fieldOffset >= objectLength)) {
154
+ throw new Error(`${label} field offset is invalid.`);
155
+ }
156
+ }
157
+ return {
158
+ label,
159
+ tableStart,
160
+ tableEnd: tableStart + objectLength,
161
+ objectLength,
162
+ vtableStart,
163
+ vtableEnd: vtableStart + vtableLength,
164
+ vtableLength,
165
+ };
166
+ }
167
+
168
+ function assertRootFlatbufferTable(buffer, identifier, label) {
169
+ assertFlatbufferIdentifier(buffer, identifier, label);
170
+ const rootTableStart = readUint32LE(buffer, 0, `${label} root offset`);
171
+ if (
172
+ !Number.isSafeInteger(rootTableStart) ||
173
+ rootTableStart < flatbuffers.SIZEOF_INT + flatbuffers.FILE_IDENTIFIER_LENGTH ||
174
+ rootTableStart > buffer.length - 4
175
+ ) {
176
+ throw new Error(`${label} root offset is invalid.`);
177
+ }
178
+ return assertFlatbufferTable(buffer, rootTableStart, label);
179
+ }
180
+
181
+ function assertOptionalStringField(buffer, tableMeta, vtableFieldOffset, label) {
182
+ const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
183
+ if (fieldOffset === 0) {
184
+ return null;
185
+ }
186
+ const fieldStart = tableMeta.tableStart + fieldOffset;
187
+ const stringStart = resolveRelativeOffset(buffer, fieldStart, label);
188
+ const stringLength = readUint32LE(buffer, stringStart, `${label} length`);
189
+ assertBounds(buffer, stringStart + 4, stringLength, `${label} data`);
190
+ return {
191
+ fieldStart,
192
+ stringStart,
193
+ stringLength,
194
+ };
195
+ }
196
+
197
+ function assertOptionalByteVectorField(
198
+ buffer,
199
+ tableMeta,
200
+ vtableFieldOffset,
201
+ label,
202
+ { minLength = 0, maxLength = Number.MAX_SAFE_INTEGER } = {},
203
+ ) {
204
+ const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
205
+ if (fieldOffset === 0) {
206
+ return null;
207
+ }
208
+ const fieldStart = tableMeta.tableStart + fieldOffset;
209
+ const vectorStart = resolveRelativeOffset(buffer, fieldStart, label);
210
+ const vectorLength = readUint32LE(buffer, vectorStart, `${label} length`);
211
+ if (vectorLength < minLength || vectorLength > maxLength) {
212
+ throw new Error(`${label} length is invalid.`);
213
+ }
214
+ assertBounds(buffer, vectorStart + 4, vectorLength, `${label} data`);
215
+ return {
216
+ fieldStart,
217
+ vectorStart,
218
+ vectorLength,
219
+ };
220
+ }
221
+
222
+ function assertTableVectorField(buffer, tableMeta, vtableFieldOffset, label) {
223
+ const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
224
+ if (fieldOffset === 0) {
225
+ return [];
226
+ }
227
+ const fieldStart = tableMeta.tableStart + fieldOffset;
228
+ const vectorStart = resolveRelativeOffset(buffer, fieldStart, label);
229
+ const vectorLength = readUint32LE(buffer, vectorStart, `${label} length`);
230
+ const vectorDataStart = vectorStart + 4;
231
+ assertBounds(
232
+ buffer,
233
+ vectorDataStart,
234
+ vectorLength * 4,
235
+ `${label} offsets`,
236
+ );
237
+ const elements = [];
238
+ for (let index = 0; index < vectorLength; index += 1) {
239
+ const elementOffset = vectorDataStart + index * 4;
240
+ const tableStart = resolveRelativeOffset(
241
+ buffer,
242
+ elementOffset,
243
+ `${label}[${index}]`,
244
+ );
245
+ elements.push(
246
+ assertFlatbufferTable(buffer, tableStart, `${label}[${index}]`),
247
+ );
248
+ }
249
+ return elements;
250
+ }
251
+
252
+ function assertUnionTableField(buffer, tableMeta, vtableFieldOffset, label) {
253
+ const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
254
+ if (fieldOffset === 0) {
255
+ return null;
256
+ }
257
+ const fieldStart = tableMeta.tableStart + fieldOffset;
258
+ const tableStart = resolveRelativeOffset(buffer, fieldStart, label);
259
+ return assertFlatbufferTable(buffer, tableStart, label);
260
+ }
261
+
262
+ function validateEncTable(table, buffer, label) {
263
+ const tableMeta = assertFlatbufferTable(buffer, table.bb_pos, label);
264
+ assertOptionalByteVectorField(buffer, tableMeta, 12, `${label} ephemeral public key`, {
265
+ minLength: 1,
266
+ maxLength: 65,
267
+ });
268
+ assertOptionalByteVectorField(buffer, tableMeta, 14, `${label} nonce start`, {
269
+ minLength: 12,
270
+ maxLength: 12,
271
+ });
272
+ assertOptionalByteVectorField(buffer, tableMeta, 16, `${label} recipient key id`, {
273
+ maxLength: 32,
274
+ });
275
+ assertOptionalStringField(buffer, tableMeta, 18, `${label} context`);
276
+ assertOptionalByteVectorField(buffer, tableMeta, 20, `${label} schema hash`, {
277
+ maxLength: 32,
278
+ });
279
+ assertOptionalStringField(buffer, tableMeta, 22, `${label} root type`);
280
+ const record = normalizeEncTable(table.unpack());
281
+ if (!record.ephemeralPublicKey?.length) {
282
+ throw new Error(`${label} is missing the ephemeral public key.`);
283
+ }
284
+ if (!record.nonceStart || record.nonceStart.length !== 12) {
285
+ throw new Error(`${label} nonce start must be 12 bytes.`);
286
+ }
287
+ if (
288
+ record.keyExchange === "X25519" &&
289
+ record.ephemeralPublicKey.length !== 32
290
+ ) {
291
+ throw new Error(`${label} X25519 ephemeral public key must be 32 bytes.`);
292
+ }
293
+ if (
294
+ record.keyExchange !== "X25519" &&
295
+ (record.ephemeralPublicKey.length < 32 || record.ephemeralPublicKey.length > 65)
296
+ ) {
297
+ throw new Error(`${label} ephemeral public key length is invalid.`);
298
+ }
299
+ if (record.recipientKeyId && record.recipientKeyId.length > 32) {
300
+ throw new Error(`${label} recipient key id is too large.`);
301
+ }
302
+ if (record.schemaHash && record.schemaHash.length !== 32) {
303
+ throw new Error(`${label} schema hash must be 32 bytes when present.`);
304
+ }
305
+ return record;
306
+ }
307
+
308
+ function validatePnmTable(table, buffer, label) {
309
+ const tableMeta = assertFlatbufferTable(buffer, table.bb_pos, label);
310
+ assertOptionalStringField(buffer, tableMeta, 4, `${label} multiformat address`);
311
+ assertOptionalStringField(buffer, tableMeta, 6, `${label} publish timestamp`);
312
+ assertOptionalStringField(buffer, tableMeta, 8, `${label} cid`);
313
+ assertOptionalStringField(buffer, tableMeta, 10, `${label} file name`);
314
+ assertOptionalStringField(buffer, tableMeta, 12, `${label} file id`);
315
+ assertOptionalStringField(buffer, tableMeta, 14, `${label} signature`);
316
+ assertOptionalStringField(buffer, tableMeta, 16, `${label} timestamp signature`);
317
+ assertOptionalStringField(buffer, tableMeta, 18, `${label} signature type`);
318
+ assertOptionalStringField(buffer, tableMeta, 20, `${label} timestamp signature type`);
319
+ const record = normalizePnmTable(table.unpack());
320
+ if (
321
+ !record.multiformatAddress &&
322
+ !record.publishTimestamp &&
323
+ !record.cid &&
324
+ !record.fileName &&
325
+ !record.fileId &&
326
+ !record.signature &&
327
+ !record.timestampSignature &&
328
+ !record.signatureType &&
329
+ !record.timestampSignatureType
330
+ ) {
331
+ throw new Error(`${label} must contain at least one populated field.`);
332
+ }
333
+ return record;
334
+ }
335
+
61
336
  function concatBytes(chunks) {
62
337
  const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);
63
338
  const out = new Uint8Array(totalLength);
@@ -73,7 +348,8 @@ function normalizeByteField(value) {
73
348
  if (value === undefined || value === null) {
74
349
  return null;
75
350
  }
76
- return toUint8Array(value);
351
+ const normalized = toUint8Array(value);
352
+ return normalized.length > 0 ? normalized : null;
77
353
  }
78
354
 
79
355
  function normalizeStringField(value) {
@@ -264,12 +540,10 @@ export function encodeEncRecord(record = {}) {
264
540
  }
265
541
 
266
542
  export function decodeEncRecord(bytes) {
267
- const bb = new flatbuffers.ByteBuffer(toUint8Array(bytes));
268
- if (!ENC.bufferHasIdentifier(bb)) {
269
- throw new Error("ENC record is missing the $ENC file identifier.");
270
- }
271
- const record = ENC.getRootAsENC(bb).unpack();
272
- return normalizeEncTable(record);
543
+ const buffer = toUint8Array(bytes);
544
+ assertRootFlatbufferTable(buffer, "$ENC", "ENC record");
545
+ const bb = new flatbuffers.ByteBuffer(buffer);
546
+ return validateEncTable(ENC.getRootAsENC(bb), buffer, "ENC record");
273
547
  }
274
548
 
275
549
  export function encodePnmRecord(record = {}) {
@@ -281,12 +555,10 @@ export function encodePnmRecord(record = {}) {
281
555
  }
282
556
 
283
557
  export function decodePnmRecord(bytes) {
284
- const bb = new flatbuffers.ByteBuffer(toUint8Array(bytes));
285
- if (!PNM.bufferHasIdentifier(bb)) {
286
- throw new Error("PNM record is missing the $PNM file identifier.");
287
- }
288
- const record = PNM.getRootAsPNM(bb).unpack();
289
- return normalizePnmTable(record);
558
+ const buffer = toUint8Array(bytes);
559
+ assertRootFlatbufferTable(buffer, "$PNM", "PNM record");
560
+ const bb = new flatbuffers.ByteBuffer(buffer);
561
+ return validatePnmTable(PNM.getRootAsPNM(bb), buffer, "PNM record");
290
562
  }
291
563
 
292
564
  export function encodePublicationRecordCollection(options = {}) {
@@ -311,33 +583,104 @@ export function encodePublicationRecordCollection(options = {}) {
311
583
 
312
584
  export function decodePublicationRecordCollection(bytes) {
313
585
  const buffer = toUint8Array(bytes);
586
+ assertRootFlatbufferTable(buffer, "$REC", "REC trailer");
314
587
  const bb = new flatbuffers.ByteBuffer(buffer);
315
- if (!REC.bufferHasIdentifier(bb)) {
316
- throw new Error("REC trailer is missing the $REC file identifier.");
588
+ const collectionTable = REC.getRootAsREC(bb);
589
+ const collectionMeta = assertFlatbufferTable(
590
+ buffer,
591
+ collectionTable.bb_pos,
592
+ "REC trailer",
593
+ );
594
+ assertOptionalStringField(buffer, collectionMeta, 4, "REC trailer version");
595
+ const recordTables = assertTableVectorField(
596
+ buffer,
597
+ collectionMeta,
598
+ 6,
599
+ "REC trailer records",
600
+ );
601
+ if (recordTables.length === 0) {
602
+ throw new Error("REC trailer does not contain any records.");
317
603
  }
318
- const collection = REC.getRootAsREC(bb).unpack();
604
+ const collection = collectionTable.unpack();
319
605
  const records = [];
320
606
  let enc = null;
321
607
  let pnm = null;
322
- for (const unpackedRecord of Array.isArray(collection.RECORDS) ? collection.RECORDS : []) {
608
+ for (let index = 0; index < recordTables.length; index += 1) {
609
+ const recordTable =
610
+ collectionTable.RECORDS(index, new Record()) ?? null;
611
+ if (!recordTable) {
612
+ throw new Error(`REC trailer record ${index} could not be loaded.`);
613
+ }
614
+ const recordMeta = assertFlatbufferTable(
615
+ buffer,
616
+ recordTable.bb_pos,
617
+ `REC trailer record ${index}`,
618
+ );
619
+ assertOptionalStringField(
620
+ buffer,
621
+ recordMeta,
622
+ 8,
623
+ `REC trailer record ${index} standard`,
624
+ );
625
+ const unpackedRecord = collection.RECORDS[index];
626
+ const recordType = recordTable.value_type();
323
627
  const standard =
324
- normalizeStringField(unpackedRecord.standard) ??
325
- STANDARD_BY_RECORD_TYPE[unpackedRecord.value_type] ??
628
+ normalizeStringField(unpackedRecord?.standard) ??
629
+ STANDARD_BY_RECORD_TYPE[recordType] ??
326
630
  null;
631
+ const expectedStandard =
632
+ STANDARD_BY_RECORD_TYPE[recordType] ?? null;
633
+ if (standard && expectedStandard && standard !== expectedStandard) {
634
+ throw new Error(
635
+ `REC trailer record ${index} standard/type mismatch (${standard} vs ${expectedStandard}).`,
636
+ );
637
+ }
638
+ const valueMeta = assertUnionTableField(
639
+ buffer,
640
+ recordMeta,
641
+ 6,
642
+ `REC trailer record ${index} value`,
643
+ );
644
+ if (!valueMeta) {
645
+ throw new Error(`REC trailer record ${index} is missing a value.`);
646
+ }
647
+ let value =
648
+ standard === "ENC" || standard === "PNM"
649
+ ? null
650
+ : unpackedRecord?.value ?? null;
327
651
  if (standard === "ENC") {
328
- enc = normalizeEncTable(unpackedRecord.value);
652
+ const encTable = recordTable.value(new ENC());
653
+ if (!encTable) {
654
+ throw new Error(`REC trailer record ${index} ENC payload is missing.`);
655
+ }
656
+ if (enc) {
657
+ throw new Error("REC trailer contains multiple ENC records.");
658
+ }
659
+ enc = validateEncTable(
660
+ encTable,
661
+ buffer,
662
+ `REC trailer record ${index} ENC payload`,
663
+ );
664
+ value = enc;
329
665
  } else if (standard === "PNM") {
330
- pnm = normalizePnmTable(unpackedRecord.value);
666
+ const pnmTable = recordTable.value(new PNM());
667
+ if (!pnmTable) {
668
+ throw new Error(`REC trailer record ${index} PNM payload is missing.`);
669
+ }
670
+ if (pnm) {
671
+ throw new Error("REC trailer contains multiple PNM records.");
672
+ }
673
+ pnm = validatePnmTable(
674
+ pnmTable,
675
+ buffer,
676
+ `REC trailer record ${index} PNM payload`,
677
+ );
678
+ value = pnm;
331
679
  }
332
680
  records.push({
333
681
  standard,
334
- recordType: unpackedRecord.value_type,
335
- value:
336
- standard === "ENC"
337
- ? enc
338
- : standard === "PNM"
339
- ? pnm
340
- : unpackedRecord.value,
682
+ recordType,
683
+ value,
341
684
  });
342
685
  }
343
686
  return {