@testsmith/api-spector 0.0.8 → 0.1.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/bin/cli.js CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
  const path = require('path')
5
5
  const { spawn } = require('child_process')
6
- const electron = require('electron')
7
6
 
8
7
  const [, , cmd = 'ui', ...rest] = process.argv
9
8
 
@@ -20,14 +19,19 @@ function printHelp() {
20
19
  console.log(' api-spector run --help Show run options')
21
20
  console.log(' api-spector mock --help Show mock options')
22
21
  console.log('')
22
+ console.log(' Environment:')
23
+ console.log(' ELECTRON_NO_SANDBOX=1 Disable Chromium sandbox')
24
+ console.log(' (needed on locked-down Linux)')
25
+ console.log('')
23
26
  }
24
27
 
25
28
  if (cmd === '--help' || cmd === '-h') {
26
29
  printHelp()
27
30
  process.exit(0)
28
31
  } else if (cmd === 'ui') {
32
+ const electron = require('electron')
29
33
  const appDir = path.join(__dirname, '..')
30
- const proc = spawn(String(electron), [appDir], {
34
+ const proc = spawn(String(electron), [appDir, ...rest], {
31
35
  stdio: 'inherit',
32
36
  env: process.env,
33
37
  })
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ const http = require("http");
25
+ const crypto = require("crypto");
26
+ const vm = require("vm");
27
+ const dayjs = require("dayjs");
28
+ function _interopNamespaceDefault(e) {
29
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
30
+ if (e) {
31
+ for (const k in e) {
32
+ if (k !== "default") {
33
+ const d = Object.getOwnPropertyDescriptor(e, k);
34
+ Object.defineProperty(n, k, d.get ? d : {
35
+ enumerable: true,
36
+ get: () => e[k]
37
+ });
38
+ }
39
+ }
40
+ }
41
+ n.default = e;
42
+ return Object.freeze(n);
43
+ }
44
+ const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
45
+ let _fakerCache = null;
46
+ async function getFaker() {
47
+ if (!_fakerCache) _fakerCache = await import("@faker-js/faker");
48
+ return _fakerCache.faker;
49
+ }
50
+ const running = /* @__PURE__ */ new Map();
51
+ const liveRoutes = /* @__PURE__ */ new Map();
52
+ let hitCallback = null;
53
+ function setHitCallback(cb) {
54
+ hitCallback = cb;
55
+ }
56
+ function updateMockRoutes(id, routes) {
57
+ liveRoutes.set(id, routes);
58
+ }
59
+ function matchPath(pattern, urlPath) {
60
+ const regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+");
61
+ try {
62
+ const regex = new RegExp("^" + regexStr + "/?$");
63
+ return regex.test(urlPath.split("?")[0]);
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function findRoute(routes, method, urlPath) {
69
+ const path = urlPath.split("?")[0];
70
+ return routes.find((r) => r.method === method && matchPath(r.path, path)) ?? routes.find((r) => r.method === "ANY" && matchPath(r.path, path)) ?? null;
71
+ }
72
+ function extractPathParams(pattern, urlPath) {
73
+ const params = {};
74
+ const patParts = pattern.split("/");
75
+ const urlParts = urlPath.split("?")[0].split("/");
76
+ patParts.forEach((part, i) => {
77
+ if (part.startsWith(":")) {
78
+ params[part.slice(1)] = decodeURIComponent(urlParts[i] ?? "");
79
+ }
80
+ });
81
+ return params;
82
+ }
83
+ function extractQueryParams(url) {
84
+ const params = {};
85
+ const qs = url.split("?")[1];
86
+ if (qs) new URLSearchParams(qs).forEach((v, k) => {
87
+ params[k] = v;
88
+ });
89
+ return params;
90
+ }
91
+ function readBody(req) {
92
+ return new Promise((resolve) => {
93
+ let data = "";
94
+ req.on("data", (chunk) => {
95
+ data += chunk.toString();
96
+ });
97
+ req.on("end", () => resolve(data));
98
+ req.on("error", () => resolve(""));
99
+ });
100
+ }
101
+ function interpolateMockBody(body, context) {
102
+ return body.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
103
+ const expr = key.trim();
104
+ if (expr.includes(".") || expr.includes("(")) {
105
+ try {
106
+ const result = vm__namespace.runInNewContext(expr, context);
107
+ if (result !== void 0 && result !== null) return String(result);
108
+ } catch {
109
+ }
110
+ }
111
+ return match;
112
+ });
113
+ }
114
+ async function handleRequest(serverId, req, res, reqStart) {
115
+ const method = (req.method ?? "GET").toUpperCase();
116
+ const urlPath = req.url ?? "/";
117
+ if (urlPath === "/favicon.ico") {
118
+ res.writeHead(204);
119
+ res.end();
120
+ return;
121
+ }
122
+ const bodyRaw = await readBody(req);
123
+ let bodyParsed = {};
124
+ try {
125
+ bodyParsed = JSON.parse(bodyRaw);
126
+ } catch {
127
+ }
128
+ const routes = liveRoutes.get(serverId) ?? [];
129
+ const route = findRoute(routes, method, urlPath);
130
+ if (!route) {
131
+ const notFoundBody = JSON.stringify({ error: "No matching mock route", method, path: urlPath });
132
+ res.writeHead(404, { "Content-Type": "application/json" });
133
+ res.end(notFoundBody);
134
+ hitCallback?.({
135
+ id: crypto.randomUUID(),
136
+ serverId,
137
+ timestamp: reqStart,
138
+ method,
139
+ path: urlPath,
140
+ matchedRouteId: null,
141
+ status: 404,
142
+ durationMs: Date.now() - reqStart,
143
+ responseBody: notFoundBody,
144
+ responseHeaders: { "Content-Type": "application/json" }
145
+ });
146
+ return;
147
+ }
148
+ const reqHeaders = {};
149
+ Object.entries(req.headers).forEach(([k, v]) => {
150
+ if (v !== void 0) reqHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
151
+ });
152
+ const requestCtx = {
153
+ method,
154
+ path: urlPath.split("?")[0],
155
+ params: extractPathParams(route.path, urlPath),
156
+ query: extractQueryParams(urlPath),
157
+ body: bodyParsed,
158
+ bodyRaw,
159
+ headers: reqHeaders
160
+ };
161
+ const responseDraft = {
162
+ statusCode: route.statusCode,
163
+ body: route.body,
164
+ headers: { "Content-Type": "application/json", ...route.headers }
165
+ };
166
+ if (route.script?.trim()) {
167
+ try {
168
+ const faker2 = await getFaker();
169
+ vm__namespace.runInNewContext(route.script, {
170
+ request: requestCtx,
171
+ response: responseDraft,
172
+ faker: faker2,
173
+ dayjs,
174
+ console: { log: (...args) => console.log("[mock-script]", ...args) }
175
+ });
176
+ } catch (err) {
177
+ console.error("[mock-script error]", err);
178
+ }
179
+ }
180
+ const faker = await getFaker();
181
+ const exprCtx = { request: requestCtx, faker, dayjs };
182
+ const finalBody = interpolateMockBody(responseDraft.body, exprCtx);
183
+ const respond = () => {
184
+ res.writeHead(responseDraft.statusCode, responseDraft.headers);
185
+ res.end(finalBody);
186
+ hitCallback?.({
187
+ id: crypto.randomUUID(),
188
+ serverId,
189
+ timestamp: reqStart,
190
+ method,
191
+ path: urlPath,
192
+ matchedRouteId: route.id,
193
+ status: responseDraft.statusCode,
194
+ durationMs: Date.now() - reqStart,
195
+ responseBody: finalBody,
196
+ responseHeaders: responseDraft.headers
197
+ });
198
+ };
199
+ if (route.delay && route.delay > 0) setTimeout(respond, route.delay);
200
+ else respond();
201
+ }
202
+ async function startMock(server) {
203
+ if (running.has(server.id)) await stopMock(server.id);
204
+ liveRoutes.set(server.id, server.routes ?? []);
205
+ const httpServer = http.createServer((req, res) => {
206
+ const reqStart = Date.now();
207
+ handleRequest(server.id, req, res, reqStart).catch((err) => {
208
+ console.error("[mock-handler uncaught]", err);
209
+ if (!res.headersSent) {
210
+ res.writeHead(500, { "Content-Type": "application/json" });
211
+ res.end(JSON.stringify({ error: "Mock handler error", message: String(err) }));
212
+ }
213
+ });
214
+ });
215
+ await new Promise((resolve, reject) => {
216
+ httpServer.once("error", reject);
217
+ httpServer.listen(server.port, "127.0.0.1", resolve);
218
+ });
219
+ running.set(server.id, httpServer);
220
+ }
221
+ async function stopMock(id) {
222
+ const srv = running.get(id);
223
+ if (!srv) return;
224
+ await new Promise(
225
+ (resolve, reject) => srv.close((err) => err ? reject(err) : resolve())
226
+ );
227
+ running.delete(id);
228
+ liveRoutes.delete(id);
229
+ }
230
+ function isRunning(id) {
231
+ return running.has(id);
232
+ }
233
+ function getRunningIds() {
234
+ return [...running.keys()];
235
+ }
236
+ async function stopAll() {
237
+ await Promise.all([...running.keys()].map(stopMock));
238
+ }
239
+ exports.getRunningIds = getRunningIds;
240
+ exports.isRunning = isRunning;
241
+ exports.setHitCallback = setHitCallback;
242
+ exports.startMock = startMock;
243
+ exports.stopAll = stopAll;
244
+ exports.stopMock = stopMock;
245
+ exports.updateMockRoutes = updateMockRoutes;
@@ -25,8 +25,8 @@ const undici = require("undici");
25
25
  const promises = require("fs/promises");
26
26
  const crypto = require("crypto");
27
27
  const path = require("path");
28
- const vm = require("vm");
29
28
  const dayjs = require("dayjs");
29
+ const vm = require("vm");
30
30
  const tv4 = require("tv4");
31
31
  const jsonpathPlus = require("jsonpath-plus");
32
32
  const xmldom = require("@xmldom/xmldom");
@@ -140,8 +140,49 @@ async function getSecret(ref) {
140
140
  }
141
141
  return process.env[ref] ?? null;
142
142
  }
143
+ let _fakerCache$1 = null;
144
+ async function getFaker$1() {
145
+ if (!_fakerCache$1) _fakerCache$1 = await import("@faker-js/faker");
146
+ return _fakerCache$1.faker;
147
+ }
148
+ let _exprContext = null;
149
+ async function buildDynamicVars() {
150
+ const faker = await getFaker$1();
151
+ const now = dayjs();
152
+ _exprContext = { faker, dayjs };
153
+ return {
154
+ $uuid: faker.string.uuid(),
155
+ $timestamp: String(Date.now()),
156
+ $isoTimestamp: now.toISOString(),
157
+ $randomInt: String(faker.number.int({ min: 0, max: 1e3 })),
158
+ $randomFloat: String(faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 })),
159
+ $randomBoolean: String(faker.datatype.boolean()),
160
+ $randomEmail: faker.internet.email(),
161
+ $randomUsername: faker.internet.username(),
162
+ $randomPassword: faker.internet.password(),
163
+ $randomFullName: faker.person.fullName(),
164
+ $randomFirstName: faker.person.firstName(),
165
+ $randomLastName: faker.person.lastName(),
166
+ $randomWord: faker.lorem.word(),
167
+ $randomPhrase: faker.lorem.sentence(),
168
+ $randomUrl: faker.internet.url(),
169
+ $randomIp: faker.internet.ip(),
170
+ $randomHexColor: faker.color.rgb({ format: "hex", casing: "lower" })
171
+ };
172
+ }
143
173
  function interpolate(str, vars) {
144
- return str.replace(/\{\{([^}]+)\}\}/g, (_, key) => vars[key.trim()] ?? `{{${key}}}`);
174
+ return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
175
+ const trimmed = key.trim();
176
+ if (trimmed in vars) return vars[trimmed];
177
+ if (_exprContext && (trimmed.includes(".") || trimmed.includes("("))) {
178
+ try {
179
+ const result = vm__namespace.runInNewContext(trimmed, _exprContext);
180
+ if (result !== void 0 && result !== null) return String(result);
181
+ } catch {
182
+ }
183
+ }
184
+ return match;
185
+ });
145
186
  }
