@twin.org/node-core 0.0.2-next.24 → 0.0.2-next.26

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.
Files changed (31) hide show
  1. package/README.md +21 -7
  2. package/dist/cjs/index.cjs +389 -51
  3. package/dist/esm/index.mjs +385 -56
  4. package/dist/types/index.d.ts +4 -0
  5. package/dist/types/models/ICacheMetadata.d.ts +17 -0
  6. package/dist/types/models/IModuleProtocol.d.ts +18 -0
  7. package/dist/types/models/INodeEnvironmentVariables.d.ts +25 -0
  8. package/dist/types/models/IProtocolHandlerResult.d.ts +13 -0
  9. package/dist/types/models/moduleProtocol.d.ts +29 -0
  10. package/dist/types/node.d.ts +3 -2
  11. package/dist/types/utils.d.ts +67 -0
  12. package/docs/changelog.md +14 -0
  13. package/docs/detailed-guide.md +129 -0
  14. package/docs/reference/functions/createModuleImportUrl.md +21 -0
  15. package/docs/reference/functions/getExtensionsCacheDir.md +31 -0
  16. package/docs/reference/functions/handleHttpsProtocol.md +49 -0
  17. package/docs/reference/functions/handleNpmProtocol.md +31 -0
  18. package/docs/reference/functions/hashUrl.md +19 -0
  19. package/docs/reference/functions/isCacheExpired.md +31 -0
  20. package/docs/reference/functions/overrideModuleImport.md +8 -2
  21. package/docs/reference/functions/parseModuleProtocol.md +19 -0
  22. package/docs/reference/functions/resolvePackageEntryPoint.md +32 -0
  23. package/docs/reference/index.md +13 -0
  24. package/docs/reference/interfaces/ICacheMetadata.md +27 -0
  25. package/docs/reference/interfaces/IModuleProtocol.md +27 -0
  26. package/docs/reference/interfaces/INodeEnvironmentVariables.md +70 -0
  27. package/docs/reference/interfaces/IProtocolHandlerResult.md +19 -0
  28. package/docs/reference/type-aliases/ModuleProtocol.md +5 -0
  29. package/docs/reference/variables/ModuleProtocol.md +37 -0
  30. package/locales/en.json +11 -2
  31. package/package.json +7 -3
@@ -1,19 +1,20 @@
1
1
  import { PasswordHelper } from '@twin.org/api-auth-entity-storage-service';
2
- import { I18n, Is, Coerce, Converter, RandomHelper, Urn, GeneralError, EnvHelper } from '@twin.org/core';
3
- import { PasswordGenerator, Bip39 } from '@twin.org/crypto';
2
+ import { I18n, Is, Converter, GeneralError, BaseError, Coerce, RandomHelper, Urn, EnvHelper } from '@twin.org/core';
3
+ import { Sha256, PasswordGenerator, Bip39 } from '@twin.org/crypto';
4
4
  import { AuthenticationComponentType, InformationComponentType, RestRouteProcessorType, SocketRouteProcessorType, AuthenticationAdminComponentType } from '@twin.org/engine-server-types';
5
5
  import { WalletConnectorType, IdentityConnectorType, EntityStorageConnectorType, BlobStorageConnectorType, BlobStorageComponentType, VaultConnectorType, DltConfigType, LoggingConnectorType, LoggingComponentType, BackgroundTaskConnectorType, TaskSchedulerComponentType, EventBusConnectorType, EventBusComponentType, TelemetryConnectorType, TelemetryComponentType, MessagingEmailConnectorType, MessagingSmsConnectorType, MessagingPushNotificationConnectorType, MessagingAdminComponentType, MessagingComponentType, FaucetConnectorType, EngineTypeHelper, NftConnectorType, NftComponentType, VerifiableStorageConnectorType, VerifiableStorageComponentType, ImmutableProofComponentType, IdentityComponentType, IdentityResolverConnectorType, IdentityResolverComponentType, IdentityProfileConnectorType, IdentityProfileComponentType, AttestationConnectorType, AttestationComponentType, DataProcessingComponentType, DataConverterConnectorType, DataExtractorConnectorType, AuditableItemGraphComponentType, AuditableItemStreamComponentType, DocumentManagementComponentType, AuthenticationGeneratorComponentType, RightsManagementPapComponentType, RightsManagementPmpComponentType, RightsManagementPipComponentType, RightsManagementPxpComponentType, RightsManagementPdpComponentType, RightsManagementPepComponentType, RightsManagementPnpComponentType, RightsManagementPnapComponentType, RightsManagementDapComponentType, RightsManagementDarpComponentType, SynchronisedStorageComponentType, FederatedCatalogueComponentType, DataSpaceConnectorComponentType } from '@twin.org/engine-types';
