space-data-module-sdk 0.2.5 → 0.2.7

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.
@@ -16,8 +16,6 @@ const FILE_URL_FETCH_PATCH_FLAG =
16
16
  "__spaceDataModuleSdkFileUrlFetchPatched";
17
17
 
18
18
  let patchedEmceptionRootPromise = null;
19
- let emceptionInstancePromise = null;
20
- let emceptionExecutionQueue = Promise.resolve();
21
19
 
22
20
  function installNodeRuntimeShims() {
23
21
  if (typeof globalThis.require !== "function") {
@@ -175,43 +173,62 @@ async function preparePatchedEmceptionRoot() {
175
173
  return patchedEmceptionRootPromise;
176
174
  }
177
175
 
178
- export async function loadEmception() {
179
- if (!emceptionInstancePromise) {
180
- emceptionInstancePromise = (async () => {
181
- installNodeRuntimeShims();
182
- const patchedRoot = await preparePatchedEmceptionRoot();
183
- const moduleUrl = pathToFileURL(path.join(patchedRoot, "emception.mjs")).href;
184
- const { default: Emception } = await import(moduleUrl);
185
- const emception = new Emception({
186
- baseUrl: pathToFileURL(`${patchedRoot}${path.sep}`).href,
176
+ class EmceptionController {
177
+ #instancePromise = null;
178
+ #executionQueue = Promise.resolve();
179
+
180
+ async load() {
181
+ if (!this.#instancePromise) {
182
+ this.#instancePromise = (async () => {
183
+ installNodeRuntimeShims();
184
+ const patchedRoot = await preparePatchedEmceptionRoot();
185
+ const moduleUrl = pathToFileURL(path.join(patchedRoot, "emception.mjs")).href;
186
+ const { default: Emception } = await import(moduleUrl);
187
+ const emception = new Emception({
188
+ baseUrl: pathToFileURL(`${patchedRoot}${path.sep}`).href,
189
+ });
190
+ await emception.init();
191
+ return emception;
192
+ })().catch((error) => {
193
+ this.#instancePromise = null;
194
+ throw error;
187
195
  });
188
- await emception.init();
189
- return emception;
190
- })().catch((error) => {
191
- emceptionInstancePromise = null;
192
- throw error;
196
+ }
197
+
198
+ return this.#instancePromise;
199
+ }
200
+
201
+ async withLock(task) {
202
+ const previous = this.#executionQueue;
203
+ let release = () => {};
204
+ this.#executionQueue = new Promise((resolve) => {
205
+ release = resolve;
193
206
  });
207
+ await previous.catch(() => {});
208
+ try {
209
+ const emception = await this.load().catch((error) => {
210
+ if (!error.code) {
211
+ error.code = "EMCEPTION_LOAD_FAILED";
212
+ }
213
+ throw error;
214
+ });
215
+ return await task(emception);
216
+ } finally {
217
+ release();
218
+ }
194
219
  }
220
+ }
221
+
222
+ const sharedEmceptionController = new EmceptionController();
195
223
 
196
- return emceptionInstancePromise;
224
+ export function getSharedEmceptionController() {
225
+ return sharedEmceptionController;
226
+ }
227
+
228
+ export async function loadEmception() {
229
+ return sharedEmceptionController.load();
197
230
  }
198
231
 
199
232
  export async function runWithEmceptionLock(task) {
200
- const previous = emceptionExecutionQueue;
201
- let release = () => {};
202
- emceptionExecutionQueue = new Promise((resolve) => {
203
- release = resolve;
204
- });
205
- await previous.catch(() => {});
206
- try {
207
- const emception = await loadEmception().catch((error) => {
208
- if (!error.code) {
209
- error.code = "EMCEPTION_LOAD_FAILED";
210
- }
211
- throw error;
212
- });
213
- return await task(emception);
214
- } finally {
215
- release();
216
- }
233
+ return sharedEmceptionController.withLock(task);
217
234
  }
@@ -0,0 +1,24 @@
1
+ export type {
2
+ CompilationResult,
3
+ ProtectedArtifact,
4
+ } from "../index.js";
5
+
6
+ export {
7
+ cleanupCompilation,
8
+ compileModuleFromSource,
9
+ createRecipientKeypairHex,
10
+ protectModuleArtifact,
11
+ } from "../index.js";
12
+
13
+ export type {
14
+ EmceptionCommandResult,
15
+ SharedEmceptionFileContent,
16
+ SharedEmceptionHandle,
17
+ SharedEmceptionSession,
18
+ } from "./emception.js";
19
+
20
+ export {
21
+ createSharedEmceptionSession,
22
+ loadSharedEmception,
23
+ withSharedEmception,
24
+ } from "./emception.js";
@@ -5,3 +5,8 @@ export {
5
5
  protectModuleArtifact,
6
6
  } from "./compileModule.js";
7
7
 
8
+ export {
9
+ createSharedEmceptionSession,
10
+ loadSharedEmception,
11
+ withSharedEmception,
12
+ } from "./emception.js";
@@ -8,6 +8,8 @@ import {
8
8
  ExternalInterfaceDirection,
9
9
  ExternalInterfaceKind,
10
10
  InvokeSurface,
11
+ ProtocolRole,
12
+ ProtocolTransportKind,
11
13
  RuntimeTarget,
12
14
  } from "../runtime/constants.js";
13
15
 
@@ -59,6 +61,8 @@ const ExternalInterfaceDirectionSet = new Set(
59
61
  Object.values(ExternalInterfaceDirection),
60
62
  );
61
63
  const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
64
+ const ProtocolRoleSet = new Set(Object.values(ProtocolRole));
65
+ const ProtocolTransportKindSet = new Set(Object.values(ProtocolTransportKind));
62
66
  const BrowserIncompatibleCapabilitySet = new Set([
63
67
  "filesystem",
64
68
  "pipe",
@@ -120,6 +124,57 @@ function hasNonEmptyByteSequence(value) {
120
124
  return false;
121
125
  }
122
126
 
127
+ function normalizeTypeIdentityString(value) {
128
+ if (!isNonEmptyString(value)) {
129
+ return null;
130
+ }
131
+ return value.trim().toLowerCase();
132
+ }
133
+
134
+ function normalizeTypeIdentityBytes(value) {
135
+ if (typeof value === "string") {
136
+ const trimmed = value.trim().toLowerCase();
137
+ return trimmed.length > 0 ? trimmed : null;
138
+ }
139
+ const bytes = ArrayBuffer.isView(value)
140
+ ? Array.from(value)
141
+ : Array.isArray(value)
142
+ ? value
143
+ : null;
144
+ if (!bytes || bytes.length === 0) {
145
+ return null;
146
+ }
147
+ return bytes
148
+ .map((entry) => Number(entry).toString(16).padStart(2, "0"))
149
+ .join("");
150
+ }
151
+
152
+ function allowedTypesReferToSameLogicalSchema(left, right) {
153
+ const leftSchemaName = normalizeTypeIdentityString(left?.schemaName);
154
+ const rightSchemaName = normalizeTypeIdentityString(right?.schemaName);
155
+ if (leftSchemaName && rightSchemaName && leftSchemaName === rightSchemaName) {
156
+ return true;
157
+ }
158
+
159
+ const leftFileIdentifier = normalizeTypeIdentityString(left?.fileIdentifier);
160
+ const rightFileIdentifier = normalizeTypeIdentityString(right?.fileIdentifier);
161
+ if (
162
+ leftFileIdentifier &&
163
+ rightFileIdentifier &&
164
+ leftFileIdentifier === rightFileIdentifier
165
+ ) {
166
+ return true;
167
+ }
168
+
169
+ const leftSchemaHash = normalizeTypeIdentityBytes(left?.schemaHash);
170
+ const rightSchemaHash = normalizeTypeIdentityBytes(right?.schemaHash);
171
+ if (leftSchemaHash && rightSchemaHash && leftSchemaHash === rightSchemaHash) {
172
+ return true;
173
+ }
174
+
175
+ return false;
176
+ }
177
+
123
178
  function normalizePayloadWireFormatName(value) {
124
179
  if (value === undefined || value === null || value === "") {
125
180
  return "flatbuffer";
@@ -151,7 +206,13 @@ function validateStringField(issues, value, location, label) {
151
206
  return true;
152
207
  }
153
208
 
154
- function validateIntegerField(issues, value, location, label, { min = null } = {}) {
209
+ function validateIntegerField(
210
+ issues,
211
+ value,
212
+ location,
213
+ label,
214
+ { min = null, max = null } = {},
215
+ ) {
155
216
  if (!Number.isInteger(value)) {
156
217
  pushIssue(issues, "error", "invalid-integer", `${label} must be an integer.`, location);
157
218
  return false;
@@ -166,6 +227,16 @@ function validateIntegerField(issues, value, location, label, { min = null } = {
166
227
  );
167
228
  return false;
168
229
  }
230
+ if (max !== null && value > max) {
231
+ pushIssue(
232
+ issues,
233
+ "error",
234
+ "integer-range",
235
+ `${label} must be less than or equal to ${max}.`,
236
+ location,
237
+ );
238
+ return false;
239
+ }
169
240
  return true;
170
241
  }
171
242
 
@@ -182,6 +253,61 @@ function validateOptionalIntegerField(
182
253
  return validateIntegerField(issues, value, location, label, options);
183
254
  }
184
255
 
256
+ function validateOptionalBooleanField(issues, value, location, label) {
257
+ if (value === undefined || value === null) {
258
+ return true;
259
+ }
260
+ if (typeof value !== "boolean") {
261
+ pushIssue(
262
+ issues,
263
+ "error",
264
+ "invalid-boolean",
265
+ `${label} must be a boolean when present.`,
266
+ location,
267
+ );
268
+ return false;
269
+ }
270
+ return true;
271
+ }
272
+
273
+ function validateOptionalStringField(issues, value, location, label) {
274
+ if (value === undefined || value === null) {
275
+ return true;
276
+ }
277
+ return validateStringField(issues, value, location, label);
278
+ }
279
+
280
+ function normalizeProtocolTransportKind(value) {
281
+ if (value === undefined || value === null || value === "") {
282
+ return null;
283
+ }
284
+ const normalized = String(value)
285
+ .trim()
286
+ .toLowerCase()
287
+ .replace(/_/g, "-");
288
+ if (normalized === "websocket") {
289
+ return ProtocolTransportKind.WS;
290
+ }
291
+ if (normalized === "pipe") {
292
+ return ProtocolTransportKind.WASI_PIPE;
293
+ }
294
+ return normalized;
295
+ }
296
+
297
+ function normalizeProtocolRole(value) {
298
+ if (value === undefined || value === null || value === "") {
299
+ return null;
300
+ }
301
+ const normalized = String(value)
302
+ .trim()
303
+ .toLowerCase()
304
+ .replace(/_/g, "-");
305
+ if (normalized === "handler") {
306
+ return ProtocolRole.HANDLE;
307
+ }
308
+ return normalized;
309
+ }
310
+
185
311
  function validateAllowedType(type, issues, location) {
186
312
  if (!type || typeof type !== "object" || Array.isArray(type)) {
187
313
  pushIssue(issues, "error", "invalid-type-record", "Allowed type entries must be objects.", location);
@@ -307,6 +433,35 @@ function validateAcceptedTypeSet(typeSet, issues, location) {
307
433
  typeSet.allowedTypes.forEach((allowedType, index) => {
308
434
  validateAllowedType(allowedType, issues, `${location}.allowedTypes[${index}]`);
309
435
  });
436
+
437
+ const regularConcreteTypes = [];
438
+ const alignedTypes = [];
439
+
440
+ typeSet.allowedTypes.forEach((allowedType, index) => {
441
+ const wireFormat = normalizePayloadWireFormatName(allowedType?.wireFormat);
442
+ if (wireFormat === "aligned-binary") {
443
+ alignedTypes.push({ allowedType, index });
444
+ return;
445
+ }
446
+ if (wireFormat === "flatbuffer" && allowedType?.acceptsAnyFlatbuffer !== true) {
447
+ regularConcreteTypes.push({ allowedType, index });
448
+ }
449
+ });
450
+
451
+ alignedTypes.forEach(({ allowedType, index }) => {
452
+ const hasRegularFallback = regularConcreteTypes.some(({ allowedType: regularType }) =>
453
+ allowedTypesReferToSameLogicalSchema(allowedType, regularType),
454
+ );
455
+ if (!hasRegularFallback) {
456
+ pushIssue(
457
+ issues,
458
+ "error",
459
+ "missing-flatbuffer-fallback",
460
+ "Aligned-binary allowed types must be paired with a regular flatbuffer fallback for the same schema in the same acceptedTypeSet.",
461
+ `${location}.allowedTypes[${index}]`,
462
+ );
463
+ }
464
+ });
310
465
  }
311
466
 
312
467
  function validatePort(port, issues, location, label) {
@@ -576,6 +731,85 @@ function validateProtocol(protocol, issues, location, methodLookup, declaredCapa
576
731
  );
577
732
  }
578
733
  }
734
+ const wireIdValid = validateStringField(
735
+ issues,
736
+ protocol.wireId,
737
+ `${location}.wireId`,
738
+ "Protocol wireId",
739
+ );
740
+ const normalizedTransportKind = normalizeProtocolTransportKind(
741
+ protocol.transportKind,
742
+ );
743
+ if (!validateStringField(
744
+ issues,
745
+ protocol.transportKind,
746
+ `${location}.transportKind`,
747
+ "Protocol transportKind",
748
+ )) {
749
+ // already reported
750
+ } else if (!ProtocolTransportKindSet.has(normalizedTransportKind)) {
751
+ pushIssue(
752
+ issues,
753
+ "error",
754
+ "unknown-protocol-transport-kind",
755
+ `Protocol "${protocol.protocolId ?? "protocol"}" transportKind must be one of: ${Array.from(ProtocolTransportKindSet).join(", ")}.`,
756
+ `${location}.transportKind`,
757
+ );
758
+ }
759
+ const normalizedRole = normalizeProtocolRole(protocol.role);
760
+ if (!validateStringField(
761
+ issues,
762
+ protocol.role,
763
+ `${location}.role`,
764
+ "Protocol role",
765
+ )) {
766
+ // already reported
767
+ } else if (!ProtocolRoleSet.has(normalizedRole)) {
768
+ pushIssue(
769
+ issues,
770
+ "error",
771
+ "unknown-protocol-role",
772
+ `Protocol "${protocol.protocolId ?? "protocol"}" role must be one of: ${Array.from(ProtocolRoleSet).join(", ")}.`,
773
+ `${location}.role`,
774
+ );
775
+ }
776
+ validateOptionalStringField(
777
+ issues,
778
+ protocol.specUri,
779
+ `${location}.specUri`,
780
+ "Protocol specUri",
781
+ );
782
+ validateOptionalStringField(
783
+ issues,
784
+ protocol.discoveryKey,
785
+ `${location}.discoveryKey`,
786
+ "Protocol discoveryKey",
787
+ );
788
+ validateOptionalBooleanField(
789
+ issues,
790
+ protocol.autoInstall,
791
+ `${location}.autoInstall`,
792
+ "Protocol autoInstall",
793
+ );
794
+ validateOptionalBooleanField(
795
+ issues,
796
+ protocol.advertise,
797
+ `${location}.advertise`,
798
+ "Protocol advertise",
799
+ );
800
+ validateOptionalBooleanField(
801
+ issues,
802
+ protocol.requireSecureTransport,
803
+ `${location}.requireSecureTransport`,
804
+ "Protocol requireSecureTransport",
805
+ );
806
+ validateOptionalIntegerField(
807
+ issues,
808
+ protocol.defaultPort,
809
+ `${location}.defaultPort`,
810
+ "Protocol defaultPort",
811
+ { min: 0, max: 65535 },
812
+ );
579
813
  if (
580
814
  protocolIdValid &&
581
815
  Array.isArray(declaredCapabilities) &&
@@ -590,6 +824,77 @@ function validateProtocol(protocol, issues, location, methodLookup, declaredCapa
590
824
  location,
591
825
  );
592
826
  }
827
+ if (
828
+ protocolIdValid &&
829
+ normalizedRole &&
830
+ (normalizedRole === ProtocolRole.HANDLE ||
831
+ normalizedRole === ProtocolRole.BOTH) &&
832
+ Array.isArray(declaredCapabilities) &&
833
+ !declaredCapabilities.includes("protocol_handle")
834
+ ) {
835
+ pushIssue(
836
+ issues,
837
+ "error",
838
+ "missing-handle-protocol-capability",
839
+ `Protocol "${protocol.protocolId}" with role "${normalizedRole}" requires the "protocol_handle" capability.`,
840
+ location,
841
+ );
842
+ }
843
+ if (
844
+ protocolIdValid &&
845
+ normalizedRole &&
846
+ (normalizedRole === ProtocolRole.DIAL ||
847
+ normalizedRole === ProtocolRole.BOTH) &&
848
+ Array.isArray(declaredCapabilities) &&
849
+ !declaredCapabilities.includes("protocol_dial")
850
+ ) {
851
+ pushIssue(
852
+ issues,
853
+ "error",
854
+ "missing-dial-protocol-capability",
855
+ `Protocol "${protocol.protocolId}" with role "${normalizedRole}" requires the "protocol_dial" capability.`,
856
+ location,
857
+ );
858
+ }
859
+ if (
860
+ protocol.advertise === true &&
861
+ normalizedRole === ProtocolRole.DIAL
862
+ ) {
863
+ pushIssue(
864
+ issues,
865
+ "error",
866
+ "protocol-advertise-role-conflict",
867
+ `Protocol "${protocol.protocolId ?? "protocol"}" cannot advertise when role is "dial".`,
868
+ `${location}.advertise`,
869
+ );
870
+ }
871
+ if (
872
+ normalizedTransportKind === ProtocolTransportKind.LIBP2P &&
873
+ Array.isArray(declaredCapabilities) &&
874
+ !declaredCapabilities.includes("ipfs")
875
+ ) {
876
+ pushIssue(
877
+ issues,
878
+ "error",
879
+ "missing-ipfs-capability",
880
+ `Protocol "${protocol.protocolId ?? "protocol"}" with transportKind "libp2p" requires the "ipfs" capability.`,
881
+ location,
882
+ );
883
+ }
884
+ if (
885
+ wireIdValid &&
886
+ normalizedTransportKind === ProtocolTransportKind.WS &&
887
+ protocol.defaultPort === 443 &&
888
+ protocol.requireSecureTransport !== true
889
+ ) {
890
+ pushIssue(
891
+ issues,
892
+ "warning",
893
+ "insecure-secure-port-hint",
894
+ `Protocol "${protocol.protocolId ?? "protocol"}" uses defaultPort 443 without requireSecureTransport=true.`,
895
+ `${location}.defaultPort`,
896
+ );
897
+ }
593
898
  }
594
899
 
595
900
  function validateRuntimeTargets(runtimeTargets, declaredCapabilities, issues, sourceName) {