146
187
  function buildUrl(baseUrl, params, vars) {
147
188
  const url = interpolate(baseUrl, vars);
@@ -173,8 +214,8 @@ async function buildEnvVars(environment) {
173
214
  }
174
215
  return vars;
175
216
  }
176
- function mergeVars(envVars, collectionVars, globals2, localVars = {}) {
177
- return { ...globals2, ...collectionVars, ...envVars, ...localVars };
217
+ function mergeVars(envVars, collectionVars, globals2, localVars = {}, dynamicVars = {}) {
218
+ return { ...dynamicVars, ...globals2, ...collectionVars, ...envVars, ...localVars };
178
219
  }
179
220
  function xmlFindAll(node, tag, nth) {
180
221
  const results = [];
@@ -753,7 +794,8 @@ function registerRequestHandler(ipc) {
753
794
  }
754
795
  }
755
796
  }
756
- let vars = mergeVars(envVars, collectionVars, mergedGlobals, localVars);
797
+ const dynamicVars = await buildDynamicVars();
798
+ let vars = mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
757
799
  let preScriptMeta = { consoleOutput: [] };
758
800
  let updatedCollectionVars = { ...collectionVars };
759
801
  let updatedEnvVars = { ...envVars };
@@ -772,7 +814,7 @@ function registerRequestHandler(ipc) {
772
814
  updatedGlobals = result.updatedGlobals;
773
815
  patchGlobals(result.updatedGlobals);
774
816
  await persistGlobals();
775
- vars = mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars);
817
+ vars = mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
776
818
  }
