@cordy/electro 1.2.16 → 1.2.18

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
@@ -774,6 +774,7 @@ declare class Runtime {
774
774
  private readonly windowManager;
775
775
  private readonly viewManager;
776
776
  private readonly eventBridge;
777
+ private readonly ipcBridge;
777
778
  constructor(config?: RuntimeConfig);
778
779
  register(features: FeatureConfig<any> | FeatureConfig<any>[]): void;
779
780
  start(): Promise<void>;
package/dist/index.mjs CHANGED
@@ -864,6 +864,92 @@ var FeatureManager = class {
864
864
  }
865
865
  };
866
866
 
867
+ //#endregion
868
+ //#region src/core/ipc/bridge.ts
869
+ /**
870
+ * Registers invoke handlers for exposed service methods and enforces
871
+ * per-view feature access at runtime.
872
+ */
873
+ var IpcBridge = class {
874
+ viewFeatures = /* @__PURE__ */ new Map();
875
+ ipcMainOverride;
876
+ ipcMain = null;
877
+ registeredChannels = /* @__PURE__ */ new Set();
878
+ constructor(featureManager, viewManager, viewRegistry, options = {}) {
879
+ this.featureManager = featureManager;
880
+ this.viewManager = viewManager;
881
+ this.ipcMainOverride = options.ipcMain ?? null;
882
+ for (const entry of viewRegistry) {
883
+ if (!entry.hasRenderer) continue;
884
+ this.viewFeatures.set(entry.id, new Set(entry.features ?? []));
885
+ }
886
+ }
887
+ async start() {
888
+ if (this.ipcMain) return;
889
+ const ipcMain = this.ipcMainOverride ?? await this.resolveIpcMain();
890
+ if (!ipcMain) return;
891
+ this.ipcMain = ipcMain;
892
+ this.registerHandlers();
893
+ }
894
+ stop() {
895
+ if (!this.ipcMain) return;
896
+ for (const channel of this.registeredChannels) this.ipcMain.removeHandler(channel);
897
+ this.registeredChannels.clear();
898
+ this.ipcMain = null;
899
+ }
900
+ async resolveIpcMain() {
901
+ if (!process.versions.electron) return null;
902
+ try {
903
+ const candidate = (await import("electron")).ipcMain;
904
+ if (!candidate) return null;
905
+ if (typeof candidate.handle !== "function") return null;
906
+ if (typeof candidate.removeHandler !== "function") return null;
907
+ return candidate;
908
+ } catch {
909
+ return null;
910
+ }
911
+ }
912
+ registerHandlers() {
913
+ if (!this.ipcMain) return;
914
+ for (const feature of this.featureManager.list()) for (const service of feature.config.services ?? []) {
915
+ if (service.scope !== ServiceScope.EXPOSED) continue;
916
+ const api = (feature.serviceManager?.get(service.id))?.api;
917
+ if (!api || typeof api !== "object") continue;
918
+ for (const [method, target] of Object.entries(api)) {
919
+ if (typeof target !== "function") continue;
920
+ const channel = `${feature.id}:${service.id}:${method}`;
921
+ if (this.registeredChannels.has(channel)) continue;
922
+ this.ipcMain.handle(channel, async (event, ...args) => {
923
+ return this.invoke(event, feature.id, service.id, method, args);
924
+ });
925
+ this.registeredChannels.add(channel);
926
+ }
927
+ }
928
+ }
929
+ async invoke(event, featureId, serviceId, method, args) {
930
+ this.assertAccess(event.sender.id, featureId, serviceId, method);
931
+ const channel = `${featureId}:${serviceId}:${method}`;
932
+ const resolved = this.featureManager.get(featureId)?.serviceManager?.get(serviceId);
933
+ if (!resolved || resolved.scope !== ServiceScope.EXPOSED) throw new Error(`No handler registered for '${channel}'`);
934
+ const fn = resolved.api?.[method];
935
+ if (typeof fn !== "function") throw new Error(`No handler registered for '${channel}'`);
936
+ return await Promise.resolve(fn(...args));
937
+ }
938
+ assertAccess(senderWebContentsId, featureId, serviceId, method) {
939
+ const viewId = this.resolveSenderViewId(senderWebContentsId);
940
+ if (!viewId) throw new Error(`Access denied for '${featureId}:${serviceId}:${method}': unknown renderer sender`);
941
+ if (!this.viewFeatures.get(viewId)?.has(featureId)) throw new Error(`Access denied for '${featureId}:${serviceId}:${method}' from view '${viewId}'`);
942
+ }
943
+ resolveSenderViewId(senderWebContentsId) {
944
+ for (const viewId of this.viewFeatures.keys()) {
945
+ const view = this.viewManager.get(viewId)?.view();
946
+ if (!view || view.webContents.isDestroyed()) continue;
947
+ if (view.webContents.id === senderWebContentsId) return viewId;
948
+ }
949
+ return null;
950
+ }
951
+ };
952
+
867
953
  //#endregion
