@serve.zone/interfaces 32.15.0 → 32.17.0

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.
@@ -19,6 +19,7 @@ import { normalizeStorageRequest } from '../platform/storage.js';
19
19
  import type { ISecretVersionReference } from './secret.js';
20
20
  import {
21
21
  canonicalizeStrictJson,
22
+ compareWireTokens,
22
23
  createCanonicalJsonSha256Hex,
23
24
  } from '../private/canonicaljson.js';
24
25
  import {
@@ -82,6 +83,37 @@ export interface IFleetCutoverMountTargetIdentityInput {
82
83
  target: string;
83
84
  }
84
85
 
86
+ export interface IFleetCutoverDockerMountInput {
87
+ /** Raw Docker mount type. Unsupported values produce an explicit unknown descriptor. */
88
+ type: string;
89
+ /** Exact source bytes from the Docker object being described; never substitute another field. */
90
+ source: string;
91
+ /** Exact container target bytes from the Docker object being described. */
92
+ target: string;
93
+ readOnly: boolean;
94
+ }
95
+
96
+ /**
97
+ * Value-free correlation between one local physical mount and its service-spec mount identity.
98
+ * The two identities stay distinct: a Docker volume's local Source is a host path while its
99
+ * service Source and local Name are the logical volume name.
100
+ */
101
+ export interface IFleetCutoverPhysicalMountCorrelation {
102
+ physicalMountIdentityDigest: TSha256Digest;
103
+ serviceMountIdentityDigest: TSha256Digest;
104
+ correlationIdentityDigest: TSha256Digest;
105
+ }
106
+
107
+ export interface IFleetCutoverPhysicalMountEvidence {
108
+ mount: IFleetCutoverMountDescriptor;
109
+ correlation: IFleetCutoverPhysicalMountCorrelation;
110
+ }
111
+
112
+ export interface IFleetCutoverDockerConstraintSetIdentity {
113
+ constraintCount: number;
114
+ constraintSetDigest: TSha256Digest;
115
+ }
116
+
85
117
  export type TFleetCutoverDockerTaskState =
86
118
  | 'new'
87
119
  | 'allocated'
@@ -148,6 +180,11 @@ export interface IFleetCutoverPhysicalContainer {
148
180
  imageId: TSha256Digest;
149
181
  serviceIdLabel: string | null;
150
182
  mounts: IFleetCutoverMountDescriptor[];
183
+ /**
184
+ * Optional only for compatibility with observations predating correlation evidence. When
185
+ * present it is a complete one-to-one mapping of mounts. Absence establishes no mount join.
186
+ */
187
+ mountCorrelations?: IFleetCutoverPhysicalMountCorrelation[];
151
188
  }
152
189
 
153
190
  export interface IFleetCutoverContainerSetIdentity {
@@ -636,6 +673,9 @@ export const fleetCutoverContract = Object.freeze({
636
673
  maximumNodes: 1024,
637
674
  maximumContainersPerNode: 512,
638
675
  maximumMountsPerOwner: 32,
676
+ maximumConstraintsPerService: 256,
677
+ maximumDockerTextBytes: 4096,
678
+ maximumDockerServiceSpecCanonicalBytes: 1_048_576,
639
679
  maximumPreparedVolumeHandoffMutationEntries: 32,
640
680
  /** Every admitted task has one node placement and at most 32 mounts: 4096 * 32. */
641
681
  maximumPreparedVolumeHandoffs: 4096 * 32,
@@ -646,6 +686,9 @@ export const fleetCutoverContract = Object.freeze({
646
686
  mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
647
687
  mountTargetIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-target-identity',
648
688
  mountIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-identity',
689
+ mountCorrelationIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-correlation-identity',
690
+ dockerServiceSpecDigestDomain: 'serve.zone/fleet-cutover-docker-service-spec',
691
+ dockerConstraintSetDigestDomain: 'serve.zone/fleet-cutover-docker-constraint-set',
649
692
  containerSetIdentityDigestDomain: 'serve.zone/fleet-cutover-container-set-identity',
650
693
  localNodeSnapshotDigestDomain: 'serve.zone/fleet-cutover-local-node-snapshot',
651
694
  managerSnapshotDigestDomain: 'serve.zone/fleet-cutover-manager-snapshot',
@@ -673,6 +716,11 @@ export const fleetCutoverContract = Object.freeze({
673
716
  } satisfies Record<TClusterRuntimePhase, TClusterRuntimePhase | null>),
674
717
  } as const);
675
718
 
719
+ /** Docker labels with established historical producer semantics. */
720
+ export const fleetCutoverDockerLabelNames = Object.freeze({
721
+ serviceId: 'serve.zone.serviceId',
722
+ } as const);
723
+
676
724
  const fail = (): never => { throw new Error('Fleet cutover contract rejected.'); };
677
725
  const safe = <T>(operationArg: () => T): T => {
678
726
  try { return operationArg(); } catch { return fail(); }
@@ -764,6 +812,28 @@ const sortedMountDescriptors = (valueArg: unknown): IFleetCutoverMountDescriptor
764
812
  networkList(valueArg, fleetCutoverContract.maximumMountsPerOwner, mountDescriptor),
765
813
  itemArg => itemArg.mountIdentityDigest,
766
814
  );
815
+ const physicalMountCorrelation = (
816
+ valueArg: unknown,
817
+ ): IFleetCutoverPhysicalMountCorrelation => {
818
+ const value = networkRecord(valueArg, [
819
+ 'physicalMountIdentityDigest', 'serviceMountIdentityDigest', 'correlationIdentityDigest',
820
+ ]);
821
+ return {
822
+ physicalMountIdentityDigest: networkDigest(value.physicalMountIdentityDigest),
823
+ serviceMountIdentityDigest: networkDigest(value.serviceMountIdentityDigest),
824
+ correlationIdentityDigest: networkDigest(value.correlationIdentityDigest),
825
+ };
826
+ };
827
+ const sortedPhysicalMountCorrelations = (
828
+ valueArg: unknown,
829
+ ): IFleetCutoverPhysicalMountCorrelation[] => networkSorted(
830
+ networkList(
831
+ valueArg,
832
+ fleetCutoverContract.maximumMountsPerOwner,
833
+ physicalMountCorrelation,
834
+ ),
835
+ itemArg => itemArg.physicalMountIdentityDigest,
836
+ );
767
837
  const dockerMode = (valueArg: unknown): TFleetCutoverDockerServiceMode => {
768
838
  requireNetwork(valueArg !== null && typeof valueArg === 'object');
769
839
  const kind = (valueArg as Record<string, unknown>).kind;
@@ -788,6 +858,8 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
788
858
  'dockerServiceId', 'dockerServiceName', 'specVersion', 'specDigest', 'imageDigest', 'mode',
789
859
  'desiredTaskCount', 'constraintCount', 'constraintSetDigest', 'mounts', 'ownershipLabels',
790
860
  ]);
861
+ const constraintCount = nonNegative(value.constraintCount);
862
+ requireNetwork(constraintCount <= fleetCutoverContract.maximumConstraintsPerService);
791
863
  return {
792
864
  dockerServiceId: networkId(value.dockerServiceId),
793
865
  dockerServiceName: networkId(value.dockerServiceName),
@@ -796,7 +868,7 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
796
868
  imageDigest: networkDigest(value.imageDigest),
797
869
  mode: dockerMode(value.mode),
798
870
  desiredTaskCount: nonNegative(value.desiredTaskCount),
799
- constraintCount: nonNegative(value.constraintCount),
871
+ constraintCount,
800
872
  constraintSetDigest: networkDigest(value.constraintSetDigest),
801
873
  mounts: sortedMountDescriptors(value.mounts),
802
874
  ownershipLabels: ownershipLabels(value.ownershipLabels),
@@ -833,8 +905,8 @@ const dockerTask = (valueArg: unknown): IFleetCutoverDockerTask => {
833
905
  const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer => {
834
906
  const value = networkRecord(valueArg, [
835
907
  'dockerContainerId', 'dockerServiceId', 'dockerTaskId', 'imageId', 'serviceIdLabel', 'mounts',
836
- ]);
837
- return {
908
+ ], ['mountCorrelations']);
909
+ const result: IFleetCutoverPhysicalContainer = {
838
910
  dockerContainerId: networkId(value.dockerContainerId),
839
911
  dockerServiceId: value.dockerServiceId === null ? null : networkId(value.dockerServiceId),
840
912
  dockerTaskId: value.dockerTaskId === null ? null : networkId(value.dockerTaskId),
@@ -842,6 +914,10 @@ const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer =>
842
914
  serviceIdLabel: value.serviceIdLabel === null ? null : networkId(value.serviceIdLabel),
843
915
  mounts: sortedMountDescriptors(value.mounts),
844
916
  };
917
+ if (Object.hasOwn(value, 'mountCorrelations')) {
918
+ result.mountCorrelations = sortedPhysicalMountCorrelations(value.mountCorrelations);
919
+ }
920
+ return result;
845
921
  };
846
922
  const inventory = (valueArg: unknown): IFleetCutoverDockerInventory => {
847
923
  const value = networkRecord(valueArg, [
@@ -925,11 +1001,17 @@ const canonicalDigest = async (domainArg: string, valueArg: object): Promise<TSh
925
1001
 
926
1002
  const exactDockerText = (valueArg: unknown, allowEmptyArg: boolean): string => {
927
1003
  requireNetwork(typeof valueArg === 'string'
928
- && new TextEncoder().encode(valueArg).length <= 4096
1004
+ && new TextEncoder().encode(valueArg).length <= fleetCutoverContract.maximumDockerTextBytes
929
1005
  && (allowEmptyArg || valueArg.length > 0));
930
1006
  return valueArg;
931
1007
  };
932
1008
 
1009
+ const openDockerRecord = (valueArg: unknown): Record<string, unknown> => {
1010
+ const value = captureNetwork(valueArg);
1011
+ requireNetwork(value !== null && typeof value === 'object' && !Array.isArray(value));
1012
+ return value as Record<string, unknown>;
1013
+ };
1014
+
933
1015
  const computeExactIdentityDigest = async (
934
1016
  domainArg: string,
935
1017
  inputArg: object,
@@ -938,6 +1020,160 @@ const computeExactIdentityDigest = async (
938
1020
  input: inputArg,
939
1021
  }, fail), fail)}`;
940
1022
 
1023
+ /**
1024
+ * Hash the complete open-schema Docker Service.Spec object. Unknown response properties are
1025
+ * intentionally included: this helper returns only the digest and never a retained projection.
1026
+ */
1027
+ export const computeFleetCutoverDockerServiceSpecDigest = async (
1028
+ valueArg: unknown,
1029
+ ): Promise<TSha256Digest> => {
1030
+ try {
1031
+ const spec = openDockerRecord(valueArg);
1032
+ const canonicalSpec = canonicalizeStrictJson(spec, fail, 'Docker Service.Spec');
1033
+ requireNetwork(new TextEncoder().encode(canonicalSpec).byteLength
1034
+ <= fleetCutoverContract.maximumDockerServiceSpecCanonicalBytes);
1035
+ return await computeExactIdentityDigest(
1036
+ fleetCutoverContract.dockerServiceSpecDigestDomain,
1037
+ { spec },
1038
+ );
1039
+ } catch {
1040
+ return fail();
1041
+ }
1042
+ };
1043
+
1044
+ /** Sort exact constraint strings while preserving duplicate entries, then return value-free identity. */
1045
+ export const computeFleetCutoverDockerConstraintSetIdentity = async (
1046
+ valueArg: unknown,
1047
+ ): Promise<IFleetCutoverDockerConstraintSetIdentity> => {
1048
+ try {
1049
+ const captured = captureNetwork(valueArg);
1050
+ requireNetwork(Array.isArray(captured)
1051
+ && captured.length <= fleetCutoverContract.maximumConstraintsPerService);
1052
+ const constraints = captured.map(itemArg => exactDockerText(itemArg, false))
1053
+ .sort(compareWireTokens);
1054
+ return {
1055
+ constraintCount: constraints.length,
1056
+ constraintSetDigest: await computeExactIdentityDigest(
1057
+ fleetCutoverContract.dockerConstraintSetDigestDomain,
1058
+ { constraints },
1059
+ ),
1060
+ };
1061
+ } catch {
1062
+ return fail();
1063
+ }
1064
+ };
1065
+
1066
+ export const snapshotFleetCutoverDockerDesiredTaskCount = (valueArg: unknown): number =>
1067
+ safe(() => nonNegative(valueArg));
1068
+
1069
+ export const validateFleetCutoverDockerDesiredTaskCount = (valueArg: unknown): string[] => (
1070
+ typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
1071
+ ? []
1072
+ : ['Docker ServiceStatus.DesiredTasks must be a non-negative safe integer']
1073
+ );
1074
+
1075
+ /** Apply only the documented exact cutover mount classifications; no path normalization occurs. */
1076
+ export const classifyFleetCutoverDockerMount = (
1077
+ valueArg: IFleetCutoverDockerMountInput,
1078
+ ): Pick<IFleetCutoverMountDescriptor, 'type' | 'classification'> => safe(() => {
1079
+ const value = networkRecord(valueArg, ['type', 'source', 'target', 'readOnly']);
1080
+ const rawType = exactDockerText(value.type, false);
1081
+ const source = exactDockerText(value.source, true);
1082
+ const target = exactDockerText(value.target, false);
1083
+ requireNetwork(typeof value.readOnly === 'boolean');
1084
+ if (rawType === 'volume') return { type: 'volume', classification: 'persistent-volume' };
1085
+ if (rawType === 'tmpfs') return { type: 'tmpfs', classification: 'tmpfs' };
1086
+ if (rawType !== 'bind') return { type: 'unknown', classification: 'unknown' };
1087
+ if (source === '/var/run/docker.sock'
1088
+ && target === '/var/run/docker.sock'
1089
+ && value.readOnly === false) {
1090
+ return { type: 'bind', classification: 'docker-socket' };
1091
+ }
1092
+ if (source === '/sys/fs/cgroup'
1093
+ && target === '/host/cgroup'
1094
+ && value.readOnly === true) {
1095
+ return { type: 'bind', classification: 'cgroup' };
1096
+ }
1097
+ return { type: 'bind', classification: 'bind' };
1098
+ });
1099
+
1100
+ export const createFleetCutoverMountDescriptor = async (
1101
+ valueArg: IFleetCutoverDockerMountInput,
1102
+ ): Promise<IFleetCutoverMountDescriptor> => {
1103
+ try {
1104
+ const value = networkRecord(valueArg, ['type', 'source', 'target', 'readOnly']);
1105
+ const source = exactDockerText(value.source, true);
1106
+ const target = exactDockerText(value.target, false);
1107
+ requireNetwork(typeof value.readOnly === 'boolean');
1108
+ const shape = classifyFleetCutoverDockerMount({
1109
+ type: exactDockerText(value.type, false),
1110
+ source,
1111
+ target,
1112
+ readOnly: value.readOnly,
1113
+ });
1114
+ requireNetwork(shape.type === 'tmpfs' ? source.length === 0 : source.length > 0);
1115
+ const identityInput: Omit<IFleetCutoverMountDescriptor, 'mountIdentityDigest'> = {
1116
+ ...shape,
1117
+ sourceIdentityDigest: await computeFleetCutoverMountSourceIdentityDigest({
1118
+ type: shape.type,
1119
+ source,
1120
+ }),
1121
+ targetIdentityDigest: await computeFleetCutoverMountTargetIdentityDigest({ target }),
1122
+ readOnly: value.readOnly,
1123
+ };
1124
+ return {
1125
+ ...identityInput,
1126
+ mountIdentityDigest: await computeFleetCutoverMountIdentityDigest(identityInput),
1127
+ };
1128
+ } catch {
1129
+ return fail();
1130
+ }
1131
+ };
1132
+
1133
+ const dockerServiceMountInput = (valueArg: unknown): IFleetCutoverDockerMountInput => {
1134
+ const value = openDockerRecord(valueArg);
1135
+ const type = exactDockerText(value.Type, false);
1136
+ const source = value.Source === undefined ? '' : exactDockerText(value.Source, true);
1137
+ const target = exactDockerText(value.Target, false);
1138
+ const readOnly = value.ReadOnly === undefined ? false : value.ReadOnly;
1139
+ requireNetwork(typeof readOnly === 'boolean');
1140
+ return { type, source, target, readOnly };
1141
+ };
1142
+
1143
+ interface IFleetCutoverDockerLocalMountInput {
1144
+ descriptorInput: IFleetCutoverDockerMountInput;
1145
+ logicalSource: string;
1146
+ }
1147
+
1148
+ const dockerLocalMountInput = (valueArg: unknown): IFleetCutoverDockerLocalMountInput => {
1149
+ const value = openDockerRecord(valueArg);
1150
+ const type = exactDockerText(value.Type, false);
1151
+ const source = exactDockerText(value.Source, true);
1152
+ const target = exactDockerText(value.Destination, false);
1153
+ requireNetwork(typeof value.RW === 'boolean');
1154
+ const logicalSource = type === 'volume'
1155
+ ? exactDockerText(value.Name, false)
1156
+ : source;
1157
+ return {
1158
+ descriptorInput: { type, source, target, readOnly: !value.RW },
1159
+ logicalSource,
1160
+ };
1161
+ };
1162
+
1163
+ /** Read only consumed Service.Spec Mount fields; future Docker response properties are ignored. */
1164
+ export const createFleetCutoverDockerServiceMountDescriptor = async (
1165
+ valueArg: unknown,
1166
+ ): Promise<IFleetCutoverMountDescriptor> => createFleetCutoverMountDescriptor(
1167
+ dockerServiceMountInput(valueArg),
1168
+ );
1169
+
1170
+ /** Read only consumed local MountPoint fields and retain the exact physical Source identity. */
1171
+ export const createFleetCutoverDockerLocalMountDescriptor = async (
1172
+ valueArg: unknown,
1173
+ ): Promise<IFleetCutoverMountDescriptor> => createFleetCutoverMountDescriptor(
1174
+ dockerLocalMountInput(valueArg).descriptorInput,
1175
+ );
1176
+
941
1177
  export const computeFleetCutoverMountSourceIdentityDigest = async (
942
1178
  valueArg: IFleetCutoverMountSourceIdentityInput,
943
1179
  ): Promise<TSha256Digest> => safe(async () => {
@@ -973,6 +1209,54 @@ export const computeFleetCutoverMountIdentityDigest = async (
973
1209
  });
974
1210
  });
975
1211
 
1212
+ export const computeFleetCutoverMountCorrelationIdentityDigest = async (
1213
+ valueArg: Omit<IFleetCutoverPhysicalMountCorrelation, 'correlationIdentityDigest'>,
1214
+ ): Promise<TSha256Digest> => safe(async () => {
1215
+ const value = networkRecord(valueArg, [
1216
+ 'physicalMountIdentityDigest', 'serviceMountIdentityDigest',
1217
+ ]);
1218
+ return computeExactIdentityDigest(fleetCutoverContract.mountCorrelationIdentityDigestDomain, {
1219
+ physicalMountIdentityDigest: networkDigest(value.physicalMountIdentityDigest),
1220
+ serviceMountIdentityDigest: networkDigest(value.serviceMountIdentityDigest),
1221
+ });
1222
+ });
1223
+
1224
+ /**
1225
+ * Produce both the exact local physical descriptor and its logical service-mount correlation.
1226
+ * For a volume only MountPoint.Name supplies the logical source. Unknown types and a missing or
1227
+ * empty volume Name are refused because they cannot establish a truthful service join.
1228
+ */
1229
+ export const createFleetCutoverDockerPhysicalMountEvidence = async (
1230
+ valueArg: unknown,
1231
+ ): Promise<IFleetCutoverPhysicalMountEvidence> => {
1232
+ try {
1233
+ const value = dockerLocalMountInput(valueArg);
1234
+ requireNetwork(['volume', 'bind', 'tmpfs'].includes(value.descriptorInput.type));
1235
+ const mount = await createFleetCutoverMountDescriptor(value.descriptorInput);
1236
+ const serviceMount = await createFleetCutoverMountDescriptor({
1237
+ ...value.descriptorInput,
1238
+ source: value.logicalSource,
1239
+ });
1240
+ const correlationInput: Omit<
1241
+ IFleetCutoverPhysicalMountCorrelation,
1242
+ 'correlationIdentityDigest'
1243
+ > = {
1244
+ physicalMountIdentityDigest: mount.mountIdentityDigest,
1245
+ serviceMountIdentityDigest: serviceMount.mountIdentityDigest,
1246
+ };
1247
+ return {
1248
+ mount,
1249
+ correlation: {
1250
+ ...correlationInput,
1251
+ correlationIdentityDigest:
1252
+ await computeFleetCutoverMountCorrelationIdentityDigest(correlationInput),
1253
+ },
1254
+ };
1255
+ } catch {
1256
+ return fail();
1257
+ }
1258
+ };
1259
+
976
1260
  const validateMountDescriptors = async (
977
1261
  mountsArg: IFleetCutoverMountDescriptor[],
978
1262
  ): Promise<void> => {
@@ -982,6 +1266,28 @@ const validateMountDescriptors = async (
982
1266
  }
983
1267
  };
984
1268
 
1269
+ const validatePhysicalMountCorrelations = async (
1270
+ containerArg: IFleetCutoverPhysicalContainer,
1271
+ ): Promise<void> => {
1272
+ if (containerArg.mountCorrelations === undefined) return;
1273
+ requireNetwork(containerArg.mountCorrelations.length === containerArg.mounts.length);
1274
+ const physicalMountIdentities = new Set(
1275
+ containerArg.mounts.map(itemArg => itemArg.mountIdentityDigest),
1276
+ );
1277
+ const serviceMountIdentities = new Set(
1278
+ containerArg.mountCorrelations.map(itemArg => itemArg.serviceMountIdentityDigest),
1279
+ );
1280
+ requireNetwork(physicalMountIdentities.size === containerArg.mounts.length
1281
+ && serviceMountIdentities.size === containerArg.mountCorrelations.length
1282
+ && containerArg.mountCorrelations.every(itemArg =>
1283
+ physicalMountIdentities.has(itemArg.physicalMountIdentityDigest)));
1284
+ for (const correlation of containerArg.mountCorrelations) {
1285
+ const { correlationIdentityDigest, ...input } = correlation;
1286
+ requireNetwork(correlationIdentityDigest
1287
+ === await computeFleetCutoverMountCorrelationIdentityDigest(input));
1288
+ }
1289
+ };
1290
+
985
1291
  export const computeFleetCutoverContainerSetIdentityDigest = async (
986
1292
  containerIdsArg: string[],
987
1293
  ): Promise<TSha256Digest> => safe(async () => {
@@ -1046,7 +1352,10 @@ export const validateFleetCutoverLocalNodeSnapshot = async (
1046
1352
  );
1047
1353
  requireNetwork(value.before.containerIdsDigest === containerIdsDigest
1048
1354
  && value.after.containerIdsDigest === containerIdsDigest);
1049
- for (const container of value.containers) await validateMountDescriptors(container.mounts);
1355
+ for (const container of value.containers) {
1356
+ await validateMountDescriptors(container.mounts);
1357
+ await validatePhysicalMountCorrelations(container);
1358
+ }
1050
1359
  return value.snapshotDigest === await computeFleetCutoverLocalNodeSnapshotDigest(value)
1051
1360
  ? [] : ['fleet cutover local node snapshot digest does not match'];
1052
1361
  } catch {
package/ts/data/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './cluster.js';
3
3
  export * from './clusterruntime.js';
4
4
  export * from './clustervpn.js';
5
5
  export * from './clusteringress.js';
6
+ export * from './clusterrelaycredential.js';
6
7
  export * from './config.js';
7
8
  export * from './corestore.js';
8
9
  export * from './coremail.js';
@@ -52,6 +53,7 @@ export * from './baseos.js';
52
53
  export * from './backup.js';
53
54
  export * from './clusternode.js';
54
55
  export * from './settings.js';
56
+ export * from './sparklegacyretirement.js';
55
57
  export * from './sparknodereport.js';
56
58
  export * from './service.js';
57
59
  export * from './servicecredential.js';