777
819
  let response;
778
820
  let sentRequest = { method: req.method, url: "", headers: {} };
@@ -961,9 +1003,23 @@ function registerRequestHandler(ipc) {
961
1003
  };
962
1004
  return { response, scriptResult, sentRequest: redactSentRequest(sentRequest) };
963
1005
  });
1006
+ ipc.handle("script:run-hook", async (_e, payload) => {
1007
+ const { script, envVars, collectionVars, globals: globals2 } = payload;
1008
+ const result = await runScript(script, { envVars, collectionVars, globals: globals2, localVars: {} });
1009
+ patchGlobals(result.updatedGlobals);
1010
+ await persistGlobals();
1011
+ return {
1012
+ updatedEnvVars: result.updatedEnvVars,
1013
+ updatedCollectionVars: result.updatedCollectionVars,
1014
+ updatedGlobals: result.updatedGlobals,
1015
+ consoleOutput: result.consoleOutput,
1016
+ error: result.error
1017
+ };
1018
+ });
964
1019
  }
965
1020
  exports.buildAuthHeaders = buildAuthHeaders;
966
1021
  exports.buildDispatcher = buildDispatcher;
1022
+ exports.buildDynamicVars = buildDynamicVars;
967
1023
  exports.buildEnvVars = buildEnvVars;