6
6
  import { EntityStorageConnectorFactory } from '@twin.org/entity-storage-models';
7
7
  import { IdentityProfileConnectorFactory, IdentityConnectorFactory, IdentityResolverConnectorFactory, DocumentHelper } from '@twin.org/identity-models';
8
8
  import { VaultConnectorFactory, VaultKeyType } from '@twin.org/vault-models';
9
9
  import { WalletConnectorFactory } from '@twin.org/wallet-models';
10
- import { readFile, stat, readdir } from 'node:fs/promises';
10
+ import { execSync } from 'node:child_process';
11
+ import { readFile, stat, readdir, mkdir, writeFile, rename } from 'node:fs/promises';
12
+ import { get } from 'node:https';
11
13
  import path from 'node:path';
12
14
  import { CLIDisplay } from '@twin.org/cli-core';
13
- import { PolicyNegotiationPointClient, DataAccessPointClient } from '@twin.org/rights-management-rest-client';
14
- import { addDefaultRestPaths, addDefaultSocketPaths, EngineServer } from '@twin.org/engine-server';
15
15
  import { ModuleHelper } from '@twin.org/modules';
16
- import { execSync } from 'node:child_process';
16
+ import { PolicyNegotiationPointRestClient, DataAccessPointRestClient } from '@twin.org/rights-management-rest-client';
17
+ import { addDefaultRestPaths, addDefaultSocketPaths, EngineServer } from '@twin.org/engine-server';
17
18
  import * as dotenv from 'dotenv';
18
19
  import { Engine } from '@twin.org/engine';
19
20
  import { FileStateStorage } from '@twin.org/engine-core';
@@ -49,6 +50,35 @@ const NodeFeatures = {
49
50
  NodeWallet: "node-wallet"
50
51
  };
51
52
 
53
+ // Copyright 2024 IOTA Stiftung.
54
+ // SPDX-License-Identifier: Apache-2.0.
55
+ /**
56
+ * The protocol types for modules.
57
+ */
58
+ // eslint-disable-next-line @typescript-eslint/naming-convention
59
+ const ModuleProtocol = {
60
+ /**
61
+ * Local module (starts with . or / or file://).
62
+ */
63
+ Local: "local",
64
+ /**
65
+ * NPM package (starts with npm:).
66
+ */
67
+ Npm: "npm",
68
+ /**
69
+ * HTTPS URL (starts with https://).
70
+ */
71
+ Https: "https",
72
+ /**
73
+ * HTTP URL (starts with http://).
74
+ */
75
+ Http: "http",
76
+ /**
77
+ * Default/standard module resolution.
78
+ */
79
+ Default: "default"
80
+ };
81
+
52
82
  // Copyright 2024 IOTA Stiftung.
53
83
  // SPDX-License-Identifier: Apache-2.0.