868
954
  //#region src/core/logger/console-handler.ts
869
955
  const dim = "\x1B[90m";
@@ -1012,8 +1098,8 @@ var View = class {
1012
1098
  }
1013
1099
  };
1014
1100
  const PRELOAD_EXTENSIONS = [
1015
- "mjs",
1016
1101
  "cjs",
1102
+ "mjs",
1017
1103
  "js"
1018
1104
  ];
1019
1105
  function resolveDefaultPreloadPath(viewId) {
@@ -1068,6 +1154,7 @@ var Runtime = class {
1068
1154
  windowManager;
1069
1155
  viewManager;
1070
1156
  eventBridge = null;
1157
+ ipcBridge = null;
1071
1158
  constructor(config) {
1072
1159
  this.state = new StateMachine({
1073
1160
  transitions: RUNTIME_TRANSITIONS,
@@ -1084,7 +1171,10 @@ var Runtime = class {
1084
1171
  this.viewManager = new ViewManager();
1085
1172
  const viewRegistry = typeof __ELECTRO_VIEW_REGISTRY__ !== "undefined" ? __ELECTRO_VIEW_REGISTRY__ : [];
1086
1173
  for (const entry of viewRegistry) this.viewManager.register(new View(entry));
1087
- if (viewRegistry.some((v) => v.features && v.features.length > 0)) this.eventBridge = new EventBridge(this.eventBus, this.viewManager, viewRegistry);
1174
+ if (viewRegistry.some((v) => v.features && v.features.length > 0)) {
1175
+ this.eventBridge = new EventBridge(this.eventBus, this.viewManager, viewRegistry);
1176
+ this.ipcBridge = new IpcBridge(this.featureManager, this.viewManager, viewRegistry);
1177
+ }
1088
1178
  this.featureManager.setWindowManager(this.windowManager);
1089
1179
  this.featureManager.setViewManager(this.viewManager);
1090
1180
  if (config?.features) this.featureManager.register(config.features);
@@ -1098,8 +1188,10 @@ var Runtime = class {
1098
1188
  try {
1099
1189
  this.eventBridge?.start();
1100
1190
  await this.featureManager.bootstrap();
1191
+ await this.ipcBridge?.start();
1101
1192
  this.state.transition(RuntimeState.RUNNING);
1102
1193
  } catch (err) {
1194
+ this.ipcBridge?.stop();
1103
1195
  this.eventBridge?.stop();
1104
1196
  this.state.transition(RuntimeState.FAILED);
1105
1197
  throw err;
@@ -1108,6 +1200,7 @@ var Runtime = class {
1108
1200
  async shutdown() {
1109
1201
  this.state.assertState(RuntimeState.RUNNING);
1110
1202
  this.state.transition(RuntimeState.STOPPING);
1203
+ this.ipcBridge?.stop();
1111
1204
  this.eventBridge?.stop();
1112
1205
  await this.featureManager.shutdown();
1113
1206
  this.viewManager.destroyAll();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cordy/electro",
3
- "version": "1.2.16",
3
+ "version": "1.2.18",
4
4
  "description": "Electron framework — feature-first runtime, codegen, and Vite-based build system",
5
5
  "license": "MIT",
6
6
  "type": "module",