968
1024
  exports.buildUrl = buildUrl;
969
1025
  exports.fetchOAuth2Token = fetchOAuth2Token;
package/out/main/index.js CHANGED
@@ -25,19 +25,19 @@ const electron = require("electron");
25
25
  const path = require("path");
26
26
  const fs = require("fs");
27
27
  const promises = require("fs/promises");
28
- const requestHandler = require("./chunks/request-handler-C1b53aHR.js");
28
+ const requestHandler = require("./chunks/request-handler-9MdHOWVf.js");
29
29
  const uuid = require("uuid");
30
30
  const jsYaml = require("js-yaml");
31
31
  const undici = require("undici");
32
32
  const JSZip = require("jszip");
33
- const mockServer = require("./chunks/mock-server-ZB7zpXh9.js");
33
+ const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
34
34
  const http = require("http");
35
35
  const WebSocket = require("ws");
36
36
  const https = require("https");
37
37
  const Ajv = require("ajv");
38
38
  require("crypto");
39
- require("vm");
40
39
  require("dayjs");
40
+ require("vm");
41
41
  require("tv4");
42
42
  require("jsonpath-plus");
43
43
  require("@xmldom/xmldom");
@@ -3436,6 +3436,10 @@ function registerContractHandlers(ipc) {
3436
3436
  return schema ? JSON.stringify(schema, null, 2) : null;
3437
3437
  });
3438
3438
  }
3439
+ if (process.env.ELECTRON_NO_SANDBOX === "1") {
3440
+ electron.app.commandLine.appendSwitch("no-sandbox");
3441
+ electron.app.commandLine.appendSwitch("disable-features", "RendererCodeIntegrity");
3442
+ }
3439
3443
  function createSplashWindow() {
3440
3444
  const splash = new electron.BrowserWindow({
3441
3445
  width: 420,
package/out/main/mock.js CHANGED
@@ -2,9 +2,11 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const mockServer = require("./chunks/mock-server-ZB7zpXh9.js");
5
+ const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
6
6
  require("http");
7
7
  require("crypto");
8
+ require("vm");
9
+ require("dayjs");
8
10
  const C = {
9
11
  reset: "\x1B[0m",
10
12
  bold: "\x1B[1m",
@@ -39,9 +41,18 @@ function parseArgs(argv) {
39
41
  }
40
42
  return args;
41
43
  }
44
+ async function resolveWorkspacePath(wsPath) {
45
+ const s = await promises.stat(wsPath);
46
+ if (!s.isDirectory()) return wsPath;
47
+ const entries = await promises.readdir(wsPath);
48
+ const spector = entries.find((e) => e.endsWith(".spector"));
49
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
50
+ return path.join(wsPath, spector);
51
+ }
42
52
  async function loadWorkspace(wsPath) {
43
- const raw = await promises.readFile(wsPath, "utf8");
44
- return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(wsPath)) };
53
+ const resolved = await resolveWorkspacePath(wsPath);
54
+ const raw = await promises.readFile(resolved, "utf8");
55
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)) };
45
56
  }
