space-data-module-sdk 0.5.3 → 0.5.5
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 +1 -1
- package/src/compiler/emception.d.ts +1 -0
- package/src/compiler/emception.js +5 -0
- package/src/compiler/emceptionNode.js +4 -0
- package/src/compiler/index.d.ts +1 -0
- package/src/compiler/index.js +1 -0
- package/src/compliance/pluginCompliance.js +58 -18
- package/src/host/abi.js +47 -1
- package/src/host/nodeHost.js +4 -4
- package/src/index.d.ts +2 -1
- package/src/manifest/codec.js +91 -0
- package/src/testing/index.js +2 -2
- package/src/transport/records.js +372 -29
package/package.json
CHANGED
|
@@ -54,6 +54,7 @@ export interface SharedEmceptionSession {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function createSharedEmceptionSession(): SharedEmceptionSession;
|
|
57
|
+
export function createIsolatedEmceptionSession(): SharedEmceptionSession;
|
|
57
58
|
export function loadSharedEmception(): Promise<unknown>;
|
|
58
59
|
export function withSharedEmception<T>(
|
|
59
60
|
task: (handle: SharedEmceptionHandle) => T | Promise<T>,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
createEmceptionController,
|
|
4
5
|
getSharedEmceptionController,
|
|
5
6
|
loadEmception,
|
|
6
7
|
runWithEmceptionLock,
|
|
@@ -180,6 +181,10 @@ export function createSharedEmceptionSession() {
|
|
|
180
181
|
return new SharedEmceptionSession();
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
export function createIsolatedEmceptionSession() {
|
|
185
|
+
return new SharedEmceptionSession(createEmceptionController());
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
export async function loadSharedEmception() {
|
|
184
189
|
return loadEmception();
|
|
185
190
|
}
|
|
@@ -225,6 +225,10 @@ export function getSharedEmceptionController() {
|
|
|
225
225
|
return sharedEmceptionController;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
export function createEmceptionController() {
|
|
229
|
+
return new EmceptionController();
|
|
230
|
+
}
|
|
231
|
+
|
|
228
232
|
export async function loadEmception() {
|
|
229
233
|
return sharedEmceptionController.load();
|
|
230
234
|
}
|
package/src/compiler/index.d.ts
CHANGED
package/src/compiler/index.js
CHANGED
|
@@ -175,6 +175,44 @@ function validateStringField(issues, value, location, label) {
|
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function validateCapabilityEntry(capability, issues, location) {
|
|
179
|
+
if (isNonEmptyString(capability)) {
|
|
180
|
+
return capability;
|
|
181
|
+
}
|
|
182
|
+
if (!capability || typeof capability !== "object" || Array.isArray(capability)) {
|
|
183
|
+
pushIssue(
|
|
184
|
+
issues,
|
|
185
|
+
"error",
|
|
186
|
+
"invalid-capability",
|
|
187
|
+
"Capability entries must be non-empty strings or host capability records.",
|
|
188
|
+
location,
|
|
189
|
+
);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!validateStringField(issues, capability.capability, `${location}.capability`, "Capability id")) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
validateOptionalStringField(
|
|
196
|
+
issues,
|
|
197
|
+
capability.scope,
|
|
198
|
+
`${location}.scope`,
|
|
199
|
+
"Capability scope",
|
|
200
|
+
);
|
|
201
|
+
validateOptionalBooleanField(
|
|
202
|
+
issues,
|
|
203
|
+
capability.required,
|
|
204
|
+
`${location}.required`,
|
|
205
|
+
"Capability required",
|
|
206
|
+
);
|
|
207
|
+
validateOptionalStringField(
|
|
208
|
+
issues,
|
|
209
|
+
capability.description,
|
|
210
|
+
`${location}.description`,
|
|
211
|
+
"Capability description",
|
|
212
|
+
);
|
|
213
|
+
return capability.capability;
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
function validateIntegerField(
|
|
179
217
|
issues,
|
|
180
218
|
value,
|
|
@@ -1066,8 +1104,9 @@ export function validatePluginManifest(manifest, options = {}) {
|
|
|
1066
1104
|
validateStringField(issues, manifest.version, `${sourceName}.version`, "version");
|
|
1067
1105
|
validateStringField(issues, manifest.pluginFamily, `${sourceName}.pluginFamily`, "pluginFamily");
|
|
1068
1106
|
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1107
|
+
const rawDeclaredCapabilities = manifest.capabilities;
|
|
1108
|
+
let declaredCapabilities = null;
|
|
1109
|
+
if (!Array.isArray(rawDeclaredCapabilities)) {
|
|
1071
1110
|
pushIssue(
|
|
1072
1111
|
issues,
|
|
1073
1112
|
"warning",
|
|
@@ -1076,38 +1115,39 @@ export function validatePluginManifest(manifest, options = {}) {
|
|
|
1076
1115
|
`${sourceName}.capabilities`,
|
|
1077
1116
|
);
|
|
1078
1117
|
} else {
|
|
1118
|
+
declaredCapabilities = [];
|
|
1079
1119
|
const seenCapabilities = new Set();
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
);
|
|
1089
|
-
continue;
|
|
1120
|
+
rawDeclaredCapabilities.forEach((capability, index) => {
|
|
1121
|
+
const normalizedCapability = validateCapabilityEntry(
|
|
1122
|
+
capability,
|
|
1123
|
+
issues,
|
|
1124
|
+
`${sourceName}.capabilities[${index}]`,
|
|
1125
|
+
);
|
|
1126
|
+
if (!normalizedCapability) {
|
|
1127
|
+
return;
|
|
1090
1128
|
}
|
|
1091
|
-
|
|
1129
|
+
declaredCapabilities.push(normalizedCapability);
|
|
1130
|
+
if (seenCapabilities.has(normalizedCapability)) {
|
|
1092
1131
|
pushIssue(
|
|
1093
1132
|
issues,
|
|
1094
1133
|
"warning",
|
|
1095
1134
|
"duplicate-capability",
|
|
1096
|
-
`Capability "${
|
|
1135
|
+
`Capability "${normalizedCapability}" is declared more than once.`,
|
|
1097
1136
|
`${sourceName}.capabilities`,
|
|
1098
1137
|
);
|
|
1138
|
+
return;
|
|
1099
1139
|
}
|
|
1100
|
-
seenCapabilities.add(
|
|
1101
|
-
if (!RecommendedCapabilitySet.has(
|
|
1140
|
+
seenCapabilities.add(normalizedCapability);
|
|
1141
|
+
if (!RecommendedCapabilitySet.has(normalizedCapability)) {
|
|
1102
1142
|
pushIssue(
|
|
1103
1143
|
issues,
|
|
1104
1144
|
"warning",
|
|
1105
1145
|
"noncanonical-capability",
|
|
1106
|
-
`Capability "${
|
|
1146
|
+
`Capability "${normalizedCapability}" is not in the current canonical SDN coarse capability set.`,
|
|
1107
1147
|
`${sourceName}.capabilities`,
|
|
1108
1148
|
);
|
|
1109
1149
|
}
|
|
1110
|
-
}
|
|
1150
|
+
});
|
|
1111
1151
|
}
|
|
1112
1152
|
validateRuntimeTargets(
|
|
1113
1153
|
manifest.runtimeTargets,
|
package/src/host/abi.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { bytesToBase64 } from "../utils/encoding.js";
|
|
2
|
+
|
|
1
3
|
const textDecoder = new TextDecoder();
|
|
2
4
|
const textEncoder = new TextEncoder();
|
|
3
5
|
|
|
@@ -14,6 +16,7 @@ export const NodeHostSyncHostcallOperations = Object.freeze([
|
|
|
14
16
|
"clock.now",
|
|
15
17
|
"clock.monotonicNow",
|
|
16
18
|
"clock.nowIso",
|
|
19
|
+
"random.bytes",
|
|
17
20
|
"schedule.parse",
|
|
18
21
|
"schedule.matches",
|
|
19
22
|
"schedule.next",
|
|
@@ -105,6 +108,47 @@ function isPromiseLike(value) {
|
|
|
105
108
|
);
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
function encodeHostcallValue(value) {
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
value instanceof Uint8Array ||
|
|
117
|
+
value instanceof ArrayBuffer ||
|
|
118
|
+
ArrayBuffer.isView(value)
|
|
119
|
+
) {
|
|
120
|
+
const bytes =
|
|
121
|
+
value instanceof Uint8Array
|
|
122
|
+
? value
|
|
123
|
+
: new Uint8Array(
|
|
124
|
+
value.buffer ?? value,
|
|
125
|
+
value.byteOffset ?? 0,
|
|
126
|
+
value.byteLength ?? value.byteLength,
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
__type: "bytes",
|
|
130
|
+
base64: bytesToBase64(bytes),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (value instanceof Date) {
|
|
134
|
+
return value.toISOString();
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === "bigint") {
|
|
137
|
+
return value.toString();
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.map((entry) => encodeHostcallValue(entry));
|
|
141
|
+
}
|
|
142
|
+
if (value && typeof value === "object") {
|
|
143
|
+
return Object.fromEntries(
|
|
144
|
+
Object.entries(value)
|
|
145
|
+
.filter(([, entry]) => entry !== undefined)
|
|
146
|
+
.map(([key, entry]) => [key, encodeHostcallValue(entry)]),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
export function dispatchNodeHostSyncOperation(host, operation, params = null) {
|
|
109
153
|
const normalized = assertNonEmptyString(operation, "Hostcall operation");
|
|
110
154
|
switch (normalized) {
|
|
@@ -124,6 +168,8 @@ export function dispatchNodeHostSyncOperation(host, operation, params = null) {
|
|
|
124
168
|
return host.clock.monotonicNow();
|
|
125
169
|
case "clock.nowIso":
|
|
126
170
|
return host.clock.nowIso();
|
|
171
|
+
case "random.bytes":
|
|
172
|
+
return host.random.bytes(params?.length);
|
|
127
173
|
case "schedule.parse":
|
|
128
174
|
return host.schedule.parse(params?.expression);
|
|
129
175
|
case "schedule.matches":
|
|
@@ -202,7 +248,7 @@ export function createJsonHostcallBridge(options = {}) {
|
|
|
202
248
|
}
|
|
203
249
|
setEnvelope(HOSTCALL_STATUS_OK, {
|
|
204
250
|
ok: true,
|
|
205
|
-
result,
|
|
251
|
+
result: encodeHostcallValue(result),
|
|
206
252
|
});
|
|
207
253
|
return HOSTCALL_STATUS_OK;
|
|
208
254
|
} catch (error) {
|
package/src/host/nodeHost.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dgram from "node:dgram";
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { randomBytes as nodeRandomBytes } from "node:crypto";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import { performance } from "node:perf_hooks";
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
ed25519Sign,
|
|
22
23
|
ed25519Verify,
|
|
23
24
|
hkdfBytes,
|
|
24
|
-
randomBytes,
|
|
25
25
|
secp256k1PublicKey,
|
|
26
26
|
secp256k1SignDigest,
|
|
27
27
|
secp256k1VerifyDigest,
|
|
@@ -1422,10 +1422,10 @@ export class NodeHost {
|
|
|
1422
1422
|
});
|
|
1423
1423
|
|
|
1424
1424
|
this.random = Object.freeze({
|
|
1425
|
-
bytes:
|
|
1426
|
-
this.#withCapability("random", "random.bytes",
|
|
1425
|
+
bytes: (length) =>
|
|
1426
|
+
this.#withCapability("random", "random.bytes", () => {
|
|
1427
1427
|
const size = assertNonNegativeInteger(length, "Random byte length");
|
|
1428
|
-
return
|
|
1428
|
+
return nodeRandomBytes(size);
|
|
1429
1429
|
}),
|
|
1430
1430
|
});
|
|
1431
1431
|
|
package/src/index.d.ts
CHANGED
|
@@ -638,6 +638,7 @@ export type {
|
|
|
638
638
|
} from "./compiler/emception.js";
|
|
639
639
|
|
|
640
640
|
export {
|
|
641
|
+
createIsolatedEmceptionSession,
|
|
641
642
|
createSharedEmceptionSession,
|
|
642
643
|
loadSharedEmception,
|
|
643
644
|
withSharedEmception,
|
|
@@ -924,7 +925,7 @@ export class NodeHost {
|
|
|
924
925
|
nowIso(): string;
|
|
925
926
|
};
|
|
926
927
|
random: {
|
|
927
|
-
bytes(length: number):
|
|
928
|
+
bytes(length: number): Uint8Array;
|
|
928
929
|
};
|
|
929
930
|
timers: {
|
|
930
931
|
delay(ms: number, options?: { signal?: unknown }): Promise<void>;
|
package/src/manifest/codec.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as flatbuffers from "flatbuffers";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
CapabilityKind,
|
|
5
|
+
DrainPolicy,
|
|
6
|
+
HostCapabilityT,
|
|
7
|
+
PluginFamily,
|
|
4
8
|
PluginManifest,
|
|
5
9
|
PluginManifestT,
|
|
6
10
|
} from "../generated/orbpro/manifest.js";
|
|
@@ -21,6 +25,88 @@ function toByteBuffer(data) {
|
|
|
21
25
|
);
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function normalizeEnumName(name, { separator = "_", lowercase = true } = {}) {
|
|
29
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const normalized = name.trim();
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const joined = normalized.replace(/_/g, separator);
|
|
37
|
+
return lowercase ? joined.toLowerCase() : joined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizePluginFamilyName(value) {
|
|
41
|
+
if (typeof value === "number" && typeof PluginFamily[value] === "string") {
|
|
42
|
+
return normalizeEnumName(PluginFamily[value], { separator: "_" });
|
|
43
|
+
}
|
|
44
|
+
return normalizeEnumName(value, { separator: "_" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeDrainPolicyName(value) {
|
|
48
|
+
if (typeof value === "number" && typeof DrainPolicy[value] === "string") {
|
|
49
|
+
return normalizeEnumName(DrainPolicy[value], { separator: "-" });
|
|
50
|
+
}
|
|
51
|
+
return normalizeEnumName(value, { separator: "-" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeCapabilityName(value) {
|
|
55
|
+
if (
|
|
56
|
+
typeof value === "number" &&
|
|
57
|
+
typeof CapabilityKind[value] === "string"
|
|
58
|
+
) {
|
|
59
|
+
return normalizeEnumName(CapabilityKind[value], { separator: "_" });
|
|
60
|
+
}
|
|
61
|
+
return normalizeEnumName(value, { separator: "_" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeDecodedCapabilities(value) {
|
|
65
|
+
if (!Array.isArray(value)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return value
|
|
69
|
+
.map((entry) => {
|
|
70
|
+
if (typeof entry === "string") {
|
|
71
|
+
return normalizeCapabilityName(entry);
|
|
72
|
+
}
|
|
73
|
+
if (!(entry instanceof HostCapabilityT) && (!entry || typeof entry !== "object")) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const capability = normalizeCapabilityName(entry.capability);
|
|
77
|
+
if (!capability) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const scope =
|
|
81
|
+
typeof entry.scope === "string" && entry.scope.trim().length > 0
|
|
82
|
+
? entry.scope.trim()
|
|
83
|
+
: null;
|
|
84
|
+
const description =
|
|
85
|
+
typeof entry.description === "string" &&
|
|
86
|
+
entry.description.trim().length > 0
|
|
87
|
+
? entry.description.trim()
|
|
88
|
+
: null;
|
|
89
|
+
const required = entry.required !== false;
|
|
90
|
+
if (!scope && !description && required) {
|
|
91
|
+
return capability;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
capability,
|
|
95
|
+
...(scope ? { scope } : {}),
|
|
96
|
+
...(required === false ? { required: false } : {}),
|
|
97
|
+
...(description ? { description } : {}),
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeDecodedMethod(method = {}) {
|
|
104
|
+
return {
|
|
105
|
+
...method,
|
|
106
|
+
drainPolicy: normalizeDrainPolicyName(method.drainPolicy),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
24
110
|
export function decodePluginManifest(data) {
|
|
25
111
|
const bb = toByteBuffer(data);
|
|
26
112
|
if (!PluginManifest.bufferHasIdentifier(bb)) {
|
|
@@ -29,6 +115,11 @@ export function decodePluginManifest(data) {
|
|
|
29
115
|
const unpacked = PluginManifest.getRootAsPluginManifest(bb).unpack();
|
|
30
116
|
return {
|
|
31
117
|
...unpacked,
|
|
118
|
+
pluginFamily: normalizePluginFamilyName(unpacked.pluginFamily),
|
|
119
|
+
capabilities: normalizeDecodedCapabilities(unpacked.capabilities),
|
|
120
|
+
methods: Array.isArray(unpacked.methods)
|
|
121
|
+
? unpacked.methods.map((method) => normalizeDecodedMethod(method))
|
|
122
|
+
: [],
|
|
32
123
|
invokeSurfaces: Array.isArray(unpacked.invokeSurfaces)
|
|
33
124
|
? unpacked.invokeSurfaces
|
|
34
125
|
.map((value) => normalizeInvokeSurfaceName(value))
|
package/src/testing/index.js
CHANGED
|
@@ -32,11 +32,11 @@ const CapabilitySurfaceMatrix = Object.freeze({
|
|
|
32
32
|
wasi: true,
|
|
33
33
|
standaloneWasi: true,
|
|
34
34
|
wasmedge: true,
|
|
35
|
-
syncHostcall:
|
|
35
|
+
syncHostcall: true,
|
|
36
36
|
nodeHostApi: true,
|
|
37
37
|
notes: [
|
|
38
38
|
"WASI random_get is available to standalone guests.",
|
|
39
|
-
"The
|
|
39
|
+
"The sync hostcall ABI exposes random.bytes through a canonical base64 byte envelope.",
|
|
40
40
|
],
|
|
41
41
|
}),
|
|
42
42
|
timers: Object.freeze({
|
package/src/transport/records.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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 =
|
|
604
|
+
const collection = collectionTable.unpack();
|
|
319
605
|
const records = [];
|
|
320
606
|
let enc = null;
|
|
321
607
|
let pnm = null;
|
|
322
|
-
for (
|
|
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
|
|
325
|
-
STANDARD_BY_RECORD_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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|