space-data-module-sdk 0.2.6 → 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.
package/README.md CHANGED
@@ -34,6 +34,7 @@ A module built with this SDK is a `.wasm` artifact with:
34
34
  - an exported `_start` entry when the artifact supports WASI command mode
35
35
  - optional `sds.bundle` custom-section payloads for:
36
36
  - manifest bytes
37
+ - resolved deployment plans and input bindings
37
38
  - deployment authorization
38
39
  - detached signatures
39
40
  - encrypted transport envelopes
@@ -81,6 +82,14 @@ outputs. The reference invoke examples live in
81
82
  - `manifest.hybrid.json`
82
83
  - `module.c`
83
84
 
85
+ Input and output ports can independently declare regular `flatbuffer` payloads
86
+ or `aligned-binary` layouts. Mixed contracts are valid. When a port advertises
87
+ an `aligned-binary` layout, it must also advertise a regular `flatbuffer`
88
+ fallback for the same schema in the same accepted type set. A module can accept
89
+ a regular `OMM.fbs` request and emit an aligned-binary `StateVector.fbs`
90
+ response, provided the output port also declares the regular `StateVector.fbs`
91
+ fallback and the aligned type ref carries the correct layout metadata.
92
+
84
93
  ## Runtime Portability
85
94
 
86
95
  The module format is language-neutral. A host can load modules from this SDK
@@ -115,6 +124,25 @@ This repo currently includes:
115
124
  - a reference Node host and sync `sdn_host` bridge for the first hostcall
116
125
  surface
117
126
 
127
+ ## Testing
128
+
129
+ This repo now exposes a manifest-driven harness generator from
130
+ `space-data-module-sdk/testing` and two complementary integration suites:
131
+
132
+ - `npm run test:runtime-matrix`
133
+ - cross-language runtime smoke across the same WASM in Node.js, Go, Python,
134
+ Rust, Java, C#, and Swift
135
+ - covers method calling, aligned-binary envelope metadata preservation,
136
+ stdin/stdout/stderr, args, env, preopened filesystem access, and basic WASI
137
+ clock/time smoke
138
+ - `npm run test:host-surfaces`
139
+ - authoritative Node-host coverage for HTTP, TCP, UDP, TLS, WebSocket, MQTT,
140
+ process execution, timers, filesystem, and the sync `sdn_host` ABI
141
+
142
+ The detailed edge cases and the current WASI-vs-host portability boundary are
143
+ documented in
144
+ [`docs/testing-harness.md`](./docs/testing-harness.md).
145
+
118
146
  ## Install
119
147
 
120
148
  ```bash
@@ -161,8 +189,36 @@ import { createDeploymentAuthorization } from "space-data-module-sdk/auth";
161
189
  import { encryptJsonForRecipient } from "space-data-module-sdk/transport";
162
190
  import { compileModuleFromSource } from "space-data-module-sdk/compiler";
163
191
  import { createSingleFileBundle } from "space-data-module-sdk/bundle";
192
+ import { validateDeploymentPlan } from "space-data-module-sdk/deployment";
193
+ import { generateManifestHarnessPlan } from "space-data-module-sdk/testing";
164
194
  ```
165
195
 
196
+ ## Protocol Installation
197
+
198
+ Modules can declare hosted protocol contracts in `manifest.protocols`.
199
+
200
+ Those declarations are for stable artifact identity:
201
+
202
+ - `wireId`
203
+ - `transportKind`
204
+ - `role`
205
+ - `specUri`
206
+ - hosting hints like `defaultPort` and `requireSecureTransport`
207
+
208
+ Concrete multiaddrs, peer IDs, and producer routing do not belong in the
209
+ canonical manifest. They belong in deployment metadata attached to the final
210
+ package or bundle.
211
+
212
+ This repo exposes that deployment surface from
213
+ `space-data-module-sdk/deployment`. Use it to:
214
+
215
+ - validate resolved protocol installations
216
+ - describe input bindings from producers to module ports
217
+ - attach a deployment plan to `sds.bundle`
218
+
219
+ The full contract split is documented in
220
+ [`docs/protocol-installation.md`](./docs/protocol-installation.md).
221
+
166
222
  ## Single-File Bundles
167
223
 
168
224
  `sds.bundle` keeps module delivery to one file without changing WebAssembly
@@ -179,6 +235,9 @@ The reference path lives in
179
235
  - the `go` and `python` directories show non-JS readers against the same
180
236
  bundle contract
181
237
 
238
+ Standard bundle payloads now include the optional `deployment-plan` JSON entry
239
+ for resolved protocol installations and producer input bindings.
240
+
182
241
  ## Module Publication
183
242
 
184
243
  Packages that publish SDN modules now use the canonical `sdn-module`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",
@@ -20,8 +20,10 @@
20
20
  "./compiler": "./src/compiler/index.js",
21
21
  "./compiler/emception": "./src/compiler/emception.js",
22
22
  "./bundle": "./src/bundle/index.js",
23
+ "./deployment": "./src/deployment/index.js",
23
24
  "./invoke": "./src/invoke/index.js",
24
25
  "./runtime": "./src/runtime/index.js",
26
+ "./testing": "./src/testing/index.js",
25
27
  "./standards": "./src/standards/index.js",
26
28
  "./schemas/*": "./schemas/*"
27
29
  },
@@ -32,6 +34,8 @@
32
34
  ],