46
57
  async function loadMocks(workspace, dir) {
47
58
  const mocks = [];
@@ -59,7 +70,7 @@ async function main() {
59
70
  const args = parseArgs(process.argv.slice(2));
60
71
  if (args.help) {
61
72
  console.log(
62
- "\nUsage:\n api-spector mock --workspace <path> [--name <server-name>]\n\nOptions:\n --workspace <path> Path to workspace.json (required)\n --name <name> Start only the named server (repeat for multiple)\n --help Show this message\n"
73
+ "\nUsage:\n api-spector mock --workspace <path> [--name <server-name>] [--log]\n\nOptions:\n --workspace <path> Path to workspace.json (required)\n --name <name> Start only the named server (repeat for multiple)\n --log Print each incoming request (matched and unmatched)\n --help Show this message\n"
63
74
  );
64
75
  process.exit(0);
65
76
  }
@@ -115,6 +126,34 @@ async function main() {
115
126
  if (started === 0) {
116
127
  process.exit(1);
117
128
  }
129
+ if (args.log) {
130
+ const serverNames = {};
131
+ const routePaths = {};
132
+ for (const mock of toStart) {
133
+ serverNames[mock.id] = mock.name;
134
+ for (const route of mock.routes) {
135
+ routePaths[route.id] = `${route.method} ${route.path}`;
136
+ }
137
+ }
138
+ mockServer.setHitCallback((hit) => {
139
+ const ts = new Date(hit.timestamp).toISOString().slice(11, 23);
140
+ const server = color(serverNames[hit.serverId] ?? hit.serverId, C.white);
141
+ const method = hit.method.padEnd(7);
142
+ const matched = hit.matchedRouteId !== null;
143
+ const status = color(String(hit.status), hit.status < 400 ? C.green : C.red);
144
+ const dur = color(`${hit.durationMs}ms`, C.gray);
145
+ if (matched) {
146
+ const route = color(routePaths[hit.matchedRouteId] ?? hit.matchedRouteId, C.gray);
147
+ console.log(
148
+ color(` ${ts}`, C.gray) + ` ${server} ` + color(method, C.cyan) + color(hit.path, C.white) + ` ${status} ${dur}` + color(` → ${route}`, C.gray)
149
+ );
150
+ } else {
151
+ console.log(
152
+ color(` ${ts}`, C.gray) + ` ${server} ` + color(method, C.yellow) + color(hit.path, C.yellow) + ` ${status} ${dur}` + color(" (no match)", C.yellow)
153
+ );
154
+ }
155
+ });
156
+ }
118
157
  console.log("");
119
158
  console.log(color(" Press Ctrl+C to stop all servers.\n", C.gray));
120
159
  async function shutdown() {
@@ -3,10 +3,10 @@
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const undici = require("undici");
6
- const requestHandler = require("./chunks/request-handler-C1b53aHR.js");
6
+ const requestHandler = require("./chunks/request-handler-9MdHOWVf.js");
7
7
  require("crypto");
8
- require("vm");
9
8
  require("dayjs");
9
+ require("vm");
10
10
  require("tv4");
11
11
  require("jsonpath-plus");
12
12
  require("@xmldom/xmldom");
@@ -180,7 +180,8 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
180
180
  if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [pre] ${l}`, C.gray)));
181
181
  if (r.error) console.error(color(` [pre-script error] ${r.error}`, C.red));
182
182
  }
183
- const vars = requestHandler.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars);
183
+ const dynamicVars = await requestHandler.buildDynamicVars();
184
+ const vars = requestHandler.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
184
185
  const resolvedUrl = requestHandler.buildUrl(req.url, req.params, vars);
185
186
  base.resolvedUrl = resolvedUrl;
186
187
  const start = Date.now();
@@ -79,6 +79,10 @@ const api = {
79
79
  // ─── Contract testing ─────────────────────────────────────────────────────
80
80
  runContracts: (payload) => electron.ipcRenderer.invoke("contract:run", payload),
81
81
  inferContractSchema: (jsonBody) => electron.ipcRenderer.invoke("contract:inferSchema", jsonBody),
82
+ // ─── Script hooks ─────────────────────────────────────────────────────────
83
+ runScriptHook: (payload) => electron.ipcRenderer.invoke("script:run-hook", payload),
84
+ // ─── Zoom ─────────────────────────────────────────────────────────────────
85
+ setZoomFactor: (factor) => electron.webFrame.setZoomFactor(factor),
82
86
  // ─── Platform ─────────────────────────────────────────────────────────────
83
87
  platform: process.platform
84
88
  };