@portel/photon 1.34.0 → 1.34.2

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.
@@ -27,7 +27,7 @@ import { getDefaultContext } from '../context.js';
27
27
  import { EnvStore, resolvePhotonNamespace } from '../context-store.js';
28
28
  import { createLogger } from '../shared/logger.js';
29
29
  import { getErrorMessage } from '../shared/error-handler.js';
30
- import { timingSafeEqual, readBody, SimpleRateLimiter, ipInAllowlist, parseAllowlistEnv, } from '../shared/security.js';
30
+ import { timingSafeEqual, readBody, SimpleRateLimiter, ipInAllowlist, parseAllowlistEnv, getCorsOrigin, } from '../shared/security.js';
31
31
  import { audit } from '../shared/audit.js';
32
32
  import { recordExecution, previewResult, readExecutionHistory, sweepAllBases as sweepExecutionHistoryBases, } from './execution-history.js';
33
33
  import { WorkerManager } from './worker-manager.js';
@@ -1734,7 +1734,9 @@ function watchBaseForProactiveMetadata(basePath, _isDefaultBase) {
1734
1734
  watchDebounce.set(debounceKey, timer);
1735
1735
  };
1736
1736
  try {
1737
- const watcher = safeWatchDir(basePath, (_eventType, filename) => onChange(filename));
1737
+ const watcher = safeWatchDir(basePath, (_eventType, filename) => onChange(filename), {
1738
+ bunPollMaxEntries: BUN_DIR_POLL_MAX_ENTRIES,
1739
+ });
1738
1740
  baseDirWatchers.set(basePath, watcher);
1739
1741
  }