54
84
  /**
@@ -182,6 +212,280 @@ function getFeatures(env) {
182
212
  }
183
213
  return features;
184
214
  }
215
+ /**
216
+ * Parse the protocol from a module name.
217
+ * @param moduleName The module name to parse.
218
+ * @returns The parsed protocol information.
219
+ */
220
+ function parseModuleProtocol(moduleName) {
221
+ const trimmed = moduleName.trim();
222
+ if (trimmed.startsWith("npm:")) {
223
+ return {
224
+ protocol: ModuleProtocol.Npm,
225
+ identifier: trimmed.slice(4),
226
+ original: trimmed
227
+ };
228
+ }
229
+ if (trimmed.startsWith("https://")) {
230
+ return {
231
+ protocol: ModuleProtocol.Https,
232
+ identifier: trimmed,
233
+ original: trimmed
234
+ };
235
+ }
236
+ if (trimmed.startsWith("http://")) {
237
+ return {
238
+ protocol: ModuleProtocol.Http,
239
+ identifier: trimmed,
240
+ original: trimmed
241
+ };
242
+ }
243
+ if (trimmed.startsWith("file://")) {
244
+ return {
245
+ protocol: ModuleProtocol.Local,
246
+ identifier: trimmed,
247
+ original: trimmed
248
+ };
249
+ }
250
+ if (ModuleHelper.isLocalModule(trimmed)) {
251
+ return {
252
+ protocol: ModuleProtocol.Local,
253
+ identifier: trimmed,
254
+ original: trimmed
255
+ };
256
+ }
257
+ return {
258
+ protocol: ModuleProtocol.Default,
259
+ identifier: trimmed,
260
+ original: trimmed
261
+ };
262
+ }
263
+ /**
264
+ * Hash a URL to create a safe filename.
265
+ * @param url The URL to hash.
266
+ * @returns A hashed filename safe for the filesystem.
267
+ */
268
+ function hashUrl(url) {
269
+ const urlBytes = Converter.utf8ToBytes(url);
270
+ const hashBytes = Sha256.sum256(urlBytes);
271
+ const hash = Converter.bytesToHex(hashBytes);
272
+ const ext = path.extname(new URL(url).pathname);
273
+ return `${hash}${ext}`;
274
+ }
275
+ /**
276
+ * Get the extensions cache directory.
277
+ * @param executionDirectory The execution directory.
278
+ * @param protocol The protocol type for subdirectory organization.
279
+ * @param cacheDirectory The cache directory base path.
280
+ * @returns The cache directory path.
281
+ */
282
+ function getExtensionsCacheDir(executionDirectory, protocol, cacheDirectory) {
283
+ // Resolve to absolute path to ensure consistent behavior
284
+ const absoluteDir = path.resolve(executionDirectory);
285
+ const baseDir = cacheDirectory ?? ".tmp";
286
+ return path.join(absoluteDir, baseDir, "extensions", protocol);
287
+ }
288
+ /**
289
+ * Handle the npm: protocol by installing the package if needed.
290
+ * @param packageName The npm package name (without npm: prefix).
291
+ * @param executionDirectory The execution directory.
292
+ * @param cacheDirectory The cache directory base path.
293
+ * @returns The resolved path to the installed module.
294
+ */
295
+ async function handleNpmProtocol(packageName, executionDirectory, cacheDirectory) {
296
+ const cacheDir = getExtensionsCacheDir(executionDirectory, ModuleProtocol.Npm, cacheDirectory);
297
+ // Extract just the package name (without version) for the directory
298
+ // e.g. "picocolors@1.0.0" becomes "picocolors"
299
+ // e.g. "@scope/package@1.0.0" becomes "@scope/package"
300
+ const lastAtIndex = packageName.lastIndexOf("@");
301
+ const packageNameOnly = lastAtIndex > 0 ? packageName.slice(0, lastAtIndex) : packageName;
302
+ const packageDir = path.join(cacheDir, "node_modules", packageNameOnly);
303
+ const packageJsonPath = path.join(packageDir, "package.json");
304
+ const exists = await fileExists(packageJsonPath);
305
+ if (exists) {
306
+ const mainFile = await resolvePackageEntryPoint(packageDir, packageNameOnly);
307
+ const modulePath = path.join(packageDir, mainFile);
308
+ return {
309
+ resolvedPath: modulePath,
310
+ cached: true
311
+ };
312
+ }
313
+ await mkdir(cacheDir, { recursive: true });
314
+ CLIDisplay.task(I18n.formatMessage("node.extensionNpmInstalling"), packageName);
315
+ try {
316
+ // Always pipe stdio to comply with env access restrictions in tests/lint
317
+ const stdio = "pipe";
318
+ execSync(`npm install ${packageName} --prefix "${cacheDir}" --no-save --no-package-lock`, {
319
+ cwd: cacheDir,
320
+ stdio
321
+ });
322
+ }
323
+ catch (err) {
324
+ throw new GeneralError("node", "extensionNpmInstallFailed", {
325
+ package: packageName
326
+ }, BaseError.fromError(err));
327
+ }
328
+ const mainFile = await resolvePackageEntryPoint(packageDir, packageNameOnly);
329
+ const modulePath = path.join(packageDir, mainFile);
330
+ return {
331
+ resolvedPath: modulePath,
332
+ cached: false
333
+ };
334
+ }
335
+ /**
336
+ * Check if a cached file has expired based on TTL and force refresh settings.
337
+ * @param metadataPath Path to the cache metadata file.
338
+ * @param ttlHours Time to live in hours.
339
+ * @param forceRefresh Whether to force refresh regardless of TTL.
340
+ * @returns True if the cache is expired or should be refreshed.
341
+ */
342
+ async function isCacheExpired(metadataPath, ttlHours, forceRefresh) {
343
+ if (forceRefresh) {
344
+ return true;
345
+ }
346
+ try {
347
+ const metadata = await loadJsonFile(metadataPath);
348
+ const ttlMillis = ttlHours * 60 * 60 * 1000;
349
+ const expireTime = metadata.downloadedAt + ttlMillis;
350
+ return Date.now() > expireTime;
351
+ }
352
+ catch {
353
+ // If metadata doesn't exist or is corrupted, consider expired
354
+ return true;
355
+ }
356
+ }
357
+ /**
358
+ * Handle the https: protocol by downloading the module if needed.
359
+ * @param url The HTTPS URL to download from.
360
+ * @param executionDirectory The execution directory.
361
+ * @param maxSizeMb The maximum size in MB for the download.
362
+ * @param cacheDirectory The cache directory base path.
363
+ * @param ttlHours TTL in hours for cache expiration.
364
+ * @param forceRefresh Whether to force refresh the cache.
365
+ * @returns The resolved path to the downloaded module.
366
+ */
367
+ async function handleHttpsProtocol(url, executionDirectory, maxSizeMb, cacheDirectory, ttlHours, forceRefresh) {
368
+ const effectiveTtlHours = ttlHours ?? 24;
369
+ const effectiveForceRefresh = forceRefresh ?? false;
370
+ const cacheDir = getExtensionsCacheDir(executionDirectory, ModuleProtocol.Https, cacheDirectory);
371
+ const filename = hashUrl(url);
372
+ const cachedPath = path.join(cacheDir, filename);
373
+ const metadataPath = `${cachedPath}.meta`;
374
+ const exists = await fileExists(cachedPath);
375
+ if (exists) {
376
+ const expired = await isCacheExpired(metadataPath, effectiveTtlHours, effectiveForceRefresh);
377
+ if (!expired) {
378
+ return {
379
+ resolvedPath: cachedPath,
380
+ cached: true
381
+ };
382
+ }
383
+ if (effectiveForceRefresh) {
384
+ CLIDisplay.warning(I18n.formatMessage("node.extensionForceRefresh", { url }));
385
+ }
386
+ else {
387
+ CLIDisplay.task(I18n.formatMessage("node.extensionCacheExpired", { url }));
388
+ }
389
+ }
390
+ CLIDisplay.warning(I18n.formatMessage("node.extensionSecurityWarning", { url }));
391
+ CLIDisplay.task(I18n.formatMessage("node.extensionHttpsDownloading"), url);
392
+ await mkdir(cacheDir, { recursive: true });
393
+ const maxSizeBytes = maxSizeMb * 1024 * 1024;
394
+ let downloadedSize = 0;
395
+ const chunks = [];
396
+ try {
397
+ await new Promise((resolve, reject) => {
398
+ get(url, response => {
399
+ if (response.statusCode !== 200) {
400
+ reject(new GeneralError("node", "extensionDownloadFailed", {
401
+ url,
402
+ status: response.statusCode ?? 0
403
+ }));
404
+ return;
405
+ }
406
+ response.on("data", (chunk) => {
407
+ downloadedSize += chunk.length;
408
+ if (downloadedSize > maxSizeBytes) {
409
+ response.destroy();
410
+ reject(new GeneralError("node", "extensionSizeLimitExceeded", {
411
+ size: downloadedSize,
412
+ limit: maxSizeBytes
413
+ }));
414
+ return;
415
+ }
416
+ chunks.push(chunk);
417
+ });
418
+ response.on("end", () => {
419
+ resolve();
420
+ });
421
+ response.on("error", err => {
422
+ reject(BaseError.fromError(err));
423
+ });
424
+ }).on("error", err => {
425
+ reject(BaseError.fromError(err));
426
+ });
427
+ });
428
+ }
429
+ catch (err) {
430
+ throw new GeneralError("node", "extensionDownloadFailed", {
431
+ url
432
+ }, BaseError.fromError(err));
433
+ }
434
+ const tempPath = `${cachedPath}.tmp`;
435
+ await writeFile(tempPath, Buffer.concat(chunks));
436
+ // Atomic move from temp to final location
437
+ await rename(tempPath, cachedPath);
438
+ // Save metadata for TTL tracking
439
+ const metadata = {
440
+ downloadedAt: Date.now(),
441
+ url,
442
+ size: Buffer.concat(chunks).length
443
+ };
444
+ await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
445
+ return {
446
+ resolvedPath: cachedPath,
447
+ cached: false
448
+ };
449
+ }
450
+ /**
451
+ * Resolve the main entry point from a package directory using Node.js resolution with fallback.
452
+ * Uses require.resolve() when possible for standard Node.js behavior, with manual fallback.
453
+ * @param packagePath The absolute path to the package directory.
454
+ * @param packageName The package name for require.resolve().
455
+ * @param fallback The fallback file name if no entry point is found.
456
+ * @returns The resolved entry point file name (relative to package directory).
457
+ */
458
+ async function resolvePackageEntryPoint(packagePath, packageName, fallback = "index.js") {
459
+ try {
460
+ // Try require.resolve() first - handles exports, main, module automatically
461
+ const resolvedPath = require.resolve(packageName, { paths: [path.dirname(packagePath)] });
462
+ // Convert absolute path back to relative filename within package
463
+ const relativePath = path.relative(packagePath, resolvedPath);
464
+ return relativePath ?? fallback;
465
+ }
466
+ catch {
467
+ // Fallback to manual package.json parsing if require.resolve fails
468
+ try {
469
+ const packageJsonPath = path.join(packagePath, "package.json");
470
+ const packageJsonContent = await loadJsonFile(packageJsonPath);
471
+ // Future: Could expand exports field support here
472
+ return packageJsonContent.module ?? packageJsonContent.main ?? fallback;
473
+ }
474
+ catch {
475
+ return fallback;
476
+ }
477
+ }
478
+ }
479
+ /**
480
+ * Convert a file path to an import-compatible URL for cross-platform module loading.
481
+ * On Windows, adds the 'file://' protocol prefix required for dynamic imports.
482
+ * On other platforms, returns the path unchanged.
483
+ * @param filePath The absolute file path to convert.
484
+ * @returns A URL string compatible with dynamic import().
485
+ */
486
+ function createModuleImportUrl(filePath) {
487
+ return process.platform === "win32" ? `file://${filePath}` : filePath;
488
+ }
185
489
 