33
35
  "scripts": {
34
36
  "test": "node --test",
37
+ "test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js",
38
+ "test:runtime-matrix": "SPACE_DATA_MODULE_SDK_ENABLE_RUNTIME_MATRIX=1 node --test test/runtime-matrix.test.js",
35
39
  "start:lab": "node ./lab/server.js",
36
40
  "check:compliance": "node ./bin/space-data-module.js check --repo-root .",
37
41
  "generate:vectors": "node ./examples/single-file-bundle/generate-vectors.mjs",
@@ -134,6 +134,15 @@ table ProtocolSpec {
134
134
  input_port_id: string;
135
135
  output_port_id: string;
136
136
  description: string;
137
+ wire_id: string;
138
+ transport_kind: string;
139
+ role: string;
140
+ spec_uri: string;
141
+ auto_install: bool = true;
142
+ advertise: bool = false;
143
+ discovery_key: string;
144
+ default_port: uint16;
145
+ require_secure_transport: bool = false;
137
146
  }
138
147
 
139
148
  /// Build artifact emitted by the plugin toolchain.
@@ -5,4 +5,7 @@ export const DEFAULT_HASH_ALGORITHM = "sha256";
5
5
  export const DEFAULT_MANIFEST_EXPORT_SYMBOL = "plugin_get_manifest_flatbuffer";
6
6
  export const DEFAULT_MANIFEST_SIZE_SYMBOL =
7
7
  "plugin_get_manifest_flatbuffer_size";
8
-
8
+ export const SDS_DEPLOYMENT_SECTION_NAME = "sds.deployment";
9
+ export const SDS_DEPLOYMENT_ENTRY_ID = "deployment-plan";
10
+ export const SDS_DEPLOYMENT_MEDIA_TYPE =
11
+ "application/vnd.space-data.module.deployment+json";
@@ -3,11 +3,14 @@ import { decodePluginManifest, encodePluginManifest } from "../manifest/index.js
3
3
  import { toUint8Array } from "../runtime/bufferLike.js";
4
4
  import { sha256Bytes } from "../utils/crypto.js";
5
5
  import { bytesToHex } from "../utils/encoding.js";
6
+ import { createDeploymentPlanBundleEntry } from "../deployment/index.js";
6
7
  import {
7
8
  DEFAULT_MANIFEST_EXPORT_SYMBOL,
8
9
  DEFAULT_MANIFEST_SIZE_SYMBOL,
9
10
  SDS_BUNDLE_SECTION_NAME,
10
11
  SDS_CUSTOM_SECTION_PREFIX,
12
+ SDS_DEPLOYMENT_ENTRY_ID,
13
+ SDS_DEPLOYMENT_SECTION_NAME,
11
14
  } from "./constants.js";
12
15
  import {
13
16
  decodeModuleBundle,
@@ -268,6 +271,9 @@ function normalizeStandardEntries(options = {}) {
268
271
  description: "Transport envelope metadata.",
269
272
  });
270
273
  }
274
+ if (options.deploymentPlan !== undefined) {
275
+ entries.push(createDeploymentPlanBundleEntry(options.deploymentPlan));
276
+ }
271
277
  return entries.filter((entry) => entry.payload !== null);
272
278
  }
273
279
 
@@ -282,7 +288,12 @@ function normalizeAdditionalEntries(entries = []) {
282
288
  }
283
289
 
284
290
  async function withSha256(entry) {
285
- const payloadBytes = normalizeBytes(entry.payload, `entry "${entry.entryId}" payload`);
291
+ const payloadBytes =
292
+ toUint8Array(entry.payload) ??
293
+ ((entry.payloadEncoding === "json-utf8" ||
294
+ moduleBundleEncodingToName(entry.payloadEncoding) === "json-utf8")
295
+ ? canonicalBytes(entry.payload)
296
+ : normalizeBytes(entry.payload, `entry "${entry.entryId}" payload`));
286
297
  return {
287
298
  ...entry,
288
299
  payload: payloadBytes,
@@ -315,6 +326,17 @@ function buildParsedEntries(bundle) {
315
326
  parsedEntry.decodedManifest = null;
316
327
  }
317
328
  }
329
+ if (
330
+ parsedEntry.entryId === SDS_DEPLOYMENT_ENTRY_ID ||
331
+ parsedEntry.sectionName === SDS_DEPLOYMENT_SECTION_NAME
332
+ ) {
333
+ parsedEntry.decodedDeploymentPlan =
334
+ parsedEntry.payloadEncodingName === "json-utf8" &&
335
+ parsedEntry.decodedPayload &&
336
+ typeof parsedEntry.decodedPayload === "object"
337
+ ? parsedEntry.decodedPayload
338
+ : null;
339
+ }
318
340
  return parsedEntry;
319
341
  });
320
342
  }
@@ -432,16 +454,22 @@ export async function parseSingleFileBundle(bytes, options = {}) {
432
454
  manifest = null;
433
455
  }
434
456
  }
457
+ const deploymentEntry =
458
+ parsedEntries.find(
459
+ (entry) =>
460
+ entry.entryId === SDS_DEPLOYMENT_ENTRY_ID ||
461
+ entry.sectionName === SDS_DEPLOYMENT_SECTION_NAME,
462
+ ) ?? null;
435
463
  return {
436
464
  wasmBytes,
437
465
  bundleBytes,
438
466
  bundle,
439
467
  entries: parsedEntries,
440
468
  manifest,
469
+ deploymentPlan: deploymentEntry?.decodedDeploymentPlan ?? null,
441
470
  customSections,
442
471
  canonicalWasmBytes: canonical.canonicalWasmBytes,
443
472
  canonicalModuleHash: canonical.hashBytes,
444
473
  canonicalModuleHashHex: canonical.hashHex,
445
474
  };
446
475
  }
447
-
@@ -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) {