1740
1742
  catch (err) {
@@ -1976,7 +1978,9 @@ function startWebhookServer(port) {
1976
1978
  return;
1977
1979
  webhookServer = http.createServer((req, res) => {
1978
1980
  void (async () => {
1979
- res.setHeader('Access-Control-Allow-Origin', '*');
1981
+ const corsOrigin = getCorsOrigin(req);
1982
+ if (corsOrigin)
1983
+ res.setHeader('Access-Control-Allow-Origin', corsOrigin);
1980
1984
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1981
1985
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Webhook-Secret, X-Photon-Name');
1982
1986
  if (req.method === 'OPTIONS') {
@@ -2968,7 +2972,7 @@ async function handleRequest(request, socket) {
2968
2972
  const key = compositeKey(photonName, request.workingDir);
2969
2973
  const sessionManager = sessionManagers.get(key);
2970
2974
  if (sessionManager) {
2971
- await sessionManager.clearInstances();
2975
+ await sessionManager.clearInstances('clear-instances');
2972
2976
  }
2973
2977
  return { type: 'result', id: request.id, success: true, data: { cleared: !!sessionManager } };
2974
2978
  }
@@ -4638,6 +4642,7 @@ async function applyPatchToInstance(instance, photonName, ops, workingDir) {
4638
4642
  */
4639
4643
  const IS_BUN = !!process.versions.bun;
4640
4644
  const POLL_INTERVAL_MS = 2000; // 2s — good balance between latency and CPU
4645
+ const BUN_DIR_POLL_MAX_ENTRIES = parseNonNegativeEnvInt('PHOTON_BUN_DIR_POLL_MAX_ENTRIES', 512);
4641
4646
  /** Track active poll timers so they can be cleaned up on shutdown */
4642
4647
  const pollTimers = new Set();
4643
4648
  function safeWatchFile(filePath, callback) {
@@ -4686,12 +4691,37 @@ function safeWatchFile(filePath, callback) {
4686
4691
  },
4687
4692
  };
4688
4693
  }
4689
- function safeWatchDir(dirPath, callback) {
4694
+ function countDirEntries(dirPath) {
4695
+ try {
4696
+ return fs.readdirSync(dirPath).length;
4697
+ }
4698
+ catch {
4699
+ return null;
4700
+ }
4701
+ }
4702
+ function shouldSkipBunDirPoll(dirPath, maxEntries) {
4703
+ if (!IS_BUN || maxEntries === 0)
4704
+ return false;
4705
+ const count = countDirEntries(dirPath);
4706
+ if (count === null || count <= maxEntries)
4707
+ return false;
4708
+ logger.warn('Skipping Bun directory polling watcher for oversized directory', {
4709
+ dir: dirPath,
4710
+ entries: count,
4711
+ maxEntries,
4712
+ });
4713
+ return true;
4714
+ }
4715
+ function safeWatchDir(dirPath, callback, options = {}) {
4690
4716
  if (!IS_BUN) {
4691
4717
  const w = fs.watch(dirPath, (eventType, filename) => callback(eventType, filename));
4692
4718
  return w;
4693
4719
  }
4694
4720
  // Bun fallback: readdir snapshot diffing
4721
+ if (options.bunPollMaxEntries !== undefined &&
4722
+ shouldSkipBunDirPoll(dirPath, options.bunPollMaxEntries)) {
4723
+ return { close() { } };
4724
+ }
4695
4725
  let prevEntries = new Map(); // name -> mtimeMs
4696
4726
  try {
4697
4727
  for (const entry of fs.readdirSync(dirPath)) {
@@ -4836,7 +4866,7 @@ function watchPhotonFile(photonName, photonPath) {
4836
4866
  for (const key of keysToDelete) {
4837
4867
  const manager = sessionManagers.get(key);
4838
4868
  if (manager)
4839
- await manager.clearInstances();
4869
+ await manager.clearInstances('photon-deleted');
4840
4870
  sessionManagers.delete(key);
4841
4871
  photonPaths.delete(key);
4842
4872
  workingDirs.delete(key);
@@ -4914,6 +4944,22 @@ function unwatchPhotonFile(watchPath) {
4914
4944
  watchDebounce.delete(watchPath);
4915
4945
  }
4916
4946
  }
4947
+ function unwatchPhotonPath(photonPath) {
4948
+ let watchPath = photonPath;
4949
+ try {
4950
+ watchPath = fs.realpathSync(photonPath);
4951
+ }
4952
+ catch {
4953
+ // Symlink target may already be gone; fall back to caller path.
4954
+ }
4955
+ unwatchPhotonFile(watchPath);
4956
+ const debounceKey = path.resolve(photonPath);
4957
+ const timer = watchDebounce.get(debounceKey);
4958
+ if (timer) {
4959
+ clearTimeout(timer);
4960
+ watchDebounce.delete(debounceKey);
4961
+ }
4962
+ }
4917
4963
  /**
4918
4964
  * Detect whether a missing workingDir was renamed (moved) or deleted.
4919
4965
  *
@@ -5041,7 +5087,7 @@ function watchWorkingDir(workingDir) {
5041
5087
  for (const key of deletedDirKeys) {
5042
5088
  const manager = sessionManagers.get(key);
5043
5089
  if (manager)
5044
- await manager.clearInstances();
5090
+ await manager.clearInstances('working-dir-deleted');
5045
5091
  sessionManagers.delete(key);
5046
5092
  photonPaths.delete(key);
5047
5093
  workingDirs.delete(key);
@@ -5108,7 +5154,7 @@ function watchStateDir(workingDir) {
5108
5154
  const key = compositeKey(filename, workingDir);
5109
5155
  const manager = sessionManagers.get(key);
5110
5156
  if (manager) {
5111
- await manager.clearInstances();
5157
+ await manager.clearInstances('state-dir-deleted');
5112
5158
  }
5113
5159
  })();
5114
5160
  }, 150);
@@ -5602,13 +5648,11 @@ function startupWatchPhotonDir(photonDir, defaultBase) {
5602
5648
  if (photonPaths.has(photonKey) && !fs.existsSync(filePath)) {
5603
5649
  photonPaths.delete(photonKey);
5604
5650
  // fileWatchers is keyed by file path, not composite key
5605
- const watcher = fileWatchers.get(filePath);
5606
- if (watcher) {
5607
- watcher.close();
5608
- fileWatchers.delete(filePath);
5609
- }
5651
+ unwatchPhotonPath(filePath);
5610
5652
  logger.info('Photon file removed', { photonName, path: filePath });
5611
5653
  }
5654
+ }, {
5655
+ bunPollMaxEntries: BUN_DIR_POLL_MAX_ENTRIES,
5612
5656
  });
5613
5657
  if (typeof dirWatcher.on === 'function') {
5614
5658
  dirWatcher.on('error', () => { }); // Non-fatal