@harperfast/harper 5.0.0-beta.7 → 5.0.0-beta.8

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 (38) hide show
  1. package/bin/cliOperations.js +3 -3
  2. package/components/ApplicationScope.ts +1 -0
  3. package/components/componentLoader.ts +4 -1
  4. package/dist/bin/cliOperations.js +3 -3
  5. package/dist/bin/cliOperations.js.map +1 -1
  6. package/dist/components/ApplicationScope.d.ts +1 -0
  7. package/dist/components/ApplicationScope.js +1 -0
  8. package/dist/components/ApplicationScope.js.map +1 -1
  9. package/dist/components/componentLoader.js +2 -1
  10. package/dist/components/componentLoader.js.map +1 -1
  11. package/dist/resources/DatabaseTransaction.d.ts +2 -1
  12. package/dist/resources/DatabaseTransaction.js +71 -35
  13. package/dist/resources/DatabaseTransaction.js.map +1 -1
  14. package/dist/resources/Table.d.ts +6 -6
  15. package/dist/resources/Table.js +15 -10
  16. package/dist/resources/Table.js.map +1 -1
  17. package/dist/security/jsLoader.js +123 -90
  18. package/dist/security/jsLoader.js.map +1 -1
  19. package/dist/server/REST.js +13 -4
  20. package/dist/server/REST.js.map +1 -1
  21. package/dist/server/http.d.ts +1 -0
  22. package/dist/server/http.js +5 -0
  23. package/dist/server/http.js.map +1 -1
  24. package/package.json +3 -3
  25. package/resources/DatabaseTransaction.ts +67 -32
  26. package/resources/Table.ts +29 -10
  27. package/security/jsLoader.ts +149 -101
  28. package/server/REST.ts +12 -2
  29. package/server/http.ts +4 -1
  30. package/studio/web/assets/{index-ClD_q6ya.js → index-CXQsBaYq.js} +5 -5
  31. package/studio/web/assets/{index-ClD_q6ya.js.map → index-CXQsBaYq.js.map} +1 -1
  32. package/studio/web/assets/{index.lazy-CXzU1gVu.js → index.lazy-C3Ejfvna.js} +2 -2
  33. package/studio/web/assets/{index.lazy-CXzU1gVu.js.map → index.lazy-C3Ejfvna.js.map} +1 -1
  34. package/studio/web/assets/{profile-DCNVg5yY.js → profile-BbbbWJCN.js} +2 -2
  35. package/studio/web/assets/{profile-DCNVg5yY.js.map → profile-BbbbWJCN.js.map} +1 -1
  36. package/studio/web/assets/{status-CoGlcjSB.js → status-CFe85l8C.js} +2 -2
  37. package/studio/web/assets/{status-CoGlcjSB.js.map → status-CFe85l8C.js.map} +1 -1
  38. package/studio/web/index.html +1 -1
@@ -56,6 +56,9 @@ export async function scopedImport(filePath: string | URL, scope?: ApplicationSc
56
56
  }
57
57
  overridableProperty(Promise.prototype, 'then');
58
58
  overridableProperty(Date, 'now');
