node-opcua-pki 6.12.2 → 6.14.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.
package/dist/index.d.mts CHANGED
@@ -671,6 +671,20 @@ interface CertificateManagerOptions {
671
671
  * Defaults are secure — all checks enabled.
672
672
  */
673
673
  addCertificateValidationOptions?: AddCertificateValidationOptions;
674
+ /**
675
+ * When `true`, the CertificateManager will **not** start
676
+ * chokidar file-system watchers on the PKI folders.
677
+ *
678
+ * The initial file-system scan still runs so the in-memory
679
+ * indexes are populated, but live change detection is
680
+ * disabled. This is useful in test / CI environments where
681
+ * many CertificateManager instances are created in parallel
682
+ * and the accumulated `fs.watch` handles exhaust the libuv
683
+ * thread-pool, causing event-loop starvation.
684
+ *
685
+ * @defaultValue false
686
+ */
687
+ disableFileWatchers?: boolean;
674
688
  }
675
689
  /**
676
690
  * Parameters for {@link createSelfSignedCertificate}.
@@ -857,6 +871,33 @@ declare enum CertificateManagerState {
857
871
  * await cm.dispose();
858
872
  * ```
859
873
  */
874
+ /**
875
+ * Status codes returned by {@link CertificateManager.completeCertificateChain}.
876
+ */
877
+ declare enum ChainCompletionStatus {
878
+ /** The chain already reached a self-signed root — no action was needed. */
879
+ AlreadyComplete = "AlreadyComplete",
880
+ /** One or more issuer certificates were successfully appended. */
881
+ ChainCompleted = "ChainCompleted",
882
+ /** The issuer for the last certificate in the chain could not be found
883
+ * in the issuers or trusted stores. The chain is still partial. */
884
+ IssuerNotFound = "IssuerNotFound",
885
+ /** The input chain was empty. */
886
+ EmptyChain = "EmptyChain",
887
+ /** Chain completion was stopped because the maximum depth was reached. */
888
+ MaxDepthReached = "MaxDepthReached"
889
+ }
890
+ /**
891
+ * Result of {@link CertificateManager.completeCertificateChain}.
892
+ */
893
+ interface ChainCompletionResult {
894
+ /** The (possibly completed) certificate chain, leaf first. */
895
+ chain: Certificate[];
896
+ /** Status code indicating whether completion succeeded and why/why not. */
897
+ status: ChainCompletionStatus;
898
+ /** Human-readable diagnostic message. */
899
+ message: string;
900
+ }
860
901
  declare class CertificateManager extends EventEmitter {
861
902
  #private;
862
903
  /**
@@ -1150,6 +1191,24 @@ declare class CertificateManager extends EventEmitter {
1150
1191
  *
1151
1192
  */
1152
1193
  findIssuerCertificate(certificate: Certificate | Certificate[]): Promise<Certificate | null>;
1194
+ /**
1195
+ * Outcome status for {@link CertificateManager.completeCertificateChain}.
1196
+ */
1197
+ static readonly ChainCompletionStatus: typeof ChainCompletionStatus;
1198
+ /**
1199
+ * Complete a certificate chain by walking the issuer store.
1200
+ *
1201
+ * Starting from the last certificate in the provided chain, this method
1202
+ * repeatedly calls {@link findIssuerCertificate} to locate the parent
1203
+ * certificate until it reaches a self-signed root or can no longer find
1204
+ * an issuer.
1205
+ *
1206
+ * @param chain - the (potentially partial) certificate chain, leaf first
1207
+ * @param maxDepth - maximum number of issuers to append (default: 10)
1208
+ * @returns a {@link ChainCompletionResult} containing the (possibly completed)
1209
+ * chain, a status code, and an optional diagnostic message.
1210
+ */
1211
+ completeCertificateChain(chain: Certificate[], maxDepth?: number): Promise<ChainCompletionResult>;
1153
1212
  /**
1154
1213
  * Check whether a certificate has been revoked by its issuer's CRL.
1155
1214
  *
@@ -1305,4 +1364,4 @@ declare function dumpPFX(pfxFile: Filename, passphrase?: string): Promise<string
1305
1364
  */
1306
1365
  declare function install_prerequisite(): Promise<string>;
1307
1366
 
1308
- export { type AddCertificateValidationOptions, CertificateAuthority, type CertificateAuthorityOptions, CertificateManager, type CertificateManagerEvents, type CertificateManagerOptions, CertificateManagerState, type CertificateStatus, type CertificateStore, type CreateCertificateSigningRequestOptions, type CreateCertificateSigningRequestWithConfigOptions, type CreatePFXOptions, type CreateSelfSignCertificateParam, type CreateSelfSignCertificateParam1, type CreateSelfSignCertificateWithConfigParam, type CrlStore, type ExtractPFXOptions, type ExtractPFXResult, type Filename, type GenerateKeyPairAndSignOptions, type GenerateKeyPairAndSignPFXOptions, type InitializeCSRResult, type InstallCACertificateResult, type KeyLength, type KeySize, type Params, type ProcessAltNamesParam, type SignCertificateOptions, type StartDateEndDateParam, type Thumbprint, VerificationStatus, type VerifyCertificateOptions, adjustApplicationUri, adjustDate, coerceCertificateChain, convertPFXtoPEM, createPFX, dumpPFX, extractAllFromPFX, extractCACertificatesFromPFX, extractCertificateFromPFX, extractPrivateKeyFromPFX, findIssuerCertificateInChain, install_prerequisite, isIntermediateIssuer, isIssuer, isRootIssuer, makeFingerprint, quote };
1367
+ export { type AddCertificateValidationOptions, CertificateAuthority, type CertificateAuthorityOptions, CertificateManager, type CertificateManagerEvents, type CertificateManagerOptions, CertificateManagerState, type CertificateStatus, type CertificateStore, type ChainCompletionResult, ChainCompletionStatus, type CreateCertificateSigningRequestOptions, type CreateCertificateSigningRequestWithConfigOptions, type CreatePFXOptions, type CreateSelfSignCertificateParam, type CreateSelfSignCertificateParam1, type CreateSelfSignCertificateWithConfigParam, type CrlStore, type ExtractPFXOptions, type ExtractPFXResult, type Filename, type GenerateKeyPairAndSignOptions, type GenerateKeyPairAndSignPFXOptions, type InitializeCSRResult, type InstallCACertificateResult, type KeyLength, type KeySize, type Params, type ProcessAltNamesParam, type SignCertificateOptions, type StartDateEndDateParam, type Thumbprint, VerificationStatus, type VerifyCertificateOptions, adjustApplicationUri, adjustDate, coerceCertificateChain, convertPFXtoPEM, createPFX, dumpPFX, extractAllFromPFX, extractCACertificatesFromPFX, extractCertificateFromPFX, extractPrivateKeyFromPFX, findIssuerCertificateInChain, install_prerequisite, isIntermediateIssuer, isIssuer, isRootIssuer, makeFingerprint, quote };
package/dist/index.d.ts CHANGED
@@ -671,6 +671,20 @@ interface CertificateManagerOptions {
671
671
  * Defaults are secure — all checks enabled.
672
672
  */
673
673
  addCertificateValidationOptions?: AddCertificateValidationOptions;
674
+ /**
675
+ * When `true`, the CertificateManager will **not** start
676
+ * chokidar file-system watchers on the PKI folders.
677
+ *
678
+ * The initial file-system scan still runs so the in-memory
679
+ * indexes are populated, but live change detection is
680
+ * disabled. This is useful in test / CI environments where
681
+ * many CertificateManager instances are created in parallel
682
+ * and the accumulated `fs.watch` handles exhaust the libuv
683
+ * thread-pool, causing event-loop starvation.
684
+ *
685
+ * @defaultValue false
686
+ */
687
+ disableFileWatchers?: boolean;
674
688
  }
675
689
  /**
676
690
  * Parameters for {@link createSelfSignedCertificate}.
@@ -857,6 +871,33 @@ declare enum CertificateManagerState {
857
871
  * await cm.dispose();
858
872
  * ```
859
873
  */
874
+ /**
875
+ * Status codes returned by {@link CertificateManager.completeCertificateChain}.
876
+ */
877
+ declare enum ChainCompletionStatus {
878
+ /** The chain already reached a self-signed root — no action was needed. */
879
+ AlreadyComplete = "AlreadyComplete",
880
+ /** One or more issuer certificates were successfully appended. */
881
+ ChainCompleted = "ChainCompleted",
882
+ /** The issuer for the last certificate in the chain could not be found
883
+ * in the issuers or trusted stores. The chain is still partial. */
884
+ IssuerNotFound = "IssuerNotFound",
885
+ /** The input chain was empty. */
886
+ EmptyChain = "EmptyChain",
887
+ /** Chain completion was stopped because the maximum depth was reached. */
888
+ MaxDepthReached = "MaxDepthReached"
889
+ }
890
+ /**
891
+ * Result of {@link CertificateManager.completeCertificateChain}.
892
+ */
893
+ interface ChainCompletionResult {
894
+ /** The (possibly completed) certificate chain, leaf first. */
895
+ chain: Certificate[];
896
+ /** Status code indicating whether completion succeeded and why/why not. */
897
+ status: ChainCompletionStatus;
898
+ /** Human-readable diagnostic message. */
899
+ message: string;
900
+ }
860
901
  declare class CertificateManager extends EventEmitter {
861
902
  #private;
862
903
  /**
@@ -1150,6 +1191,24 @@ declare class CertificateManager extends EventEmitter {
1150
1191
  *
1151
1192
  */
1152
1193
  findIssuerCertificate(certificate: Certificate | Certificate[]): Promise<Certificate | null>;
1194
+ /**
1195
+ * Outcome status for {@link CertificateManager.completeCertificateChain}.
1196
+ */
1197
+ static readonly ChainCompletionStatus: typeof ChainCompletionStatus;
1198
+ /**
1199
+ * Complete a certificate chain by walking the issuer store.
1200
+ *
1201
+ * Starting from the last certificate in the provided chain, this method
1202
+ * repeatedly calls {@link findIssuerCertificate} to locate the parent
1203
+ * certificate until it reaches a self-signed root or can no longer find
1204
+ * an issuer.
1205
+ *
1206
+ * @param chain - the (potentially partial) certificate chain, leaf first
1207
+ * @param maxDepth - maximum number of issuers to append (default: 10)
1208
+ * @returns a {@link ChainCompletionResult} containing the (possibly completed)
1209
+ * chain, a status code, and an optional diagnostic message.
1210
+ */
1211
+ completeCertificateChain(chain: Certificate[], maxDepth?: number): Promise<ChainCompletionResult>;
1153
1212
  /**
1154
1213
  * Check whether a certificate has been revoked by its issuer's CRL.
1155
1214
  *
@@ -1305,4 +1364,4 @@ declare function dumpPFX(pfxFile: Filename, passphrase?: string): Promise<string
1305
1364
  */
1306
1365
  declare function install_prerequisite(): Promise<string>;
1307
1366
 
1308
- export { type AddCertificateValidationOptions, CertificateAuthority, type CertificateAuthorityOptions, CertificateManager, type CertificateManagerEvents, type CertificateManagerOptions, CertificateManagerState, type CertificateStatus, type CertificateStore, type CreateCertificateSigningRequestOptions, type CreateCertificateSigningRequestWithConfigOptions, type CreatePFXOptions, type CreateSelfSignCertificateParam, type CreateSelfSignCertificateParam1, type CreateSelfSignCertificateWithConfigParam, type CrlStore, type ExtractPFXOptions, type ExtractPFXResult, type Filename, type GenerateKeyPairAndSignOptions, type GenerateKeyPairAndSignPFXOptions, type InitializeCSRResult, type InstallCACertificateResult, type KeyLength, type KeySize, type Params, type ProcessAltNamesParam, type SignCertificateOptions, type StartDateEndDateParam, type Thumbprint, VerificationStatus, type VerifyCertificateOptions, adjustApplicationUri, adjustDate, coerceCertificateChain, convertPFXtoPEM, createPFX, dumpPFX, extractAllFromPFX, extractCACertificatesFromPFX, extractCertificateFromPFX, extractPrivateKeyFromPFX, findIssuerCertificateInChain, install_prerequisite, isIntermediateIssuer, isIssuer, isRootIssuer, makeFingerprint, quote };
1367
+ export { type AddCertificateValidationOptions, CertificateAuthority, type CertificateAuthorityOptions, CertificateManager, type CertificateManagerEvents, type CertificateManagerOptions, CertificateManagerState, type CertificateStatus, type CertificateStore, type ChainCompletionResult, ChainCompletionStatus, type CreateCertificateSigningRequestOptions, type CreateCertificateSigningRequestWithConfigOptions, type CreatePFXOptions, type CreateSelfSignCertificateParam, type CreateSelfSignCertificateParam1, type CreateSelfSignCertificateWithConfigParam, type CrlStore, type ExtractPFXOptions, type ExtractPFXResult, type Filename, type GenerateKeyPairAndSignOptions, type GenerateKeyPairAndSignPFXOptions, type InitializeCSRResult, type InstallCACertificateResult, type KeyLength, type KeySize, type Params, type ProcessAltNamesParam, type SignCertificateOptions, type StartDateEndDateParam, type Thumbprint, VerificationStatus, type VerifyCertificateOptions, adjustApplicationUri, adjustDate, coerceCertificateChain, convertPFXtoPEM, createPFX, dumpPFX, extractAllFromPFX, extractCACertificatesFromPFX, extractCertificateFromPFX, extractPrivateKeyFromPFX, findIssuerCertificateInChain, install_prerequisite, isIntermediateIssuer, isIssuer, isRootIssuer, makeFingerprint, quote };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ __export(lib_exports, {
33
33
  CertificateAuthority: () => CertificateAuthority,
34
34
  CertificateManager: () => CertificateManager,
35
35
  CertificateManagerState: () => CertificateManagerState,
36
+ ChainCompletionStatus: () => ChainCompletionStatus,
36
37
  Subject: () => import_node_opcua_crypto.Subject,
37
38
  VerificationStatus: () => VerificationStatus,
38
39
  adjustApplicationUri: () => adjustApplicationUri,
@@ -853,15 +854,16 @@ nsComment = "CA Generated by Node-OPCUA Certificate utility usin
853
854
  subjectKeyIdentifier = hash
854
855
  basicConstraints = CA:TRUE
855
856
  keyUsage = critical, cRLSign, keyCertSign
857
+ subjectAltName = $ENV::ALTNAME
856
858
  nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
857
859
  [ v3_ca ]
858
860
  subjectKeyIdentifier = hash
859
861
  authorityKeyIdentifier = keyid:always,issuer:always
860
862
  basicConstraints = CA:TRUE
861
863
  keyUsage = critical, cRLSign, keyCertSign
864
+ subjectAltName = $ENV::ALTNAME
862
865
  nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
863
866
  #nsCertType = sslCA, emailCA
864
- #subjectAltName = email:copy
865
867
  #issuerAltName = issuer:copy
866
868
  #obj = DER:02:03
867
869
  crlDistributionPoints = @crl_info
@@ -948,7 +950,8 @@ async function construct_CertificateAuthority(certificateAuthority) {
948
950
  await import_node_fs7.default.promises.writeFile(caConfigFile, data);
949
951
  }
950
952
  const subjectOpt = ` -subj "${subject.toString()}" `;
951
- processAltNames({});
953
+ const caCommonName = subject.commonName || "NodeOPCUA-CA";
954
+ setEnv("ALTNAME", `URI:urn:${caCommonName}`);
952
955
  const options = { cwd: caRootDir };
953
956
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
954
957
  const configOption = ` -config ${q3(n4(configFile))}`;
@@ -2050,6 +2053,14 @@ var CertificateManagerState = /* @__PURE__ */ ((CertificateManagerState2) => {
2050
2053
  CertificateManagerState2[CertificateManagerState2["Disposed"] = 4] = "Disposed";
2051
2054
  return CertificateManagerState2;
2052
2055
  })(CertificateManagerState || {});
2056
+ var ChainCompletionStatus = /* @__PURE__ */ ((ChainCompletionStatus2) => {
2057
+ ChainCompletionStatus2["AlreadyComplete"] = "AlreadyComplete";
2058
+ ChainCompletionStatus2["ChainCompleted"] = "ChainCompleted";
2059
+ ChainCompletionStatus2["IssuerNotFound"] = "IssuerNotFound";
2060
+ ChainCompletionStatus2["EmptyChain"] = "EmptyChain";
2061
+ ChainCompletionStatus2["MaxDepthReached"] = "MaxDepthReached";
2062
+ return ChainCompletionStatus2;
2063
+ })(ChainCompletionStatus || {});
2053
2064
  var CertificateManager = class _CertificateManager extends import_node_events.EventEmitter {
2054
2065
  // ── Global instance registry ─────────────────────────────────
2055
2066
  // Tracks all initialized CertificateManager instances so their
@@ -2142,6 +2153,7 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2142
2153
  #filenameToHash = /* @__PURE__ */ new Map();
2143
2154
  #initializingPromise;
2144
2155
  #addCertValidation;
2156
+ #disableFileWatchers;
2145
2157
  #thumbs = {
2146
2158
  rejected: /* @__PURE__ */ new Map(),
2147
2159
  trusted: /* @__PURE__ */ new Map(),
@@ -2175,6 +2187,7 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2175
2187
  ignoreMissingRevocationList: v.ignoreMissingRevocationList ?? false,
2176
2188
  maxChainLength: v.maxChainLength ?? 5
2177
2189
  };
2190
+ this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
2178
2191
  mkdirRecursiveSync(options.location);
2179
2192
  if (!import_node_fs10.default.existsSync(this.#location)) {
2180
2193
  throw new Error(`CertificateManager cannot access location ${this.#location}`);
@@ -3039,6 +3052,75 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
3039
3052
  }
3040
3053
  return selectedTrustedCertificates.length > 0 ? selectedTrustedCertificates[0].certificate : null;
3041
3054
  }
3055
+ /**
3056
+ * Outcome status for {@link CertificateManager.completeCertificateChain}.
3057
+ */
3058
+ static ChainCompletionStatus = ChainCompletionStatus;
3059
+ /**
3060
+ * Complete a certificate chain by walking the issuer store.
3061
+ *
3062
+ * Starting from the last certificate in the provided chain, this method
3063
+ * repeatedly calls {@link findIssuerCertificate} to locate the parent
3064
+ * certificate until it reaches a self-signed root or can no longer find
3065
+ * an issuer.
3066
+ *
3067
+ * @param chain - the (potentially partial) certificate chain, leaf first
3068
+ * @param maxDepth - maximum number of issuers to append (default: 10)
3069
+ * @returns a {@link ChainCompletionResult} containing the (possibly completed)
3070
+ * chain, a status code, and an optional diagnostic message.
3071
+ */
3072
+ async completeCertificateChain(chain, maxDepth = 10) {
3073
+ if (chain.length === 0) {
3074
+ return {
3075
+ chain,
3076
+ status: "EmptyChain" /* EmptyChain */,
3077
+ message: "Input chain is empty \u2014 nothing to complete."
3078
+ };
3079
+ }
3080
+ await this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs);
3081
+ const result = [...chain];
3082
+ let depth = 0;
3083
+ while (depth < maxDepth) {
3084
+ const lastCert = result[result.length - 1];
3085
+ const lastInfo = (0, import_node_opcua_crypto5.exploreCertificate)(lastCert);
3086
+ if (isSelfSigned2(lastInfo)) {
3087
+ const wasExtended = result.length > chain.length;
3088
+ return {
3089
+ chain: result,
3090
+ status: wasExtended ? "ChainCompleted" /* ChainCompleted */ : "AlreadyComplete" /* AlreadyComplete */,
3091
+ message: wasExtended ? `Chain completed: ${result.length - chain.length} issuer(s) appended, ending at self-signed root "${lastInfo.tbsCertificate.subject.commonName}".` : `Chain is already complete (self-signed root "${lastInfo.tbsCertificate.subject.commonName}").`
3092
+ };
3093
+ }
3094
+ const issuerCert = await this.findIssuerCertificate(lastCert);
3095
+ if (!issuerCert) {
3096
+ const cn = lastInfo.tbsCertificate.subject.commonName ?? "?";
3097
+ const akid = lastInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier ?? "?";
3098
+ const msg = `Cannot find issuer for "${cn}" (authorityKeyIdentifier: ${akid}). Ensure the CA certificate is present in the issuers/certs folder.`;
3099
+ warningLog(`completeCertificateChain: ${msg}`);
3100
+ return {
3101
+ chain: result,
3102
+ status: "IssuerNotFound" /* IssuerNotFound */,
3103
+ message: msg
3104
+ };
3105
+ }
3106
+ const issuerFingerprint = makeFingerprint(issuerCert);
3107
+ const alreadyInChain = result.some((c) => makeFingerprint(c) === issuerFingerprint);
3108
+ if (alreadyInChain) {
3109
+ return {
3110
+ chain: result,
3111
+ status: "AlreadyComplete" /* AlreadyComplete */,
3112
+ message: `Chain ends at root "${(0, import_node_opcua_crypto5.exploreCertificate)(issuerCert).tbsCertificate.subject.commonName}" (already present in chain).`
3113
+ };
3114
+ }
3115
+ result.push(issuerCert);
3116
+ depth++;
3117
+ }
3118
+ return {
3119
+ chain: result,
3120
+ status: "MaxDepthReached" /* MaxDepthReached */,
3121
+ message: `Chain completion stopped after ${maxDepth} iterations \u2014 possible circular chain or very deep hierarchy.`
3122
+ };
3123
+ }
3042
3124
  /**
3043
3125
  *
3044
3126
  * check if the certificate explicitly appear in the trust list, the reject list or none.
@@ -3234,11 +3316,15 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
3234
3316
  this.#scanCrlFolder(this.crlFolder, this.#thumbs.crl),
3235
3317
  this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
3236
3318
  ]);
3237
- this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
3238
- this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
3239
- this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
3240
- this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
3241
- this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
3319
+ if (this.#disableFileWatchers) {
3320
+ import_node_fs10.default.watch = origWatch;
3321
+ } else {
3322
+ this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
3323
+ this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
3324
+ this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
3325
+ this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
3326
+ this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
3327
+ }
3242
3328
  }
3243
3329
  /**
3244
3330
  * Scan a certificate folder and populate the in-memory index.
@@ -3420,6 +3506,7 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
3420
3506
  CertificateAuthority,
3421
3507
  CertificateManager,
3422
3508
  CertificateManagerState,
3509
+ ChainCompletionStatus,
3423
3510
  Subject,
3424
3511
  VerificationStatus,
3425
3512
  adjustApplicationUri,