186
490
  // Copyright 2024 IOTA Stiftung.
187
491
  // SPDX-License-Identifier: Apache-2.0.
@@ -1494,7 +1798,7 @@ async function configureRightsManagement(coreConfig, envVars) {
1494
1798
  offers: Is.arrayValue(envVars.rightsManagementOffers)
1495
1799
  ? envVars.rightsManagementOffers
1496
1800
  : [],
1497
- negotiationComponentCreator: async (url) => new PolicyNegotiationPointClient({ endpoint: url })
1801
+ negotiationComponentCreator: async (url) => new PolicyNegotiationPointRestClient({ endpoint: url })
1498
1802
  }
1499
1803
  }
1500
1804
  });
@@ -1511,7 +1815,7 @@ async function configureRightsManagement(coreConfig, envVars) {
1511
1815
  type: RightsManagementDarpComponentType.Service,
1512
1816
  options: {
1513
1817
  config: {
1514
- dataAccessComponentCreator: async (url) => new DataAccessPointClient({ endpoint: url })
1818
+ dataAccessComponentCreator: async (url) => new DataAccessPointRestClient({ endpoint: url })
1515
1819
  }
1516
1820
  }
1517
1821
  });
@@ -1973,7 +2277,7 @@ async function run(nodeOptions) {
1973
2277
  nodeOptions ??= {};
1974
2278
  const serverInfo = {
1975
2279
  name: nodeOptions?.serverName ?? "TWIN Node Server",
1976
- version: nodeOptions?.serverVersion ?? "0.0.2-next.24" // x-release-please-version
2280
+ version: nodeOptions?.serverVersion ?? "0.0.2-next.26" // x-release-please-version
1977
2281
  };
1978
2282
  CLIDisplay.header(serverInfo.name, serverInfo.version, "🌩️ ");
1979
2283
  if (!Is.stringValue(nodeOptions?.executionDirectory)) {
@@ -2111,10 +2415,13 @@ async function buildConfiguration(processEnv, options, serverInfo) {
2111
2415
  return { nodeEngineConfig, nodeEnvVars: envVars };
2112
2416
  }
2113
2417
  /**
2114
- * Override module imports to use local files where possible.
2418
+ * Override module imports to support protocol-based loading (npm:, https:) and local files.
2115
2419
  * @param executionDirectory The execution directory for resolving local module paths.
2420
+ * @param envVars The environment variables containing extension configuration (optional, uses defaults if not provided).
2116
2421
  */
2117
- function overrideModuleImport(executionDirectory) {
2422
+ function overrideModuleImport(executionDirectory, envVars) {
2423
+ const maxSizeMb = Coerce.number(envVars?.extensionsMaxSizeMb) ?? 10;
2424
+ const cacheDirectory = envVars?.extensionsCacheDirectory;
2118
2425
  ModuleHelper.overrideImport(async (moduleName) => {
2119
2426
  if (moduleCache[moduleName]) {
2120
2427
  return {
@@ -2122,58 +2429,80 @@ function overrideModuleImport(executionDirectory) {
2122
2429
  useDefault: false
2123
2430
  };
2124
2431
  }
2125
- // If the module path for example when dynamically loading
2126
- // modules looks like a local file then we try to resolve
2127
- // using the local file system
2128
- const isLocal = ModuleHelper.isLocalModule(moduleName);
2129
- if (isLocal) {
2130
- // See if we can just resolve the filename locally
2131
- let localFilename = path.resolve(moduleName);
2132
- let exists = await fileExists(localFilename);
2133
- if (!exists) {
2134
- // Doesn't exist in the current directory, try the execution directory
2135
- localFilename = path.resolve(executionDirectory, moduleName);
2136
- exists = await fileExists(localFilename);
2137
- }
2138
- if (exists) {
2139
- // If the module exists then we can load it, otherwise
2140
- // we fallback to regular handling to see if that can import it
2141
- const module = await import(process.platform === "win32" ? `file://${localFilename}` : localFilename);
2142
- moduleCache[moduleName] = module;
2143
- return {
2144
- module,
2145
- useDefault: false
2146
- };
2432
+ const parsed = parseModuleProtocol(moduleName);
2433
+ let resolvedPath;
2434
+ switch (parsed.protocol) {
2435
+ case ModuleProtocol.Npm: {
2436
+ const result = await handleNpmProtocol(parsed.identifier, executionDirectory, cacheDirectory);
2437
+ resolvedPath = result.resolvedPath;
2438
+ break;
2147
2439
  }
2148
- }
2149
- try {
2150
- // Try and load from node_modules manually
2151
- // This is needed for some environments where
2152
- // the module resolution doesn't work as expected
2153
- const npmRoot = execSync("npm root").toString().trim().replace(/\\/g, "/");
2154
- const packageJson = await loadJsonFile(path.resolve(npmRoot, moduleName, "package.json"));
2155
- const mainFile = packageJson?.module ?? packageJson?.main ?? "index.js";
2156
- const modulePath = path.resolve(npmRoot, moduleName, mainFile);
2157
- const exists = await fileExists(modulePath);
2158
- if (exists) {
2159
- const module = await import(process.platform === "win32" ? `file://${modulePath}` : modulePath);
2160
- moduleCache[moduleName] = module;
2161
- return {
2162
- module,
2163
- useDefault: false
2164
- };
2440
+ case ModuleProtocol.Https: {
2441
+ const result = await handleHttpsProtocol(parsed.identifier, executionDirectory, maxSizeMb, cacheDirectory, envVars?.extensionsCacheTtlHours, envVars?.extensionsForceRefresh);
2442
+ resolvedPath = result.resolvedPath;
2443
+ break;
2444
+ }
2445
+ case ModuleProtocol.Http: {
2446
+ throw new GeneralError("node", "insecureProtocol", { protocol: ModuleProtocol.Http });
2447
+ }
2448
+ case ModuleProtocol.Local: {
2449
+ let localFilename = path.resolve(moduleName);
2450
+ let exists = await fileExists(localFilename);
2451
+ if (!exists) {
2452
+ localFilename = path.resolve(executionDirectory, moduleName);
2453
+ exists = await fileExists(localFilename);
2454
+ }
2455
+ if (exists) {
2456
+ resolvedPath = localFilename;
2457
+ }
2458
+ break;
2459
+ }
2460
+ case ModuleProtocol.Default: {
2461
+ try {
2462
+ const npmRoot = execSync("npm root").toString().trim().replace(/\\/g, "/");
2463
+ const packagePath = path.resolve(npmRoot, moduleName);
2464
+ const mainFile = await resolvePackageEntryPoint(packagePath, moduleName);
2465
+ const modulePath = path.resolve(packagePath, mainFile);
2466
+ const exists = await fileExists(modulePath);
2467
+ if (exists) {
2468
+ resolvedPath = modulePath;
2469
+ break;
2470
+ }
2471
+ }
2472
+ catch {
2473
+ // Continue to fallback resolution
2474
+ }
2475
+ // Fallback: resolve from npm protocol cache directory (installed via handleNpmProtocol)
2476
+ try {
2477
+ const cacheNpmRoot = path.resolve(getExtensionsCacheDir(executionDirectory, ModuleProtocol.Npm, cacheDirectory), "node_modules");
2478
+ const packagePath = path.resolve(cacheNpmRoot, moduleName);
2479
+ const mainFile = await resolvePackageEntryPoint(packagePath, moduleName);
2480
+ const modulePath = path.resolve(packagePath, mainFile);
2481
+ const exists = await fileExists(modulePath);
2482
+ if (exists) {
2483
+ resolvedPath = modulePath;
2484
+ }
2485
+ }
2486
+ catch {
2487
+ // No cached resolution either; fall through
2488
+ }
2489
+ break;
2165
2490
  }
2166
2491
  }
2167
- catch {
2168
- // We just fallback to default handling if not possible
2492
+ // Common module loading and caching logic
2493
+ if (resolvedPath) {
2494
+ const module = await import(createModuleImportUrl(resolvedPath));
2495
+ moduleCache[moduleName] = module;
2496
+ return {
2497
+ module,
2498
+ useDefault: false
2499
+ };
2169
2500
  }
2170
- // We don't appear to be able to manually resolve this module
2171
- // So we let the default handling take care of it
2172
- // This will allow built-in modules and regular node_modules to load as normal
2173
2501
  return {
2502
+ module: undefined,
2174
2503
  useDefault: true
2175
2504
  };
2176
2505
  });
2177
2506
  }
2178
2507
 
2179
- export { ATTESTATION_VERIFICATION_METHOD_ID, AUTH_SIGNING_KEY_ID, BLOB_STORAGE_ENCRYPTION_KEY_ID, IMMUTABLE_PROOF_VERIFICATION_METHOD_ID, NodeFeatures, SYNCHRONISED_STORAGE_BLOB_STORAGE_ENCRYPTION_KEY_ID, VC_AUTHENTICATION_VERIFICATION_METHOD_ID, bootstrap, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, bootstrapSynchronisedStorage, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, directoryExists, extensionsConfiguration, extensionsInitialiseEngine, extensionsInitialiseEngineServer, fileExists, getExecutionDirectory, getFeatures, getFiles, getSubFolders, initialiseLocales, loadJsonFile, loadTextFile, overrideModuleImport, run, shutdownExtensions, start };
2508
+ export { ATTESTATION_VERIFICATION_METHOD_ID, AUTH_SIGNING_KEY_ID, BLOB_STORAGE_ENCRYPTION_KEY_ID, IMMUTABLE_PROOF_VERIFICATION_METHOD_ID, ModuleProtocol, NodeFeatures, SYNCHRONISED_STORAGE_BLOB_STORAGE_ENCRYPTION_KEY_ID, VC_AUTHENTICATION_VERIFICATION_METHOD_ID, bootstrap, bootstrapAuth, bootstrapBlobEncryption, bootstrapImmutableProofMethod, bootstrapNodeIdentity, bootstrapNodeUser, bootstrapSynchronisedStorage, buildConfiguration, buildEngineConfiguration, buildEngineServerConfiguration, createModuleImportUrl, directoryExists, extensionsConfiguration, extensionsInitialiseEngine, extensionsInitialiseEngineServer, fileExists, getExecutionDirectory, getExtensionsCacheDir, getFeatures, getFiles, getSubFolders, handleHttpsProtocol, handleNpmProtocol, hashUrl, initialiseLocales, isCacheExpired, loadJsonFile, loadTextFile, overrideModuleImport, parseModuleProtocol, resolvePackageEntryPoint, run, shutdownExtensions, start };
@@ -3,11 +3,15 @@ export * from "./builders/engineEnvBuilder";
3
3
  export * from "./builders/engineServerEnvBuilder";
4
4
  export * from "./builders/extensionsBuilder";
5
5
  export * from "./defaults";
6
+ export * from "./models/ICacheMetadata";
6
7
  export * from "./models/IEngineEnvironmentVariables";
7
8
  export * from "./models/IEngineServerEnvironmentVariables";
9
+ export * from "./models/IModuleProtocol";
8
10
  export * from "./models/INodeEngineConfig";
9
11
  export * from "./models/INodeEnvironmentVariables";
10
12
  export * from "./models/INodeOptions";
13
+ export * from "./models/IProtocolHandlerResult";
14
+ export * from "./models/moduleProtocol";
11
15
  export * from "./models/nodeExtensionMethods";
12
16
  export * from "./models/nodeFeatures";
13
17
  export * from "./node";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Metadata for cached HTTPS extensions.
3
+ */
4
+ export interface ICacheMetadata {
5
+ /**
6
+ * Timestamp when the file was downloaded.
7
+ */
8
+ downloadedAt: number;
9
+ /**
10
+ * Original URL of the cached file.
11
+ */
12
+ url: string;
13
+ /**
14
+ * Size of the cached file in bytes.
15
+ */
16
+ size: number;
17
+ }
@@ -0,0 +1,18 @@
1
+ import type { ModuleProtocol } from "./moduleProtocol";
2
+ /**
3
+ * The parsed module protocol information.
4
+ */
5
+ export interface IModuleProtocol {
6
+ /**
7
+ * The protocol type.
8
+ */
9
+ protocol: ModuleProtocol;
10
+ /**
11
+ * The identifier after the protocol (or the original if no protocol).
12
+ */
13
+ identifier: string;
14
+ /**
15
+ * The original module string.
16
+ */
17
+ original: string;
18
+ }
@@ -25,4 +25,29 @@ export interface INodeEnvironmentVariables extends IEngineServerEnvironmentVaria
25
25
  * If the node-user feature is enabled, this will be the password of the user, if empty it will be randomly generated.
26
26
  */
27
27
  password?: string;
28
+ /**
29
+ * Maximum size in MB for HTTPS extensions downloads.
30
+ * @default 10
31
+ */
32
+ extensionsMaxSizeMb?: number;
33
+ /**
34
+ * Whether to clear the extensions cache on startup.
35
+ * @default false
36
+ */
37
+ extensionsClearCache?: boolean;
38
+ /**
39
+ * Custom directory for extensions cache storage.
40
+ * @default ".tmp"
41
+ */
42
+ extensionsCacheDirectory?: string;
43
+ /**
44
+ * TTL in hours for HTTPS extensions cache.
45
+ * @default 24
46
+ */
47
+ extensionsCacheTtlHours?: number;
48
+ /**
49
+ * Force refresh of all cached extensions.
50
+ * @default false
51
+ */
52
+ extensionsForceRefresh?: boolean;
28
53
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The result from a protocol handler.
3
+ */
4
+ export interface IProtocolHandlerResult {
5
+ /**
6
+ * The resolved path to the module file.
7
+ */
8
+ resolvedPath: string;
9
+ /**
10
+ * Whether the module was cached.
11
+ */
12
+ cached: boolean;
13
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The protocol types for modules.
3
+ */
4
+ export declare const ModuleProtocol: {
5
+ /**
6
+ * Local module (starts with . or / or file://).
7
+ */
8
+ readonly Local: "local";
9
+ /**
10
+ * NPM package (starts with npm:).
11
+ */
12
+ readonly Npm: "npm";
13
+ /**
14
+ * HTTPS URL (starts with https://).
15
+ */
16
+ readonly Https: "https";
17
+ /**
18
+ * HTTP URL (starts with http://).
19
+ */
20
+ readonly Http: "http";
21
+ /**
22
+ * Default/standard module resolution.
23
+ */
24
+ readonly Default: "default";
25
+ };
26
+ /**
27
+ * The protocol type for a module.
28
+ */
29
+ export type ModuleProtocol = (typeof ModuleProtocol)[keyof typeof ModuleProtocol];
@@ -25,7 +25,8 @@ export declare function buildConfiguration(processEnv: {
25
25
  nodeEngineConfig: INodeEngineConfig;
26
26
  }>;
27
27
  /**
28
- * Override module imports to use local files where possible.
28
+ * Override module imports to support protocol-based loading (npm:, https:) and local files.
29
29
  * @param executionDirectory The execution directory for resolving local module paths.
30
+ * @param envVars The environment variables containing extension configuration (optional, uses defaults if not provided).
30
31
  */
31
- export declare function overrideModuleImport(executionDirectory: string): void;
32
+ export declare function overrideModuleImport(executionDirectory: string, envVars?: INodeEnvironmentVariables): void;