59
+ for (let name of ['get', 'set', 'has', 'delete', 'clear', 'forEach', 'entries', 'keys', 'values']) {
60
+ overridableProperty(Map.prototype, name);
61
+ }
59
62
  for (let Intrinsic of [
60
63
  Object,
61
64
  Array,
@@ -121,12 +124,13 @@ export async function scopedImport(filePath: string | URL, scope?: ApplicationSc
121
124
 
122
125
  let amaro: typeof import('amaro') | undefined;
123
126
  /**
124
- * Strip TypeScript types using the amaro library (what Node.js uses internally)
125
- * Falls back to regex-based stripping if amaro is not available
127
+ * Strip TypeScript types synchronously using the amaro library (what Node.js uses internally)
126
128
  */
127
- async function stripTypeScriptTypes(source: string): Promise<string> {
129
+ function stripTypeScriptTypes(source: string): string {
128
130
  // Use amaro - the library that Node.js uses internally for type stripping
129
- amaro = await import('amaro');
131
+ if (!amaro) {
132
+ amaro = require('amaro');
133
+ }
130
134
  return amaro.transformSync(source, { mode: 'strip-only' }).code;
131
135
  }
132
136
 
@@ -146,13 +150,27 @@ function parseJsonModule(source: string, url: string): any {
146
150
  * Load a module using Node's vm.Module API with (not really secure) sandboxing
147
151
  */
148
152
  async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
149
- const moduleCache = new Map<string, Promise<SourceTextModule | SyntheticModule>>();
150
- const linkingPromises = new Map<string, Promise<void>>();
151
- const cjsCache = new Map<string, { exports: any }>();
152
-
153
- // Create a secure context with limited globals
154
- const contextObject = getGlobalObject(scope);
155
- const context = createContext(contextObject);
153
+ // we want to retain the same module caches across any loading with the application scope
154
+ let moduleCaches = scope.moduleCache as {
155
+ moduleCache: Map<string, SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule>>;
156
+ linkingPromises: Map<string, Promise<void>>;
157
+ cjsCache: Map<string, { exports: any }>;
158
+ contextObject: any;
159
+ context: any;
160
+ };
161
+ if (!moduleCaches) {
162
+ // if they haven't been initialized, do so now
163
+ const contextObject = getGlobalObject(scope, true);
164
+ moduleCaches = scope.moduleCache = {
165
+ moduleCache: new Map(),
166
+ linkingPromises: new Map(),
167
+ cjsCache: new Map(),
168
+ // Create a secure context with limited globals
169
+ contextObject,
170
+ context: createContext(contextObject),
171
+ };
172
+ }
173
+ const { moduleCache, linkingPromises, cjsCache, contextObject, context } = moduleCaches;
156
174
 
157
175
  /**
158
176
  * Resolve module specifier to absolute URL
@@ -298,9 +316,11 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
298
316
  }
299
317
 
300
318
  /**
301
- * Linker function for module resolution during instantiation
319
+ * Linker function for module resolution during instantiation.
320
+ * This is synchronous because Node's module.link() requires the linker
321
+ * to return modules synchronously.
302
322
  */
303
- async function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
323
+ function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
304
324
  const resolvedUrl = resolveModule(specifier, referencingModule.identifier);
305
325
 
306
326
  // Determine if we should use VM containment for this module
@@ -316,15 +336,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
316
336
  }
317
337
  }
318
338
 
319
- // Return the module immediately (even if not yet linked) to support circular dependencies
320
- return await getOrCreateModule(resolvedUrl, useContainment);
339
+ // Return the module
340
+ return getOrCreateModule(resolvedUrl, useContainment);
321
341
  }
322
342
 
323
- async function getOrCreateModule(
343
+ function getOrCreateModule(
324
344
  url: string,
325
345
  usePrivateGlobal: boolean
326
- ): Promise<SourceTextModule | SyntheticModule> {
327
- // Check cache first - return cached module immediately (even if not linked yet)
346
+ ): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
347
+ // Check if module is already created
328
348
  if (moduleCache.has(url)) {
329
349
  return moduleCache.get(url)!;
330
350
  }
@@ -345,8 +365,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
345
365
  // Only link/evaluate once per module
346
366
  if (!linkingPromises.has(url)) {
347
367
  const linkingPromise = (async () => {
348
- await module.link(linker);
349
- await module.evaluate();
368
+ // Check module status - only link if it's 'unlinked'
369
+ // Status can be: 'unlinked', 'linking', 'linked', 'evaluating', 'evaluated'
370
+ if (module.status === 'unlinked') {
371
+ await module.link(linker);
372
+ }
373
+ // Only evaluate if not already evaluated
374
+ if (module.status === 'linked') {
375
+ await module.evaluate();
376
+ }
350
377
  })();
351
378
  linkingPromises.set(url, linkingPromise);
352
379
  }
@@ -357,94 +384,107 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
357
384
  return module;
358
385
  }
359
386
  /**
360
- * Create a module from URL without linking or evaluating
387
+ * Create a SyntheticModule from exported object
361
388
  */
362
- async function createModule(url: string, usePrivateGlobal: boolean): Promise<SourceTextModule | SyntheticModule> {
363
- let module: SourceTextModule | SyntheticModule;
364
-
365
- // Handle special built-in modules
366
- if (url === 'harper') {
367
- let harperExports = getHarperExports(scope);
368
- module = new SyntheticModule(
369
- Object.keys(harperExports),
370
- function () {
371
- for (let key in harperExports) {
372
- this.setExport(key, harperExports[key]);
373
- }
374
- },
375
- { identifier: url, context }
376
- );
377
- } else if (url.startsWith('file://') && usePrivateGlobal) {
378
- checkAllowedModulePath(url, scope.verifyPath);
379
- let source = await readFile(new URL(url), { encoding: 'utf-8' });
380
-
381
- // Handle JSON modules as a SyntheticModule with a default export.
382
- // JSON imports only support default exports per the ESM spec.
383
- if (url.endsWith('.json')) {
384
- const jsonData = parseJsonModule(source, url);
385
- module = new SyntheticModule(
386
- ['default'],
387
- function () {
388
- this.setExport('default', jsonData);
389
- },
390
- { identifier: url, context }
391
- );
392
- } else {
393
- // Strip TypeScript types if this is a .ts file
394
- if (url.endsWith('.ts') || url.endsWith('.tsx')) {
395
- source = await stripTypeScriptTypes(source);
389
+ function createSyntheticModule(url: string, exportedObject: any): SyntheticModule {
390
+ const exportNames = Object.keys(exportedObject);
391
+ return new SyntheticModule(
392
+ exportNames,
393
+ function () {
394
+ for (const key of exportNames) {
395
+ this.setExport(key, exportedObject[key]);
396
396
  }
397
+ },
398
+ { identifier: url, context }
399
+ );
400
+ }
397
401
 
398
- // Try CJS first since it will fail fast with clear syntax errors on ESM syntax
399
- try {
400
- module = loadCJSModule(url, source, usePrivateGlobal);
401
- } catch {
402
- // If CJS loading fails (likely due to ESM syntax like import/export), try ESM
403
- try {
404
- module = new SourceTextModule(source, {
405
- identifier: url,
406
- context,
407
- initializeImportMeta(meta) {
408
- meta.url = url;
409
- },
410
- async importModuleDynamically(specifier: string) {
411
- const resolvedUrl = resolveModule(specifier, url);
412
- const dynamicModule = await loadModuleWithCache(resolvedUrl, true);
413
- return dynamicModule;
414
- },
415
- });
416
- } catch (esmErr) {
417
- // Both failed - throw the ESM error as it's likely more relevant
418
- throw esmErr;
419
- }
420
- }
421
- }
422
- } else {
423
- const replacedModule = checkAllowedModulePath(url, scope.verifyPath);
424
- // For Node.js built-in modules (node:) and npm packages without dependency containment
425
- // Always try require first to properly handle CJS modules with named exports
426
- // Fall back to dynamic import for ESM packages
427
- let importedModule = replacedModule ?? (await import(url));
428
- const cjsModule = importedModule['module.exports'];
429
- if (cjsModule) {
430
- // back-compat import
431
- importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
432
- }
433
- const exportNames = Object.keys(importedModule);
434
- module = new SyntheticModule(
435
- exportNames,
402
+ /**
403
+ * Normalize imported module to ensure it has proper exports including default
404
+ */
405
+ function normalizeImportedModule(importedModule: any): any {
406
+ const cjsModule = importedModule['module.exports'];
407
+ if (cjsModule) {
408
+ // back-compat import
409
+ importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
410
+ }
411
+ // Ensure there's a default export for ESM imports that expect it
412
+ if (!importedModule.default) {
413
+ importedModule = { default: importedModule, ...importedModule };
414
+ }
415
+ return importedModule;
416
+ }
417
+
418
+ /**
419
+ * Create a SourceTextModule or SyntheticModule from source code
420
+ */
421
+ function createModuleFromSource(
422
+ url: string,
423
+ source: string,
424
+ usePrivateGlobal: boolean
425
+ ): SourceTextModule | SyntheticModule {
426
+ // Handle JSON modules
427
+ if (url.endsWith('.json')) {
428
+ const jsonData = parseJsonModule(source, url);
429
+ return new SyntheticModule(
430
+ ['default'],
436
431
  function () {
437
- for (const key of exportNames) {
438
- this.setExport(key, importedModule[key]);
439
- }
432
+ this.setExport('default', jsonData);
440
433
  },
441
434
  { identifier: url, context }
442
435
  );
443
436
  }
444
437
 
445
- return module;
438
+ // Strip TypeScript types if this is a .ts file
439
+ if (url.endsWith('.ts') || url.endsWith('.tsx')) {
440
+ source = stripTypeScriptTypes(source);
441
+ }
442
+
443
+ // Try CJS first since it will fail fast with clear syntax errors on ESM syntax
444
+ try {
445
+ return loadCJSModule(url, source, usePrivateGlobal);
446
+ } catch {
447
+ // If CJS loading fails (likely due to ESM syntax like import/export), try ESM
448
+ return new SourceTextModule(source, {
449
+ identifier: url,
450
+ context,
451
+ initializeImportMeta(meta) {
452
+ meta.url = url;
453
+ },
454
+ importModuleDynamically(specifier: string) {
455
+ const resolvedUrl = resolveModule(specifier, url);
456
+ const useContainment = specifier.startsWith('.') || scope.dependencyContainment !== false;
457
+ return loadModuleWithCache(resolvedUrl, useContainment);
458
+ },
459
+ });
460
+ }
446
461
  }
447
462
 
463
+ /**
464
+ * Create a module from URL without linking or evaluating (async version for initial load)
465
+ */
466
+ function createModule(
467
+ url: string,
468
+ usePrivateGlobal: boolean
469
+ ): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
470
+ // Handle special built-in modules
471
+ if (url === 'harper') {
472
+ return createSyntheticModule(url, getHarperExports(scope));
473
+ }
474
+
475
+ if (url.startsWith('file://') && usePrivateGlobal) {
476
+ checkAllowedModulePath(url, scope.verifyPath);
477
+ const source = readFileSync(new URL(url), { encoding: 'utf-8' });
478
+ return createModuleFromSource(url, source, usePrivateGlobal);
479
+ }
480
+
481
+ // For Node.js built-in modules (node:) and npm packages without dependency containment
482
+ const replacedModule = checkAllowedModulePath(url, scope.verifyPath);
483
+ if (replacedModule) {
484
+ return createSyntheticModule(url, normalizeImportedModule(replacedModule));
485
+ }
486
+ return import(url).then((importedModule) => createSyntheticModule(url, normalizeImportedModule(importedModule)));
487
+ }
448
488
  // Load the entry module
449
489
  const entryModule = await loadModuleWithCache(moduleUrl, true);
450
490
 
@@ -490,8 +530,8 @@ async function getCompartment(scope: ApplicationScope, globals) {
490
530
  },
491
531
  };
492
532
  } else if (moduleSpecifier.startsWith('file:') && !moduleSpecifier.includes('node_modules')) {
493
- const moduleText = await readFile(new URL(moduleSpecifier), { encoding: 'utf-8' });
494
- // Handle JSON files in comparttment mode the same way as in VM mode
533
+ let moduleText = await readFile(new URL(moduleSpecifier), { encoding: 'utf-8' });
534
+ // Handle JSON files in compartment mode the same way as in VM mode
495
535
  if (moduleSpecifier.endsWith('.json')) {
496
536
  const jsonData = parseJsonModule(moduleText, moduleSpecifier);
497
537
  return {
@@ -502,6 +542,10 @@ async function getCompartment(scope: ApplicationScope, globals) {
502
542
  },
503
543
  };
504
544
  }
545
+ // Strip TypeScript types if this is a .ts file
546
+ if (moduleSpecifier.endsWith('.ts') || moduleSpecifier.endsWith('.tsx')) {
547
+ moduleText = stripTypeScriptTypes(moduleText);
548
+ }
505
549
  return new StaticModuleRecord(moduleText, moduleSpecifier);
506
550
  } else {
507
551
  checkAllowedModulePath(moduleSpecifier, scope.verifyPath);
@@ -536,6 +580,9 @@ function secureOnlyFetch(resource, options) {
536
580
  return fetch(resource, options);
537
581
  }
538
582
 
583
+ // These globals need to match the literals produced in the VM context
584
+ const contextualizedJSGlobals = ['Object', 'Array', 'Function', 'globalThis'];
585
+
539
586
  let defaultJSGlobalNames: string[];
540
587
  // get the global variable names that are intrinsically present in a VM context (so we don't override them)
541
588
  function getDefaultJSGlobalNames() {
@@ -551,12 +598,13 @@ function getDefaultJSGlobalNames() {
551
598
  /**
552
599
  * Get the set of global variables that should be available to modules that run in scoped compartments/contexts.
553
600
  */
554
- function getGlobalObject(scope: ApplicationScope) {
601
+ function getGlobalObject(scope: ApplicationScope, copyIntrinsics = false) {
555
602
  const appGlobal = {};
556
603
  // create the new global object, assigning all the global variables from this global
557
604
  // except those that will be natural intrinsics of the new VM
605
+ const globalsToExclude = copyIntrinsics ? contextualizedJSGlobals : getDefaultJSGlobalNames();
558
606
  for (let name of Object.getOwnPropertyNames(global)) {
559
- if (getDefaultJSGlobalNames().includes(name)) continue;
607
+ if (globalsToExclude.includes(name)) continue;
560
608
  appGlobal[name] = global[name];
561
609
  }
562
610
  // now assign Harper scope-specific variables
package/server/REST.ts CHANGED
@@ -173,8 +173,18 @@ async function http(request: Context & Request, nextHandler) {
173
173
  responseData.headers = responseHeaders;
174
174
  // if no body, look for provided data to serialize
175
175
  if (!responseData.body) {
176
- if ('data' in responseData) responseData.body = serialize(responseData.data, request, responseData);
177
- else responseData.body = serialize(responseData, request, responseData);
176
+ let body: any;
177
+ if ('data' in responseData) {
178
+ // a standard Response object does not have a setter for body, so we force it
179
+ body = serialize(responseData.data, request, responseData);
180
+ } else if (responseData.body === undefined) {
181
+ // if there is really no body, serialize this object into the body. Note that `new Response()` creates a response
182
+ // with a null body, and will not fall into this branch
183
+ body = serialize(responseData, request, responseData);
184
+ }
185
+ if (body) {
186
+ responseData = { status: responseData.status, headers: responseData.headers, body };
187
+ }
178
188
  }
179
189
  responseData.status ??= status ?? 200;
180
190
  return responseData;
package/server/http.ts CHANGED
@@ -35,6 +35,7 @@ const httpServers = {},
35
35
  httpChain = {},
36
36
  httpResponders = [];
37
37
  let httpOptions: HttpOptions = {};
38
+ export const universalHeaders: [string, string][] = [];
38
39
 
39
40
  export function handleApplication(scope: Scope) {
40
41
  httpOptions = scope.options.getAll() as HttpOptions;
@@ -269,7 +270,9 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
269
270
  if (!response.headers?.set) {
270
271
  response.headers = new Headers(response.headers);
271
272
  }
272
-
273
+ for (let [key, value] of universalHeaders) {
274
+ response.headers.set(key, value);
275
+ }
273
276
  if (response.status === -1) {
274
277
  // This means the HDB stack didn't handle the request, and we can then cascade the request
275
278
  // to the server-level handler, forming the bridge to the slower legacy fastify framework that expects