@wenathlan/extension 1.1.51 → 1.1.53
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/README.md +7 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1908 -200
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +85 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +37 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +139 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/trigger.d.ts +151 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/types.d.ts +289 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/workflow.d.ts +19 -3
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workfloweditor.d.ts +108 -0
- package/dist/workfloweditor.d.ts.map +1 -0
- package/extension/dist/background.js +2306 -20
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +1 -1
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +24 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +1442 -1
- package/extension/dist/sidepanel.js.map +4 -4
- package/extension/dist/style.css +2 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -2211,6 +2211,178 @@ var sessionmemory = class {
|
|
|
2211
2211
|
}
|
|
2212
2212
|
return history2;
|
|
2213
2213
|
}
|
|
2214
|
+
/** Stores one armed trigger rule with its workflow reference; re-arming the same id replaces the rule while the fire history survives. */
|
|
2215
|
+
async addtriggerule(rule) {
|
|
2216
|
+
const rules = (await this.gettriggerules()).filter((entry) => entry.id !== rule.id);
|
|
2217
|
+
await this.adapter.set("triggerules", [rule, ...rules]);
|
|
2218
|
+
}
|
|
2219
|
+
/** Returns every armed trigger rule, newest first. */
|
|
2220
|
+
async gettriggerules() {
|
|
2221
|
+
return await this.adapter.get("triggerules") ?? [];
|
|
2222
|
+
}
|
|
2223
|
+
/** Returns one armed trigger rule by its id. */
|
|
2224
|
+
async gettriggerule(id) {
|
|
2225
|
+
return (await this.gettriggerules()).find((rule) => rule.id === id);
|
|
2226
|
+
}
|
|
2227
|
+
/** Replaces one stored rule after an enable, disable, pause, resume, cooldown or fire bookkeeping change. */
|
|
2228
|
+
async settriggerule(rule) {
|
|
2229
|
+
const rules = await this.gettriggerules();
|
|
2230
|
+
await this.adapter.set("triggerules", rules.map((entry) => entry.id === rule.id ? rule : entry));
|
|
2231
|
+
}
|
|
2232
|
+
/** Replaces every stored rule at once so the session pause and resume suspend and release the whole rule set atomically. */
|
|
2233
|
+
async settriggerules(rules) {
|
|
2234
|
+
return this.adapter.set("triggerules", rules);
|
|
2235
|
+
}
|
|
2236
|
+
/** Removes one armed rule when the user disarms it; the fire history survives for the audit trail. */
|
|
2237
|
+
async removetriggerule(id) {
|
|
2238
|
+
await this.adapter.set("triggerules", (await this.gettriggerules()).filter((rule) => rule.id !== id));
|
|
2239
|
+
}
|
|
2240
|
+
/** Lists every armed rule joined with the name of its composed workflow so the trigger list shows what each rule launches. */
|
|
2241
|
+
async listtriggers() {
|
|
2242
|
+
const rules = await this.gettriggerules();
|
|
2243
|
+
const names = new Map((await this.listworkflows()).map((record2) => [record2.id, record2.name]));
|
|
2244
|
+
return rules.map((rule) => ({ rule, ...names.has(rule.workflowid) ? { workflowname: names.get(rule.workflowid) } : {} }));
|
|
2245
|
+
}
|
|
2246
|
+
/** Records one trigger fire with the reviewed retention window; an absent window keeps every fire record while the rule counters always survive. */
|
|
2247
|
+
async addtriggerfire(fire) {
|
|
2248
|
+
const fires = await this.listtriggerfires();
|
|
2249
|
+
const combined = [fire, ...fires];
|
|
2250
|
+
const retention = (await this.getsettings())?.triggerretention;
|
|
2251
|
+
await this.adapter.set("triggerfires", retention === void 0 ? combined : combined.slice(0, retention));
|
|
2252
|
+
}
|
|
2253
|
+
/** Returns every stored trigger fire record, newest first, optionally filtered to one rule. */
|
|
2254
|
+
async listtriggerfires(ruleid) {
|
|
2255
|
+
const fires = await this.adapter.get("triggerfires") ?? [];
|
|
2256
|
+
return ruleid === void 0 ? fires : fires.filter((fire) => fire.ruleid === ruleid);
|
|
2257
|
+
}
|
|
2258
|
+
/** Stores the pending trigger queue: fires that arrived while the target run was busy or the session paused; the resume drains them through the same gates. */
|
|
2259
|
+
async settriggerqueue(queue) {
|
|
2260
|
+
return this.adapter.set("triggerqueue", queue);
|
|
2261
|
+
}
|
|
2262
|
+
/** Returns the pending trigger queue, oldest first. */
|
|
2263
|
+
async gettriggerqueue() {
|
|
2264
|
+
return await this.adapter.get("triggerqueue") ?? [];
|
|
2265
|
+
}
|
|
2266
|
+
/** Stores one verified webhook payload of a rule; the executor verifies the shared secret and the schema before anything persists. */
|
|
2267
|
+
async addwebhookpayload(ruleid, payload, at) {
|
|
2268
|
+
const stored = await this.adapter.get("webhookpayloads") ?? [];
|
|
2269
|
+
await this.adapter.set("webhookpayloads", [{ ruleid, payload, at }, ...stored]);
|
|
2270
|
+
}
|
|
2271
|
+
/** Returns the stored webhook payloads of one rule, newest first; only secret verified deliveries ever reach this store. */
|
|
2272
|
+
async listwebhookpayloads(ruleid) {
|
|
2273
|
+
const stored = await this.adapter.get("webhookpayloads") ?? [];
|
|
2274
|
+
return stored.filter((entry) => entry.ruleid === ruleid);
|
|
2275
|
+
}
|
|
2276
|
+
/** Stores one manual run preview with its step list so the panel renders it before confirmation. */
|
|
2277
|
+
async addmanualrun(preview) {
|
|
2278
|
+
const runs = (await this.listmanualruns()).filter((entry) => entry.id !== preview.id);
|
|
2279
|
+
await this.adapter.set("manualruns", [preview, ...runs]);
|
|
2280
|
+
}
|
|
2281
|
+
/** Returns every stored manual run preview with its confirmation outcome, newest first. */
|
|
2282
|
+
async listmanualruns() {
|
|
2283
|
+
return await this.adapter.get("manualruns") ?? [];
|
|
2284
|
+
}
|
|
2285
|
+
/** Stores one workflow version record with its change note; saving the same version again replaces its note while older versions survive for the timeline. */
|
|
2286
|
+
async addworkflowversion(version) {
|
|
2287
|
+
const versions = (await this.listworkflowversions()).filter((entry) => !(entry.workflowid === version.workflowid && entry.version === version.version));
|
|
2288
|
+
await this.adapter.set("workflowversions", [version, ...versions]);
|
|
2289
|
+
}
|
|
2290
|
+
/** Returns every stored workflow version record, newest first, optionally filtered to one workflow. */
|
|
2291
|
+
async listworkflowversions(workflowid) {
|
|
2292
|
+
const versions = await this.adapter.get("workflowversions") ?? [];
|
|
2293
|
+
return workflowid === void 0 ? versions : versions.filter((entry) => entry.workflowid === workflowid);
|
|
2294
|
+
}
|
|
2295
|
+
/** Stores one version diff result for the history view. */
|
|
2296
|
+
async addversiondiff(diff) {
|
|
2297
|
+
const diffs = (await this.listversiondiffs()).filter((entry) => !(entry.workflowid === diff.workflowid && entry.from === diff.from && entry.to === diff.to));
|
|
2298
|
+
await this.adapter.set("versiondiffs", [diff, ...diffs]);
|
|
2299
|
+
}
|
|
2300
|
+
/** Returns every stored version diff result, newest first, optionally filtered to one workflow. */
|
|
2301
|
+
async listversiondiffs(workflowid) {
|
|
2302
|
+
const diffs = await this.adapter.get("versiondiffs") ?? [];
|
|
2303
|
+
return workflowid === void 0 ? diffs : diffs.filter((entry) => entry.workflowid === workflowid);
|
|
2304
|
+
}
|
|
2305
|
+
/** Records one run history entry — the outcome, duration and trigger cause of one execution — under the user configured retention window with no code ceiling. */
|
|
2306
|
+
async addrunhistory(entry) {
|
|
2307
|
+
const entries = await this.gethistory();
|
|
2308
|
+
const combined = [entry, ...entries];
|
|
2309
|
+
const retention = (await this.getsettings())?.runhistoryretention;
|
|
2310
|
+
await this.adapter.set("runhistory", retention === void 0 ? combined : combined.slice(0, retention));
|
|
2311
|
+
}
|
|
2312
|
+
/** Returns the stored run history, newest first, filtered by workflow, outcome and time floor; the filters stay user choices. */
|
|
2313
|
+
async gethistory(filter) {
|
|
2314
|
+
const entries = await this.adapter.get("runhistory") ?? [];
|
|
2315
|
+
let filtered = entries;
|
|
2316
|
+
if (filter?.workflowid !== void 0) filtered = filtered.filter((entry) => entry.workflowid === filter.workflowid);
|
|
2317
|
+
if (filter?.outcome !== void 0) filtered = filtered.filter((entry) => entry.outcome === filter.outcome);
|
|
2318
|
+
if (filter?.since !== void 0) filtered = filtered.filter((entry) => entry.endedat >= filter.since);
|
|
2319
|
+
if (filter?.limit !== void 0) filtered = filtered.slice(0, filter.limit);
|
|
2320
|
+
return filtered;
|
|
2321
|
+
}
|
|
2322
|
+
/** Stores the editor layout of one workflow so the canvas reopens exactly as left. */
|
|
2323
|
+
async seteditorlayout(workflowid, layout) {
|
|
2324
|
+
return this.adapter.set(`editorlayout${workflowid}`, layout);
|
|
2325
|
+
}
|
|
2326
|
+
/** Returns the stored editor layout of one workflow. */
|
|
2327
|
+
async geteditorlayout(workflowid) {
|
|
2328
|
+
return await this.adapter.get(`editorlayout${workflowid}`) ?? void 0;
|
|
2329
|
+
}
|
|
2330
|
+
/** Stores the breakpoint step ids of one workflow. */
|
|
2331
|
+
async setworkflowbreakpoints(workflowid, stepids) {
|
|
2332
|
+
return this.adapter.set(`workflowbreakpoints${workflowid}`, stepids);
|
|
2333
|
+
}
|
|
2334
|
+
/** Returns the stored breakpoint step ids of one workflow, oldest first. */
|
|
2335
|
+
async getworkflowbreakpoints(workflowid) {
|
|
2336
|
+
return await this.adapter.get(`workflowbreakpoints${workflowid}`) ?? [];
|
|
2337
|
+
}
|
|
2338
|
+
/** Stores one per site policy override; re-adding the same id replaces its deltas. */
|
|
2339
|
+
async addsiteoverride(override) {
|
|
2340
|
+
const overrides = (await this.listsiteoverrides()).filter((entry) => entry.id !== override.id);
|
|
2341
|
+
await this.adapter.set("siteoverrides", [override, ...overrides]);
|
|
2342
|
+
}
|
|
2343
|
+
/** Returns every stored per site override, newest first, optionally filtered to one workflow. */
|
|
2344
|
+
async listsiteoverrides(workflowid) {
|
|
2345
|
+
const overrides = await this.adapter.get("siteoverrides") ?? [];
|
|
2346
|
+
return workflowid === void 0 ? overrides : overrides.filter((entry) => entry.workflowid === workflowid);
|
|
2347
|
+
}
|
|
2348
|
+
/** Removes one per site override when the user deletes it. */
|
|
2349
|
+
async removesiteoverride(id) {
|
|
2350
|
+
await this.adapter.set("siteoverrides", (await this.listsiteoverrides()).filter((entry) => entry.id !== id));
|
|
2351
|
+
}
|
|
2352
|
+
/** Stores one watchdog event with its recovery outcome; the event history keeps the audit trail of every scan. */
|
|
2353
|
+
async addwatchdogevent(event) {
|
|
2354
|
+
const events = (await this.listwatchdogevents()).filter((entry) => entry.id !== event.id);
|
|
2355
|
+
await this.adapter.set("watchdogevents", [event, ...events]);
|
|
2356
|
+
}
|
|
2357
|
+
/** Returns every stored watchdog event, newest first. */
|
|
2358
|
+
async listwatchdogevents() {
|
|
2359
|
+
return await this.adapter.get("watchdogevents") ?? [];
|
|
2360
|
+
}
|
|
2361
|
+
/** Stores one pending workflow import held for review; approving it later stores the record as runnable. */
|
|
2362
|
+
async addworkflowimport(entry) {
|
|
2363
|
+
const imports = (await this.listworkflowimports()).filter((candidate) => candidate.id !== entry.id);
|
|
2364
|
+
await this.adapter.set("workflowimports", [entry, ...imports]);
|
|
2365
|
+
}
|
|
2366
|
+
/** Returns every pending workflow import, newest first. */
|
|
2367
|
+
async listworkflowimports() {
|
|
2368
|
+
return await this.adapter.get("workflowimports") ?? [];
|
|
2369
|
+
}
|
|
2370
|
+
/** Removes one pending import when the user approves or rejects it. */
|
|
2371
|
+
async removeworkflowimport(id) {
|
|
2372
|
+
await this.adapter.set("workflowimports", (await this.listworkflowimports()).filter((entry) => entry.id !== id));
|
|
2373
|
+
}
|
|
2374
|
+
/** Stores the per workflow background run flags so a workflow keeps running with the panel closed. */
|
|
2375
|
+
async setbackgroundruns(flags) {
|
|
2376
|
+
return this.adapter.set("backgroundruns", flags);
|
|
2377
|
+
}
|
|
2378
|
+
/** Returns the per workflow background run flags. */
|
|
2379
|
+
async getbackgroundruns() {
|
|
2380
|
+
return await this.adapter.get("backgroundruns") ?? {};
|
|
2381
|
+
}
|
|
2382
|
+
/** Removes one stored workflow record version; a rejected import or rollback disappears from the library while every other version survives. */
|
|
2383
|
+
async removeworkflowversion(id, version) {
|
|
2384
|
+
await this.adapter.set("workflowrecords", (await this.getworkflowrecordversions()).filter((entry) => !(entry.id === id && entry.version === version)));
|
|
2385
|
+
}
|
|
2214
2386
|
};
|
|
2215
2387
|
function mediakindof(record2) {
|
|
2216
2388
|
if ("pages" in record2) return "pdf";
|
|
@@ -3871,6 +4043,14 @@ async function runcontrolstep(input) {
|
|
|
3871
4043
|
|
|
3872
4044
|
// workflow.ts
|
|
3873
4045
|
var workflowkinds = ["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars"];
|
|
4046
|
+
function nestedparamof(value) {
|
|
4047
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4048
|
+
const candidate = value;
|
|
4049
|
+
if (typeof candidate.name !== "string" || !/^[a-z][a-z0-9]*$/.test(candidate.name)) return void 0;
|
|
4050
|
+
if (!variablekinds.includes(candidate.kind)) return void 0;
|
|
4051
|
+
if (candidate.default !== void 0 && !["string", "number", "boolean"].includes(typeof candidate.default) && !Array.isArray(candidate.default)) return void 0;
|
|
4052
|
+
return { name: candidate.name, kind: candidate.kind, ...candidate.default !== void 0 ? { default: candidate.default } : {} };
|
|
4053
|
+
}
|
|
3874
4054
|
function workflowstepof(value) {
|
|
3875
4055
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3876
4056
|
const candidate = value;
|
|
@@ -3880,6 +4060,7 @@ function workflowstepof(value) {
|
|
|
3880
4060
|
if (candidate.target !== void 0 && (typeof candidate.target !== "string" || !candidate.target)) return void 0;
|
|
3881
4061
|
if (candidate.value !== void 0 && typeof candidate.value !== "string") return void 0;
|
|
3882
4062
|
if (candidate.options !== void 0 && typeof candidate.options !== "string") return void 0;
|
|
4063
|
+
if (candidate.breakpoint !== void 0 && typeof candidate.breakpoint !== "boolean") return void 0;
|
|
3883
4064
|
const bindings = Array.isArray(candidate.bindings) ? candidate.bindings.flatMap((binding) => bindingof(binding) !== void 0 ? [bindingof(binding)] : []) : void 0;
|
|
3884
4065
|
if (candidate.bindings !== void 0 && bindings === void 0) return void 0;
|
|
3885
4066
|
if (Array.isArray(candidate.bindings) && bindings !== void 0 && bindings.length !== candidate.bindings.length) return void 0;
|
|
@@ -3887,14 +4068,20 @@ function workflowstepof(value) {
|
|
|
3887
4068
|
if (candidate.expression !== void 0 && expression === void 0) return void 0;
|
|
3888
4069
|
const extract = candidate.extract === void 0 ? void 0 : regexruleof(candidate.extract);
|
|
3889
4070
|
if (candidate.extract !== void 0 && extract === void 0) return void 0;
|
|
3890
|
-
|
|
4071
|
+
const params = Array.isArray(candidate.params) ? candidate.params.flatMap((param) => nestedparamof(param) !== void 0 ? [nestedparamof(param)] : []) : void 0;
|
|
4072
|
+
if (candidate.params !== void 0 && params === void 0) return void 0;
|
|
4073
|
+
if (Array.isArray(candidate.params) && params !== void 0 && params.length !== candidate.params.length) return void 0;
|
|
4074
|
+
return { id: candidate.id, kind: candidate.kind, label: candidate.label, ...candidate.target !== void 0 ? { target: candidate.target } : {}, ...candidate.value !== void 0 ? { value: candidate.value } : {}, ...candidate.options !== void 0 ? { options: candidate.options } : {}, ...bindings !== void 0 && bindings.length > 0 ? { bindings } : {}, ...expression !== void 0 ? { expression } : {}, ...extract !== void 0 ? { extract } : {}, ...candidate.breakpoint === true ? { breakpoint: true } : {}, ...params !== void 0 && params.length > 0 ? { params } : {} };
|
|
3891
4075
|
}
|
|
3892
4076
|
function blockinvocationof(value) {
|
|
3893
4077
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3894
4078
|
const candidate = value;
|
|
3895
4079
|
if (typeof candidate.block !== "string" || !candidate.block.trim()) return void 0;
|
|
3896
4080
|
if (typeof candidate.label !== "string" || !candidate.label.trim()) return void 0;
|
|
3897
|
-
|
|
4081
|
+
const params = Array.isArray(candidate.params) ? candidate.params.flatMap((param) => nestedparamof(param) !== void 0 ? [nestedparamof(param)] : []) : void 0;
|
|
4082
|
+
if (candidate.params !== void 0 && params === void 0) return void 0;
|
|
4083
|
+
if (Array.isArray(candidate.params) && params !== void 0 && params.length !== candidate.params.length) return void 0;
|
|
4084
|
+
return { block: candidate.block, label: candidate.label, ...params !== void 0 && params.length > 0 ? { params } : {} };
|
|
3898
4085
|
}
|
|
3899
4086
|
function workflowblockof(value) {
|
|
3900
4087
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
@@ -3973,10 +4160,15 @@ function regexruleof(value) {
|
|
|
3973
4160
|
function expandblocks(steps, blocks) {
|
|
3974
4161
|
const byname = new Map(blocks.map((block) => [block.name, block]));
|
|
3975
4162
|
const expanded = [];
|
|
3976
|
-
const visit = (entries, path, inside) => {
|
|
4163
|
+
const visit = (entries, path, inside, params) => {
|
|
4164
|
+
let stamped = params === void 0;
|
|
3977
4165
|
for (const entry of entries) {
|
|
3978
4166
|
if ("kind" in entry && "label" in entry && !("block" in entry)) {
|
|
3979
|
-
|
|
4167
|
+
const marked = inside === void 0 ? entry : { ...entry, block: inside };
|
|
4168
|
+
if (!stamped && params !== void 0) {
|
|
4169
|
+
expanded.push({ ...marked, params });
|
|
4170
|
+
stamped = true;
|
|
4171
|
+
} else expanded.push(marked);
|
|
3980
4172
|
continue;
|
|
3981
4173
|
}
|
|
3982
4174
|
const invocation = blockinvocationof(entry);
|
|
@@ -3984,7 +4176,7 @@ function expandblocks(steps, blocks) {
|
|
|
3984
4176
|
if (path.includes(invocation.block)) throw new Error(`The block ${invocation.block} recurs inside itself and cannot expand.`);
|
|
3985
4177
|
const block = byname.get(invocation.block);
|
|
3986
4178
|
if (!block) throw new Error(`The block ${invocation.block} is not defined in the workflow.`);
|
|
3987
|
-
visit(block.steps, [...path, invocation.block], invocation.block);
|
|
4179
|
+
visit(block.steps, [...path, invocation.block], invocation.block, invocation.params ?? params);
|
|
3988
4180
|
}
|
|
3989
4181
|
};
|
|
3990
4182
|
visit(steps, [], void 0);
|
|
@@ -4352,6 +4544,17 @@ async function runworkflow(input) {
|
|
|
4352
4544
|
if (step.block !== void 0 && step.block !== activeblock) {
|
|
4353
4545
|
scopes = pushscope(scopes, step.block, scopes[scopes.length - 1].name);
|
|
4354
4546
|
activeblock = step.block;
|
|
4547
|
+
if (step.params) {
|
|
4548
|
+
try {
|
|
4549
|
+
for (const param of step.params) {
|
|
4550
|
+
if (param.default === void 0) continue;
|
|
4551
|
+
scopes = setvariable(scopes, param.name, param.kind, coercevariable(param.default, param.kind), input.now);
|
|
4552
|
+
}
|
|
4553
|
+
} catch (error) {
|
|
4554
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4555
|
+
return { run: { ...run, state: "failed", endedat: Date.now(), failreason: `The nested parameter of block ${step.block} failed: ${reason}` }, scopes, log, outputs };
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4355
4558
|
} else if (step.block === void 0 && activeblock !== void 0) {
|
|
4356
4559
|
while (scopes.length > 1) scopes = popscope(scopes);
|
|
4357
4560
|
activeblock = void 0;
|
|
@@ -4384,6 +4587,415 @@ function dryrunworkflow(input) {
|
|
|
4384
4587
|
}
|
|
4385
4588
|
return { run: { ...run, state: "done", cursor: input.record.steps.length, endedat: input.now }, scopes, log };
|
|
4386
4589
|
}
|
|
4590
|
+
function watchdogpass(input) {
|
|
4591
|
+
const verdicts = [];
|
|
4592
|
+
for (const run of input.runs) {
|
|
4593
|
+
if (run.state !== "running") continue;
|
|
4594
|
+
const lastcompletedat = input.lastcompletedat[run.id] ?? run.startedat;
|
|
4595
|
+
const live = input.liveexecutors.includes(run.id);
|
|
4596
|
+
const silence = input.now - lastcompletedat;
|
|
4597
|
+
if (!live && input.config.zombiewindow !== void 0 && silence >= input.config.zombiewindow) {
|
|
4598
|
+
verdicts.push({ runid: run.id, verdict: "zombie", action: "reap", reason: `The run ${run.id} lost its executor ${silence} ms ago and reaps as a zombie of a browser shutdown at its last checkpoint ${run.cursor}.`, ...lastcompletedat !== run.startedat ? { lastcompletedat } : {} });
|
|
4599
|
+
continue;
|
|
4600
|
+
}
|
|
4601
|
+
if (!live) continue;
|
|
4602
|
+
if (silence >= input.config.stallthreshold) {
|
|
4603
|
+
const action = input.config.action;
|
|
4604
|
+
verdicts.push({ runid: run.id, verdict: "stalled", action, reason: `The run ${run.id} completed no step for ${silence} ms past the reviewed threshold and the watchdog recovers it with ${action} at cursor ${run.cursor}.`, ...lastcompletedat !== run.startedat ? { lastcompletedat } : {} });
|
|
4605
|
+
continue;
|
|
4606
|
+
}
|
|
4607
|
+
verdicts.push({ runid: run.id, verdict: "healthy", action: "none", reason: `The run ${run.id} completed its last step ${silence} ms ago and stays healthy.`, ...lastcompletedat !== run.startedat ? { lastcompletedat } : {} });
|
|
4608
|
+
}
|
|
4609
|
+
return verdicts;
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
// trigger.ts
|
|
4613
|
+
var triggerkinds = ["visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"];
|
|
4614
|
+
var triggerfamilies = ["visit", "url", "menu", "key", "button", "cron", "interval", "urllist", "webhook", "event"];
|
|
4615
|
+
var triggereventcatalog = ["mutate", "focus", "banner", "console", "error", "navigate"];
|
|
4616
|
+
var defaulttriggercooldown = 1e4;
|
|
4617
|
+
function triggerfamilyof(kind) {
|
|
4618
|
+
const index = triggerkinds.indexOf(kind);
|
|
4619
|
+
return index >= 0 ? triggerfamilies[index] : void 0;
|
|
4620
|
+
}
|
|
4621
|
+
function triggerlabel(value) {
|
|
4622
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
4623
|
+
}
|
|
4624
|
+
function positivewindow(value) {
|
|
4625
|
+
if (value === void 0) return void 0;
|
|
4626
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
4627
|
+
}
|
|
4628
|
+
function jitterwindow(value) {
|
|
4629
|
+
if (value === void 0) return void 0;
|
|
4630
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
4631
|
+
}
|
|
4632
|
+
function httpsorigin(value) {
|
|
4633
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
4634
|
+
try {
|
|
4635
|
+
const parsed = new URL(value.trim());
|
|
4636
|
+
if (parsed.protocol !== "https:") return void 0;
|
|
4637
|
+
return parsed.origin;
|
|
4638
|
+
} catch {
|
|
4639
|
+
return void 0;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
function webhookfieldof(value) {
|
|
4643
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4644
|
+
const candidate = value;
|
|
4645
|
+
if (typeof candidate.name !== "string" || !/^[a-z][a-z0-9]*$/i.test(candidate.name)) return void 0;
|
|
4646
|
+
if (candidate.kind !== "string" && candidate.kind !== "number" && candidate.kind !== "boolean") return void 0;
|
|
4647
|
+
if (candidate.required !== void 0 && typeof candidate.required !== "boolean") return void 0;
|
|
4648
|
+
return { name: candidate.name, kind: candidate.kind, ...candidate.required === true ? { required: true } : {} };
|
|
4649
|
+
}
|
|
4650
|
+
function triggerpayloadof(family, value) {
|
|
4651
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
4652
|
+
const candidate = value;
|
|
4653
|
+
if (family === "visit") {
|
|
4654
|
+
if (!Array.isArray(candidate.origins) || candidate.origins.length === 0) return void 0;
|
|
4655
|
+
const origins = candidate.origins.map((origin) => httpsorigin(origin));
|
|
4656
|
+
if (origins.some((origin) => origin === void 0)) return void 0;
|
|
4657
|
+
return { origins: [...new Set(origins)] };
|
|
4658
|
+
}
|
|
4659
|
+
if (family === "url") {
|
|
4660
|
+
if (typeof candidate.pattern !== "string" || !candidate.pattern.trim()) return void 0;
|
|
4661
|
+
if (httpsorigin(candidate.pattern) === void 0) return void 0;
|
|
4662
|
+
return { pattern: candidate.pattern.trim() };
|
|
4663
|
+
}
|
|
4664
|
+
if (family === "menu") {
|
|
4665
|
+
const title = triggerlabel(candidate.title);
|
|
4666
|
+
if (!title) return void 0;
|
|
4667
|
+
return { title };
|
|
4668
|
+
}
|
|
4669
|
+
if (family === "key") {
|
|
4670
|
+
if (typeof candidate.command !== "string" || !/^[a-z][a-z0-9-]*$/.test(candidate.command)) return void 0;
|
|
4671
|
+
if (candidate.key !== void 0 && (typeof candidate.key !== "string" || !candidate.key.trim())) return void 0;
|
|
4672
|
+
return { command: candidate.command, ...candidate.key !== void 0 ? { key: candidate.key } : {} };
|
|
4673
|
+
}
|
|
4674
|
+
if (family === "button") return {};
|
|
4675
|
+
if (family === "cron") {
|
|
4676
|
+
if (typeof candidate.cron !== "string" || !candidate.cron.trim()) return void 0;
|
|
4677
|
+
if (cronparse(candidate.cron) === void 0) return void 0;
|
|
4678
|
+
if (candidate.timezone !== void 0 && (typeof candidate.timezone !== "string" || !timezonevalid(candidate.timezone))) return void 0;
|
|
4679
|
+
return { cron: candidate.cron.trim(), ...candidate.timezone !== void 0 ? { timezone: candidate.timezone } : {} };
|
|
4680
|
+
}
|
|
4681
|
+
if (family === "interval") {
|
|
4682
|
+
const period = positivewindow(candidate.period);
|
|
4683
|
+
if (period === void 0) return void 0;
|
|
4684
|
+
const jitter = jitterwindow(candidate.jitter);
|
|
4685
|
+
if (candidate.jitter !== void 0 && jitter === void 0) return void 0;
|
|
4686
|
+
return { period, ...jitter !== void 0 ? { jitter } : {} };
|
|
4687
|
+
}
|
|
4688
|
+
if (family === "urllist") {
|
|
4689
|
+
if (!Array.isArray(candidate.urls) || candidate.urls.length === 0) return void 0;
|
|
4690
|
+
const urls = candidate.urls.map((url) => httpsorigin(url) === void 0 ? void 0 : url.trim());
|
|
4691
|
+
if (urls.some((url) => url === void 0)) return void 0;
|
|
4692
|
+
return { urls };
|
|
4693
|
+
}
|
|
4694
|
+
if (family === "webhook") {
|
|
4695
|
+
if (typeof candidate.secret !== "string" || !webhooksecretok(candidate.secret)) return void 0;
|
|
4696
|
+
if (!Array.isArray(candidate.schema) || candidate.schema.length === 0) return void 0;
|
|
4697
|
+
const schema = candidate.schema.map((field) => webhookfieldof(field));
|
|
4698
|
+
if (schema.some((field) => field === void 0)) return void 0;
|
|
4699
|
+
const names = schema.map((field) => field.name);
|
|
4700
|
+
if (new Set(names).size !== names.length) return void 0;
|
|
4701
|
+
return { secret: candidate.secret, schema };
|
|
4702
|
+
}
|
|
4703
|
+
const events = candidate.events;
|
|
4704
|
+
if (!Array.isArray(events) || events.length === 0) return void 0;
|
|
4705
|
+
if (!events.every((name) => typeof name === "string" && triggereventcatalog.includes(name))) return void 0;
|
|
4706
|
+
return { events: [...new Set(events)] };
|
|
4707
|
+
}
|
|
4708
|
+
function webhooksecretok(secret) {
|
|
4709
|
+
if (secret.length < 24) return false;
|
|
4710
|
+
if (/^(.)\1+$/.test(secret)) return false;
|
|
4711
|
+
return /[a-z]/i.test(secret) && /\d/.test(secret);
|
|
4712
|
+
}
|
|
4713
|
+
function timezonevalid(timezone) {
|
|
4714
|
+
try {
|
|
4715
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
4716
|
+
return true;
|
|
4717
|
+
} catch {
|
|
4718
|
+
return false;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
function armrule(input) {
|
|
4722
|
+
if (typeof input.workflowid !== "string" || !input.workflowid.trim()) return void 0;
|
|
4723
|
+
const payload = triggerpayloadof(input.family, input.payload);
|
|
4724
|
+
if (!payload) return void 0;
|
|
4725
|
+
if (input.cooldown !== void 0 && (typeof input.cooldown !== "number" || !Number.isFinite(input.cooldown) || input.cooldown <= 0)) return void 0;
|
|
4726
|
+
const cooldown = input.cooldown ?? (input.family === "webhook" || input.family === "event" ? defaulttriggercooldown : 0);
|
|
4727
|
+
const label = input.label ?? `The ${input.family} rule of ${input.workflowid}`;
|
|
4728
|
+
return { id: input.id ?? crypto.randomUUID(), kind: input.family, workflowid: input.workflowid, label, ...payload, cooldown, state: { enabled: true, cooldown }, stats: { fires: 0, launches: 0, suppressions: 0 }, createdat: input.now };
|
|
4729
|
+
}
|
|
4730
|
+
function updaterule(rule, patch) {
|
|
4731
|
+
return { ...rule, ...patch.state !== void 0 ? { state: { ...rule.state, ...patch.state } } : {}, ...patch.stats !== void 0 ? { stats: { ...rule.stats, ...patch.stats } } : {} };
|
|
4732
|
+
}
|
|
4733
|
+
function matchurl(pattern, url) {
|
|
4734
|
+
let parsedpattern;
|
|
4735
|
+
let parsedurl;
|
|
4736
|
+
try {
|
|
4737
|
+
parsedpattern = new URL(pattern);
|
|
4738
|
+
parsedurl = new URL(url);
|
|
4739
|
+
} catch {
|
|
4740
|
+
return false;
|
|
4741
|
+
}
|
|
4742
|
+
if (parsedpattern.protocol !== parsedurl.protocol) return false;
|
|
4743
|
+
if (parsedpattern.hostname !== parsedurl.hostname) return false;
|
|
4744
|
+
if (parsedpattern.port !== "" && parsedpattern.port !== parsedurl.port) return false;
|
|
4745
|
+
return globmatch(`${parsedpattern.pathname}${parsedpattern.search}`, `${parsedurl.pathname}${parsedurl.search}`);
|
|
4746
|
+
}
|
|
4747
|
+
function globmatch(pattern, text2) {
|
|
4748
|
+
const escaped = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4749
|
+
const expression = new RegExp(["^", pattern.split("**").map((segment) => segment.split("*").map(escaped).join("[^/]*")).join(".*"), "$"].join(""));
|
|
4750
|
+
return expression.test(text2);
|
|
4751
|
+
}
|
|
4752
|
+
function visitmatch(origins, url) {
|
|
4753
|
+
let origin = "";
|
|
4754
|
+
try {
|
|
4755
|
+
origin = new URL(url).origin;
|
|
4756
|
+
} catch {
|
|
4757
|
+
return false;
|
|
4758
|
+
}
|
|
4759
|
+
return origins.includes(origin);
|
|
4760
|
+
}
|
|
4761
|
+
function cronparse(expression) {
|
|
4762
|
+
const fields = expression.trim().split(/\s+/);
|
|
4763
|
+
if (fields.length !== 5) return void 0;
|
|
4764
|
+
const minutes = cronfield(fields[0] ?? "", 0, 59);
|
|
4765
|
+
const hours = cronfield(fields[1] ?? "", 0, 23);
|
|
4766
|
+
const daysofmonth = cronfield(fields[2] ?? "", 1, 31);
|
|
4767
|
+
const months = cronfield(fields[3] ?? "", 1, 12, monthnames);
|
|
4768
|
+
const daysofweek = cronfield(fields[4] ?? "", 0, 7, weekdaynames, true);
|
|
4769
|
+
if (!minutes || !hours || !daysofmonth || !months || !daysofweek) return void 0;
|
|
4770
|
+
return { minutes, hours, daysofmonth, months, daysofweek: [...new Set(daysofweek.map((day) => day % 7))].sort((left, right) => left - right) };
|
|
4771
|
+
}
|
|
4772
|
+
var weekdaynames = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
4773
|
+
var monthnames = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 };
|
|
4774
|
+
function cronfield(field, min, max, names, sundayseven = false) {
|
|
4775
|
+
const values = /* @__PURE__ */ new Set();
|
|
4776
|
+
for (const part of field.split(",")) {
|
|
4777
|
+
if (!part) return void 0;
|
|
4778
|
+
const [range, stepstring] = part.split("/");
|
|
4779
|
+
const step = stepstring === void 0 ? 1 : Number(stepstring);
|
|
4780
|
+
if (!Number.isInteger(step) || step < 1) return void 0;
|
|
4781
|
+
let low = min;
|
|
4782
|
+
let high = max;
|
|
4783
|
+
if (range !== void 0 && range !== "*") {
|
|
4784
|
+
const bounds = range.split("-");
|
|
4785
|
+
if (bounds.length > 2) return void 0;
|
|
4786
|
+
const lowvalue = cronvalue(bounds[0] ?? "", min, max, names);
|
|
4787
|
+
if (lowvalue === void 0) return void 0;
|
|
4788
|
+
low = lowvalue;
|
|
4789
|
+
high = lowvalue;
|
|
4790
|
+
if (bounds.length === 2) {
|
|
4791
|
+
const highvalue = cronvalue(bounds[1] ?? "", min, max, names);
|
|
4792
|
+
if (highvalue === void 0 || highvalue < lowvalue) return void 0;
|
|
4793
|
+
high = highvalue;
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
for (let value = low; value <= high; value += step) values.add(value);
|
|
4797
|
+
}
|
|
4798
|
+
const list = [...values];
|
|
4799
|
+
if (list.some((value) => value < min || value > max)) return void 0;
|
|
4800
|
+
if (sundayseven && values.has(7)) {
|
|
4801
|
+
values.delete(7);
|
|
4802
|
+
values.add(0);
|
|
4803
|
+
}
|
|
4804
|
+
return [...values].sort((left, right) => left - right);
|
|
4805
|
+
}
|
|
4806
|
+
function cronvalue(value, min, max, names) {
|
|
4807
|
+
const candidate = names?.[value.toLowerCase()];
|
|
4808
|
+
if (candidate !== void 0) return candidate;
|
|
4809
|
+
if (!/^\d+$/.test(value)) return void 0;
|
|
4810
|
+
const parsed = Number(value);
|
|
4811
|
+
if (parsed < min || parsed > max) return void 0;
|
|
4812
|
+
return parsed;
|
|
4813
|
+
}
|
|
4814
|
+
function calendarparts(at, timezone) {
|
|
4815
|
+
if (timezone === void 0) {
|
|
4816
|
+
const date = new Date(at);
|
|
4817
|
+
return { minute: date.getUTCMinutes(), hour: date.getUTCHours(), day: date.getUTCDate(), month: date.getUTCMonth() + 1, weekday: date.getUTCDay() };
|
|
4818
|
+
}
|
|
4819
|
+
const parts = new Intl.DateTimeFormat("en-US", { timeZone: timezone, hourCycle: "h23", minute: "numeric", hour: "numeric", day: "numeric", month: "short", weekday: "short" }).formatToParts(new Date(at));
|
|
4820
|
+
const pick = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
4821
|
+
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(pick("weekday"));
|
|
4822
|
+
const month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].indexOf(pick("month")) + 1;
|
|
4823
|
+
return { minute: Number(pick("minute")), hour: Number(pick("hour")), day: Number(pick("day")), month, weekday };
|
|
4824
|
+
}
|
|
4825
|
+
function crondaymatch(schedule, parts) {
|
|
4826
|
+
if (!schedule.months.includes(parts.month)) return false;
|
|
4827
|
+
const domfull = schedule.daysofmonth.length === 31;
|
|
4828
|
+
const dowfull = schedule.daysofweek.length === 7;
|
|
4829
|
+
const dommatch = schedule.daysofmonth.includes(parts.day);
|
|
4830
|
+
const dowmatch = schedule.daysofweek.includes(parts.weekday);
|
|
4831
|
+
if (!domfull && !dowfull) return dommatch || dowmatch;
|
|
4832
|
+
if (!domfull) return dommatch;
|
|
4833
|
+
if (!dowfull) return dowmatch;
|
|
4834
|
+
return true;
|
|
4835
|
+
}
|
|
4836
|
+
function cronnext(expression, from, timezone) {
|
|
4837
|
+
const schedule = cronparse(expression);
|
|
4838
|
+
if (!schedule) return void 0;
|
|
4839
|
+
const minute = 6e4;
|
|
4840
|
+
const hour = 60 * minute;
|
|
4841
|
+
const day = 24 * hour;
|
|
4842
|
+
let candidate = Math.floor(from / minute) * minute + minute;
|
|
4843
|
+
const horizon = from + 4 * 366 * day;
|
|
4844
|
+
while (candidate <= horizon) {
|
|
4845
|
+
const parts = calendarparts(candidate, timezone);
|
|
4846
|
+
if (!crondaymatch(schedule, parts)) {
|
|
4847
|
+
candidate += day - parts.hour * hour - parts.minute * minute;
|
|
4848
|
+
continue;
|
|
4849
|
+
}
|
|
4850
|
+
if (!schedule.hours.includes(parts.hour)) {
|
|
4851
|
+
const later = schedule.hours.find((value) => value > parts.hour);
|
|
4852
|
+
candidate += later === void 0 ? day - parts.hour * hour - parts.minute * minute : (later - parts.hour) * hour - parts.minute * minute;
|
|
4853
|
+
continue;
|
|
4854
|
+
}
|
|
4855
|
+
if (!schedule.minutes.includes(parts.minute)) {
|
|
4856
|
+
const later = schedule.minutes.find((value) => value > parts.minute);
|
|
4857
|
+
candidate += later === void 0 ? (60 - parts.minute) * minute : (later - parts.minute) * minute;
|
|
4858
|
+
continue;
|
|
4859
|
+
}
|
|
4860
|
+
return candidate;
|
|
4861
|
+
}
|
|
4862
|
+
return void 0;
|
|
4863
|
+
}
|
|
4864
|
+
function schedulecron(rule, from) {
|
|
4865
|
+
return cronnext(rule.cron, from, rule.timezone);
|
|
4866
|
+
}
|
|
4867
|
+
function scheduleinterval(rule, lastfire, armedat, seed) {
|
|
4868
|
+
const base = (lastfire ?? armedat) + rule.period;
|
|
4869
|
+
const jitter = rule.jitter ?? 0;
|
|
4870
|
+
if (jitter <= 0) return base;
|
|
4871
|
+
return Math.max(0, Math.round(base - jitter / 2 + seededrandom(seed) * jitter));
|
|
4872
|
+
}
|
|
4873
|
+
function listdue(rules, now) {
|
|
4874
|
+
return rules.flatMap((rule) => {
|
|
4875
|
+
if (rule.state.nextfireat === void 0 || rule.state.nextfireat > now) return [];
|
|
4876
|
+
if (!rule.state.enabled || rule.state.pausedat !== void 0) return [];
|
|
4877
|
+
return [{ rule, overdueby: now - rule.state.nextfireat }];
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
function applycooldown(rule, now) {
|
|
4881
|
+
const lastfireat = rule.state.lastfireat;
|
|
4882
|
+
if (lastfireat === void 0 || rule.state.cooldown <= 0) return { suppressed: false, remaining: 0 };
|
|
4883
|
+
const remaining = lastfireat + rule.state.cooldown - now;
|
|
4884
|
+
return { suppressed: remaining > 0, remaining: Math.max(0, remaining) };
|
|
4885
|
+
}
|
|
4886
|
+
function evaluatetrigger(input) {
|
|
4887
|
+
const rule = input.rule;
|
|
4888
|
+
if (!rule.state.enabled) return { fired: false, suppressed: "disabled" };
|
|
4889
|
+
if (rule.state.pausedat !== void 0) return { fired: false, suppressed: "paused" };
|
|
4890
|
+
if (!input.workflowreviewed) return { fired: false, suppressed: "unreviewed" };
|
|
4891
|
+
if (input.runactive) return { fired: false, suppressed: "dedupe" };
|
|
4892
|
+
const cooldown = applycooldown(rule, input.now);
|
|
4893
|
+
if (cooldown.suppressed) return { fired: false, suppressed: "cooldown", remaining: cooldown.remaining };
|
|
4894
|
+
const fire = { id: crypto.randomUUID(), ruleid: rule.id, at: input.now, cause: input.cause, ...input.url !== void 0 ? { url: input.url } : {}, ...input.title !== void 0 ? { title: input.title } : {}, ...input.payload !== void 0 ? { payload: input.payload } : {} };
|
|
4895
|
+
return { fired: true, fire };
|
|
4896
|
+
}
|
|
4897
|
+
function queuefire(queue, fire) {
|
|
4898
|
+
if (queue.some((pending) => pending.ruleid === fire.ruleid)) return { queue, queued: false, deduped: true };
|
|
4899
|
+
return { queue: [...queue, fire], queued: true, deduped: false };
|
|
4900
|
+
}
|
|
4901
|
+
async function drainqueue(queue, launch) {
|
|
4902
|
+
let remaining = [...queue];
|
|
4903
|
+
let launched = 0;
|
|
4904
|
+
while (remaining.length > 0) {
|
|
4905
|
+
const fire = remaining[0];
|
|
4906
|
+
try {
|
|
4907
|
+
await launch(fire);
|
|
4908
|
+
} catch {
|
|
4909
|
+
return { launched, remaining };
|
|
4910
|
+
}
|
|
4911
|
+
remaining = remaining.slice(1);
|
|
4912
|
+
launched += 1;
|
|
4913
|
+
}
|
|
4914
|
+
return { launched, remaining };
|
|
4915
|
+
}
|
|
4916
|
+
function verifywebhook(input) {
|
|
4917
|
+
if (typeof input.rule.secret !== "string" || !input.rule.secret) return { verified: false, reason: "The webhook rule carries no reviewed secret." };
|
|
4918
|
+
if (!secrectsmatch(input.secret, input.rule.secret)) return { verified: false, reason: "The webhook secret does not match the reviewed secret of the rule." };
|
|
4919
|
+
const payload = input.payload;
|
|
4920
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { verified: false, reason: "The webhook payload must be a JSON object." };
|
|
4921
|
+
const candidate = payload;
|
|
4922
|
+
for (const field of input.rule.schema ?? []) {
|
|
4923
|
+
const value = candidate[field.name];
|
|
4924
|
+
if (value === void 0) {
|
|
4925
|
+
if (field.required === true) return { verified: false, reason: `The required webhook field ${field.name} is missing.` };
|
|
4926
|
+
continue;
|
|
4927
|
+
}
|
|
4928
|
+
if (typeof value !== field.kind) return { verified: false, reason: `The webhook field ${field.name} is not a ${field.kind}.` };
|
|
4929
|
+
}
|
|
4930
|
+
return { verified: true };
|
|
4931
|
+
}
|
|
4932
|
+
function secrectsmatch(left, right) {
|
|
4933
|
+
if (left.length !== right.length) return false;
|
|
4934
|
+
let same = true;
|
|
4935
|
+
for (let index = 0; index < left.length; index += 1) if (left.charCodeAt(index) !== right.charCodeAt(index)) same = false;
|
|
4936
|
+
return same;
|
|
4937
|
+
}
|
|
4938
|
+
function eventrulematches(rule, event) {
|
|
4939
|
+
return (rule.events ?? []).includes(event);
|
|
4940
|
+
}
|
|
4941
|
+
function observeevents(rules, event) {
|
|
4942
|
+
if (!triggereventcatalog.includes(event)) return [];
|
|
4943
|
+
return rules.filter((rule) => rule.kind === "event" && rule.state.enabled && rule.state.pausedat === void 0 && eventrulematches(rule, event));
|
|
4944
|
+
}
|
|
4945
|
+
function pauseall(rules, now) {
|
|
4946
|
+
return rules.map((rule) => rule.state.enabled && rule.state.pausedat === void 0 ? updaterule(rule, { state: { pausedat: now } }) : rule);
|
|
4947
|
+
}
|
|
4948
|
+
function resumeall(rules) {
|
|
4949
|
+
return rules.map((rule) => {
|
|
4950
|
+
if (rule.state.pausedat === void 0) return rule;
|
|
4951
|
+
const state = { ...rule.state };
|
|
4952
|
+
delete state.pausedat;
|
|
4953
|
+
return { ...rule, state };
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
function manualpreview(record2, now) {
|
|
4957
|
+
return { id: crypto.randomUUID(), workflowid: record2.id, preview: record2.steps.map((step) => ({ stepid: step.id, kind: step.kind, label: step.label, ...step.block !== void 0 ? { block: step.block } : {}, ...controlsummary(step) !== void 0 ? { control: controlsummary(step) } : {} })), at: now };
|
|
4958
|
+
}
|
|
4959
|
+
function confirmmanualrun(preview, confirmed, now) {
|
|
4960
|
+
return { ...preview, confirmed, decidedat: now };
|
|
4961
|
+
}
|
|
4962
|
+
function triggersummary(rule) {
|
|
4963
|
+
return {
|
|
4964
|
+
kind: rule.kind,
|
|
4965
|
+
workflowid: rule.workflowid,
|
|
4966
|
+
label: rule.label,
|
|
4967
|
+
...rule.origins !== void 0 ? { origins: rule.origins } : {},
|
|
4968
|
+
...rule.pattern !== void 0 ? { pattern: rule.pattern } : {},
|
|
4969
|
+
...rule.title !== void 0 ? { title: rule.title } : {},
|
|
4970
|
+
...rule.command !== void 0 ? { command: rule.command } : {},
|
|
4971
|
+
...rule.key !== void 0 ? { key: rule.key } : {},
|
|
4972
|
+
...rule.cron !== void 0 ? { cron: rule.cron, ...rule.timezone !== void 0 ? { timezone: rule.timezone } : {} } : {},
|
|
4973
|
+
...rule.period !== void 0 ? { period: rule.period, ...rule.jitter !== void 0 ? { jitter: rule.jitter } : {} } : {},
|
|
4974
|
+
...rule.urls !== void 0 ? { urls: rule.urls } : {},
|
|
4975
|
+
...rule.events !== void 0 ? { events: rule.events } : {},
|
|
4976
|
+
...rule.schema !== void 0 ? { fields: rule.schema.length } : {},
|
|
4977
|
+
cooldown: rule.state.cooldown,
|
|
4978
|
+
enabled: rule.state.enabled,
|
|
4979
|
+
...rule.state.nextfireat !== void 0 ? { nextfireat: rule.state.nextfireat } : {}
|
|
4980
|
+
};
|
|
4981
|
+
}
|
|
4982
|
+
function ruleorigins(rule) {
|
|
4983
|
+
const origins = /* @__PURE__ */ new Set();
|
|
4984
|
+
for (const origin of rule.origins ?? []) origins.add(origin);
|
|
4985
|
+
if (rule.pattern !== void 0) {
|
|
4986
|
+
const origin = httpsorigin(rule.pattern);
|
|
4987
|
+
if (origin !== void 0) origins.add(origin);
|
|
4988
|
+
}
|
|
4989
|
+
for (const url of rule.urls ?? []) {
|
|
4990
|
+
const origin = httpsorigin(url);
|
|
4991
|
+
if (origin !== void 0) origins.add(origin);
|
|
4992
|
+
}
|
|
4993
|
+
return [...origins];
|
|
4994
|
+
}
|
|
4995
|
+
function ruleoriginsgranted(rule, workfloworigins) {
|
|
4996
|
+
const granted = new Set(workfloworigins);
|
|
4997
|
+
return ruleorigins(rule).every((origin) => granted.has(origin));
|
|
4998
|
+
}
|
|
4387
4999
|
|
|
4388
5000
|
// netauth.ts
|
|
4389
5001
|
function oauthflowof(value) {
|
|
@@ -4618,7 +5230,7 @@ function consolediff(input) {
|
|
|
4618
5230
|
}
|
|
4619
5231
|
|
|
4620
5232
|
// policy.ts
|
|
4621
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "restoresession", "exportsessions", "importsessions", "runworkflow"]);
|
|
5233
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "restoresession", "exportsessions", "importsessions", "runworkflow", "visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"]);
|
|
4622
5234
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction", "capturebodies", "setbreakpoint", "stepcode", "watchexpr", "loop", "repeatuntil", "whileloop", "foreach", "parallel", "trycatch"]);
|
|
4623
5235
|
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts", "persiststate", "capturesession", "namedsessions", "diffsessions", "searchsessions", "composeworkflow", "savetemplate", "dryrun", "delay", "waitelement", "compute", "extractvars", "condition", "branch"]);
|
|
4624
5236
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
@@ -4642,6 +5254,7 @@ var profileractions = /* @__PURE__ */ new Set(["measureflow", "heapshot", "track
|
|
|
4642
5254
|
var emulationactions = /* @__PURE__ */ new Set(["emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "blackboxscripts"]);
|
|
4643
5255
|
var sessionactions = /* @__PURE__ */ new Set(["persiststate", "capturesession", "restoresession", "namedsessions", "diffsessions", "searchsessions", "exportsessions", "importsessions"]);
|
|
4644
5256
|
var workflowactions = /* @__PURE__ */ new Set(["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars", "condition", "branch", "loop", "repeatuntil", "whileloop", "foreach", "parallel", "trycatch"]);
|
|
5257
|
+
var triggeractions = /* @__PURE__ */ new Set(["visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"]);
|
|
4645
5258
|
var credentialheaders = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie", "cookie2", "set-cookie", "api-key", "x-api-key", "x-auth-token", "x-session-token", "proxy-authorization"]);
|
|
4646
5259
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
4647
5260
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
@@ -4663,6 +5276,9 @@ function issessionkind(kind) {
|
|
|
4663
5276
|
function isworkflowkind(kind) {
|
|
4664
5277
|
return workflowactions.has(kind);
|
|
4665
5278
|
}
|
|
5279
|
+
function istriggeraction(kind) {
|
|
5280
|
+
return triggeractions.has(kind);
|
|
5281
|
+
}
|
|
4666
5282
|
function isdebugkind(kind) {
|
|
4667
5283
|
return debugactions.has(kind);
|
|
4668
5284
|
}
|
|
@@ -6408,6 +7024,79 @@ function workflowgate(input) {
|
|
|
6408
7024
|
}
|
|
6409
7025
|
return { allowed: true };
|
|
6410
7026
|
}
|
|
7027
|
+
function validatetriggergrammar(step, options) {
|
|
7028
|
+
const family = triggerfamilyof(step.kind);
|
|
7029
|
+
if (family === void 0) return { allowed: false, reason: "The trigger step is not a reviewed trigger kind." };
|
|
7030
|
+
if (typeof options.workflowid !== "string" || !options.workflowid.trim()) return { allowed: false, reason: "Every trigger rule needs the reviewed id of the composed workflow it launches." };
|
|
7031
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Every trigger rule needs the explicit arm review with its match fields and bound workflow shown before it arms." };
|
|
7032
|
+
if (options.label !== void 0 && (typeof options.label !== "string" || !options.label.trim())) return { allowed: false, reason: "The reviewed trigger label must be a non-empty string." };
|
|
7033
|
+
if (options.cooldown !== void 0 && (typeof options.cooldown !== "number" || !Number.isFinite(options.cooldown) || options.cooldown <= 0)) return { allowed: false, reason: "The reviewed cooldown window must be a positive number of milliseconds with no code ceiling; the webhook and event families keep the documented default when the review configures none." };
|
|
7034
|
+
const payload = options.rule;
|
|
7035
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { allowed: false, reason: `The ${step.kind} step needs its reviewed rule payload in options.` };
|
|
7036
|
+
if (triggerpayloadof(family, payload) === void 0) {
|
|
7037
|
+
if (family === "visit") return { allowed: false, reason: "The visit rule needs a non-empty reviewed list of HTTPS origins it fires on." };
|
|
7038
|
+
if (family === "url") return { allowed: false, reason: "The url rule needs a reviewed HTTPS glob url pattern; `*` spans one path segment and `**` spans across segments." };
|
|
7039
|
+
if (family === "menu") return { allowed: false, reason: "The menu rule needs a reviewed non-empty context menu entry title." };
|
|
7040
|
+
if (family === "key") return { allowed: false, reason: "The keyboard shortcut rule needs a reviewed lowercase command name and an optional suggested key binding." };
|
|
7041
|
+
if (family === "cron") return { allowed: false, reason: "The cron rule needs a reviewed five field cron expression of minutes, hours, days, months and weekdays with named weekdays and months and an optional resolvable timezone; unparseable schedules are refused." };
|
|
7042
|
+
if (family === "interval") return { allowed: false, reason: "The interval rule needs a reviewed positive period in milliseconds with an optional zero or positive jitter window." };
|
|
7043
|
+
if (family === "urllist") return { allowed: false, reason: "The url list rule needs a reviewed non-empty list of HTTPS urls its workflow runs across." };
|
|
7044
|
+
if (family === "webhook") return { allowed: false, reason: `The webhook rule needs a reviewed shared secret of at least twenty four characters mixing letters and digits and a non-empty payload schema of named string, number or boolean fields.` };
|
|
7045
|
+
if (family === "event") return { allowed: false, reason: `The page event rule needs a reviewed non-empty list of event names of the observed event catalog: ${triggereventcatalog.join(", ")}.` };
|
|
7046
|
+
return { allowed: false, reason: "The trigger rule payload does not follow its family grammar." };
|
|
7047
|
+
}
|
|
7048
|
+
if (family === "cron") {
|
|
7049
|
+
const candidate = payload;
|
|
7050
|
+
if (typeof candidate.cron === "string" && cronparse(candidate.cron) === void 0) return { allowed: false, reason: "The cron expression does not parse as a five field schedule and is refused." };
|
|
7051
|
+
}
|
|
7052
|
+
if (family === "webhook") {
|
|
7053
|
+
const candidate = payload;
|
|
7054
|
+
if (typeof candidate.secret === "string" && !webhooksecretok(candidate.secret)) return { allowed: false, reason: "The webhook shared secret must hold at least twenty four characters mixing letters and digits; the entropy floor is a floor, never a cap." };
|
|
7055
|
+
}
|
|
7056
|
+
const armed = armrule({ family, workflowid: options.workflowid, ...typeof options.label === "string" && options.label.trim() ? { label: options.label } : {}, payload, ...typeof options.cooldown === "number" ? { cooldown: options.cooldown } : {}, now: 0 });
|
|
7057
|
+
if (armed === void 0) return { allowed: false, reason: "The trigger rule payload does not arm as a reviewed rule." };
|
|
7058
|
+
return { allowed: true };
|
|
7059
|
+
}
|
|
7060
|
+
function triggergate(input) {
|
|
7061
|
+
const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now: input.now, action: "arm the trigger rule" });
|
|
7062
|
+
if (!gate.allowed) return gate;
|
|
7063
|
+
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Trigger rules need the approved plan review before they arm." };
|
|
7064
|
+
let triggeroptions = {};
|
|
7065
|
+
try {
|
|
7066
|
+
triggeroptions = parseoptions(input.step);
|
|
7067
|
+
} catch {
|
|
7068
|
+
triggeroptions = {};
|
|
7069
|
+
}
|
|
7070
|
+
if (triggeroptions.reviewed !== true) return { allowed: false, reason: "Every trigger rule needs the explicit arm review with its match fields and bound workflow shown before it arms." };
|
|
7071
|
+
return { allowed: true };
|
|
7072
|
+
}
|
|
7073
|
+
function triggerorigins(step) {
|
|
7074
|
+
let triggeroptions = {};
|
|
7075
|
+
try {
|
|
7076
|
+
triggeroptions = parseoptions(step);
|
|
7077
|
+
} catch {
|
|
7078
|
+
return [];
|
|
7079
|
+
}
|
|
7080
|
+
const family = triggerfamilyof(step.kind);
|
|
7081
|
+
if (family === void 0) return [];
|
|
7082
|
+
const armed = armrule({ family, workflowid: typeof triggeroptions.workflowid === "string" ? triggeroptions.workflowid : "", payload: triggeroptions.rule, ...typeof triggeroptions.cooldown === "number" ? { cooldown: triggeroptions.cooldown } : {}, now: 0 });
|
|
7083
|
+
if (armed === void 0) return [];
|
|
7084
|
+
const origins = [];
|
|
7085
|
+
for (const origin of armed.origins ?? []) origins.push(origin);
|
|
7086
|
+
if (armed.pattern !== void 0) {
|
|
7087
|
+
try {
|
|
7088
|
+
origins.push(new URL(armed.pattern).origin);
|
|
7089
|
+
} catch {
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
for (const url of armed.urls ?? []) {
|
|
7093
|
+
try {
|
|
7094
|
+
origins.push(new URL(url).origin);
|
|
7095
|
+
} catch {
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
return [...new Set(origins)];
|
|
7099
|
+
}
|
|
6411
7100
|
function dryrunprojection(step) {
|
|
6412
7101
|
if (iscontrolflowkind(step.kind)) {
|
|
6413
7102
|
for (const child of controlsteps(step)) {
|
|
@@ -7004,6 +7693,10 @@ function validatestep(step, origin) {
|
|
|
7004
7693
|
const workflowcheck = validateworkflowgrammar(step, options);
|
|
7005
7694
|
if (!workflowcheck.allowed) return workflowcheck;
|
|
7006
7695
|
}
|
|
7696
|
+
if (istriggeraction(step.kind)) {
|
|
7697
|
+
const triggercheck = validatetriggergrammar(step, options);
|
|
7698
|
+
if (!triggercheck.allowed) return triggercheck;
|
|
7699
|
+
}
|
|
7007
7700
|
if (step.kind === "tabcreate") {
|
|
7008
7701
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
7009
7702
|
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
@@ -7198,6 +7891,10 @@ function canexecute(input) {
|
|
|
7198
7891
|
const workflowgatecheck = workflowgate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
|
|
7199
7892
|
if (!workflowgatecheck.allowed) return workflowgatecheck;
|
|
7200
7893
|
}
|
|
7894
|
+
if (istriggeraction(input.step.kind)) {
|
|
7895
|
+
const triggergatecheck = triggergate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
|
|
7896
|
+
if (!triggergatecheck.allowed) return triggergatecheck;
|
|
7897
|
+
}
|
|
7201
7898
|
if (iscontrolkind(input.step.kind)) {
|
|
7202
7899
|
const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
|
|
7203
7900
|
if (!controlgate.allowed) return controlgate;
|
|
@@ -7294,6 +7991,134 @@ function canpreview(input) {
|
|
|
7294
7991
|
if (!targetactions.has(input.step.kind) && options.targetref === void 0) return { allowed: false, reason: "Only a target-based action can be previewed." };
|
|
7295
7992
|
return validatestep(input.step, input.origin);
|
|
7296
7993
|
}
|
|
7994
|
+
function reviewedkinds() {
|
|
7995
|
+
return [...allowedactions].sort();
|
|
7996
|
+
}
|
|
7997
|
+
function editorsavegate(input) {
|
|
7998
|
+
const gate = sessiongate({ session: input.session, tabid: input.session?.tabid ?? 0, origin: input.session?.origin ?? "https://example.com", now: input.now, action: "save the workflow editor canvas" });
|
|
7999
|
+
if (!gate.allowed) return gate;
|
|
8000
|
+
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Editor saves need the approved plan review before a new workflow version composes." };
|
|
8001
|
+
const model = input.model;
|
|
8002
|
+
if (typeof model.name !== "string" || !model.name.trim()) return { allowed: false, reason: "The workflow name of the canvas must be a non-empty string." };
|
|
8003
|
+
if (typeof model.version !== "number" || !Number.isInteger(model.version) || model.version < 1) return { allowed: false, reason: "The workflow version of the canvas must be a positive integer." };
|
|
8004
|
+
if (!Array.isArray(model.origins) || model.origins.length === 0) return { allowed: false, reason: "The canvas needs at least one granted HTTPS origin." };
|
|
8005
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8006
|
+
for (const node of model.nodes) {
|
|
8007
|
+
if (node.step === void 0 === (node.invocation === void 0)) return { allowed: false, reason: "Every canvas node must be exactly one workflow step or one block invocation." };
|
|
8008
|
+
const id = node.id ?? (node.step !== void 0 ? node.step.id : node.invocation.block);
|
|
8009
|
+
if (!id || ids.has(id)) return { allowed: false, reason: `The canvas node id ${id || "(empty)"} must be unique.` };
|
|
8010
|
+
ids.add(id);
|
|
8011
|
+
}
|
|
8012
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
8013
|
+
for (const node of model.nodes) {
|
|
8014
|
+
if (node.step !== void 0) {
|
|
8015
|
+
reachable.add(node.step.id);
|
|
8016
|
+
continue;
|
|
8017
|
+
}
|
|
8018
|
+
const walk = (entries) => {
|
|
8019
|
+
for (const entry of entries) {
|
|
8020
|
+
if (typeof entry.id === "string" && typeof entry.kind === "string") {
|
|
8021
|
+
reachable.add(entry.id);
|
|
8022
|
+
continue;
|
|
8023
|
+
}
|
|
8024
|
+
if (typeof entry.block === "string") {
|
|
8025
|
+
const nested = model.blocks.find((candidate) => candidate.name === entry.block);
|
|
8026
|
+
if (nested) walk(nested.steps);
|
|
8027
|
+
}
|
|
8028
|
+
}
|
|
8029
|
+
};
|
|
8030
|
+
const block = model.blocks.find((candidate) => candidate.name === node.invocation.block);
|
|
8031
|
+
if (!block) return { allowed: false, reason: `The block ${node.invocation.block} of the canvas has no definition.` };
|
|
8032
|
+
walk(block.steps);
|
|
8033
|
+
}
|
|
8034
|
+
let order = 0;
|
|
8035
|
+
const positionof = /* @__PURE__ */ new Map();
|
|
8036
|
+
for (const node of model.nodes) {
|
|
8037
|
+
if (node.step !== void 0) {
|
|
8038
|
+
positionof.set(node.step.id, order);
|
|
8039
|
+
order += 1;
|
|
8040
|
+
continue;
|
|
8041
|
+
}
|
|
8042
|
+
const walk = (entries) => {
|
|
8043
|
+
for (const entry of entries) {
|
|
8044
|
+
if (typeof entry.id === "string" && typeof entry.kind === "string") {
|
|
8045
|
+
positionof.set(entry.id, order);
|
|
8046
|
+
order += 1;
|
|
8047
|
+
continue;
|
|
8048
|
+
}
|
|
8049
|
+
if (typeof entry.block === "string") {
|
|
8050
|
+
const nested = model.blocks.find((candidate) => candidate.name === entry.block);
|
|
8051
|
+
if (nested) walk(nested.steps);
|
|
8052
|
+
}
|
|
8053
|
+
}
|
|
8054
|
+
};
|
|
8055
|
+
walk(model.blocks.find((candidate) => candidate.name === node.invocation.block).steps);
|
|
8056
|
+
}
|
|
8057
|
+
for (const edge of model.edges) {
|
|
8058
|
+
if (!reachable.has(edge.from)) return { allowed: false, reason: `The canvas edge of ${edge.variable} references the unknown source step ${edge.from}.` };
|
|
8059
|
+
if (!reachable.has(edge.to)) return { allowed: false, reason: `The canvas edge of ${edge.variable} references the unknown target step ${edge.to}.` };
|
|
8060
|
+
if ((positionof.get(edge.from) ?? -1) >= (positionof.get(edge.to) ?? -1)) return { allowed: false, reason: `The canvas edge of ${edge.variable} runs backwards and would form a cycle.` };
|
|
8061
|
+
}
|
|
8062
|
+
return { allowed: true };
|
|
8063
|
+
}
|
|
8064
|
+
function runreviewgranted(record2) {
|
|
8065
|
+
if (record2.reviewstate === "pending") return { allowed: false, reason: "The workflow stays unreviewed: the import or rollback review must approve its expanded step list before any run." };
|
|
8066
|
+
return { allowed: true };
|
|
8067
|
+
}
|
|
8068
|
+
var overrideknobs = ["loopbound", "stepms", "runms", "waitms", "delaybase"];
|
|
8069
|
+
function validatesiteoverride(override) {
|
|
8070
|
+
if (typeof override.pattern !== "string" || !override.pattern.startsWith("https://") || !/[a-z0-9.-]+/i.test(override.pattern.slice(8))) return { allowed: false, reason: "The override pattern must be an https origin or a `*` subdomain glob of one." };
|
|
8071
|
+
if (!override.pattern.includes("*")) {
|
|
8072
|
+
try {
|
|
8073
|
+
if (new URL(override.pattern).origin !== override.pattern) return { allowed: false, reason: "The override pattern must be a bare https origin or a `*` subdomain glob, never a path." };
|
|
8074
|
+
} catch {
|
|
8075
|
+
return { allowed: false, reason: "The override pattern must parse as an https origin or a `*` subdomain glob of one." };
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
for (const [knob, delta] of Object.entries(override.deltas)) {
|
|
8079
|
+
if (!overrideknobs.includes(knob)) return { allowed: false, reason: `The override knob ${knob} is not one of the reviewed knobs: ${overrideknobs.join(", ")}.` };
|
|
8080
|
+
if (typeof delta !== "number" || !Number.isFinite(delta) || delta <= 0) return { allowed: false, reason: `The override delta of ${knob} must be a positive user value with no code ceiling.` };
|
|
8081
|
+
}
|
|
8082
|
+
return { allowed: true };
|
|
8083
|
+
}
|
|
8084
|
+
function exportcontentreview(file) {
|
|
8085
|
+
const secretkeys = /(secret|token|apikey|api_key|password|authorization|credential)/i;
|
|
8086
|
+
const scan = (label, options) => {
|
|
8087
|
+
if (options === void 0) return void 0;
|
|
8088
|
+
let payload;
|
|
8089
|
+
try {
|
|
8090
|
+
payload = JSON.parse(options);
|
|
8091
|
+
} catch {
|
|
8092
|
+
return void 0;
|
|
8093
|
+
}
|
|
8094
|
+
const walk = (value, path) => {
|
|
8095
|
+
if (!value || typeof value !== "object") return void 0;
|
|
8096
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
8097
|
+
if (secretkeys.test(key)) return { allowed: false, reason: `The export of ${label} carries the secret field ${path}${key} and secrets never leave the browser.` };
|
|
8098
|
+
const nested = walk(entry, `${path}${key}.`);
|
|
8099
|
+
if (nested !== void 0) return nested;
|
|
8100
|
+
}
|
|
8101
|
+
return void 0;
|
|
8102
|
+
};
|
|
8103
|
+
return walk(payload, "");
|
|
8104
|
+
};
|
|
8105
|
+
for (const step of file.workflow.steps) {
|
|
8106
|
+
const refusal = scan(`the step ${step.id}`, step.options);
|
|
8107
|
+
if (refusal !== void 0) return refusal;
|
|
8108
|
+
}
|
|
8109
|
+
for (const template of file.templates) {
|
|
8110
|
+
const refusal = scan(`the template ${template.name}`, template.step.options);
|
|
8111
|
+
if (refusal !== void 0) return refusal;
|
|
8112
|
+
}
|
|
8113
|
+
return { allowed: true };
|
|
8114
|
+
}
|
|
8115
|
+
function watchdogconfigvalid(config) {
|
|
8116
|
+
if (typeof config.enabled !== "boolean") return { allowed: false, reason: "The watchdog enabled flag must be a boolean." };
|
|
8117
|
+
if (typeof config.stallthreshold !== "number" || !Number.isFinite(config.stallthreshold) || config.stallthreshold <= 0) return { allowed: false, reason: "The watchdog stall threshold must be a positive number of milliseconds with no code ceiling." };
|
|
8118
|
+
if (!["retry", "pause", "cancel"].includes(config.action)) return { allowed: false, reason: "The watchdog recovery action must be retry, pause or cancel." };
|
|
8119
|
+
if (config.zombiewindow !== void 0 && (typeof config.zombiewindow !== "number" || !Number.isFinite(config.zombiewindow) || config.zombiewindow <= 0)) return { allowed: false, reason: "The watchdog zombie window, when configured, must be a positive number of milliseconds with no code ceiling." };
|
|
8120
|
+
return { allowed: true };
|
|
8121
|
+
}
|
|
7297
8122
|
|
|
7298
8123
|
// progress.ts
|
|
7299
8124
|
function emptyprogress(planid, now) {
|
|
@@ -7460,9 +8285,15 @@ function recordworkflow(progress, planid, stepid, entry, now) {
|
|
|
7460
8285
|
const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { workflow: entry }, at: now };
|
|
7461
8286
|
return recordoutcome(base, planid, outcome, now);
|
|
7462
8287
|
}
|
|
8288
|
+
function recordtrigger(progress, planid, stepid, entry, now) {
|
|
8289
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
8290
|
+
const counts = `${entry.fires !== void 0 ? `${entry.fires} fire${entry.fires === 1 ? "" : "s"}, ` : ""}${entry.launches !== void 0 ? `${entry.launches} launch${entry.launches === 1 ? "" : "es"}, ` : ""}${entry.suppressions !== void 0 ? `${entry.suppressions} suppression${entry.suppressions === 1 ? "" : "s"}, ` : ""}${entry.queued !== void 0 ? `${entry.queued} queued fire${entry.queued === 1 ? "" : "s"}, ` : ""}`.replace(/, $/, "");
|
|
8291
|
+
const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { trigger: entry }, at: now };
|
|
8292
|
+
return recordoutcome(base, planid, outcome, now);
|
|
8293
|
+
}
|
|
7463
8294
|
|
|
7464
8295
|
// version.ts
|
|
7465
|
-
var packageversion = "1.1.
|
|
8296
|
+
var packageversion = "1.1.53";
|
|
7466
8297
|
|
|
7467
8298
|
// types.ts
|
|
7468
8299
|
var protocolversion = packageversion;
|
|
@@ -7701,6 +8532,25 @@ function parseproposal(value, origin, grants) {
|
|
|
7701
8532
|
}
|
|
7702
8533
|
if (step.kind === "runworkflow" && workflowoptions.reviewed !== true) throw new Error("Workflow runs without the explicit run review of the expanded step list are refused.");
|
|
7703
8534
|
}
|
|
8535
|
+
if (istriggeraction(step.kind)) {
|
|
8536
|
+
let triggeroptions = {};
|
|
8537
|
+
try {
|
|
8538
|
+
triggeroptions = parseoptions(step);
|
|
8539
|
+
} catch {
|
|
8540
|
+
triggeroptions = {};
|
|
8541
|
+
}
|
|
8542
|
+
if (triggeroptions.reviewed !== true) throw new Error("Trigger rules without the explicit arm review of their match fields and bound workflow are refused.");
|
|
8543
|
+
for (const ruleorigin of triggerorigins(step)) {
|
|
8544
|
+
const granted = covered.some((pattern) => {
|
|
8545
|
+
try {
|
|
8546
|
+
return new URL(ruleorigin).origin === new URL(pattern).origin;
|
|
8547
|
+
} catch {
|
|
8548
|
+
return false;
|
|
8549
|
+
}
|
|
8550
|
+
});
|
|
8551
|
+
if (!granted) throw new Error(`The trigger on ${ruleorigin} stays outside the grants.`);
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
7704
8554
|
const evaluation = validatestep(step, origin);
|
|
7705
8555
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
7706
8556
|
const target = outboundtarget(step);
|
|
@@ -7780,7 +8630,7 @@ function requestbody(input) {
|
|
|
7780
8630
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
7781
8631
|
}
|
|
7782
8632
|
function outcomeresponse(input) {
|
|
7783
|
-
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {}, ...input.session ? { session: input.session } : {}, ...input.workflow ? { workflow: { runid: input.workflow.runid, state: input.workflow.state, ...input.workflow.dryrun === true ? { dryrun: true } : {}, produced: input.workflow.produced, consumed: input.workflow.consumed, ...input.workflow.timeout !== void 0 ? { timeout: input.workflow.timeout } : {}, ...input.workflow.retry !== void 0 ? { retry: input.workflow.retry } : {} } } : {} });
|
|
8633
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {}, ...input.session ? { session: input.session } : {}, ...input.workflow ? { workflow: { runid: input.workflow.runid, state: input.workflow.state, ...input.workflow.dryrun === true ? { dryrun: true } : {}, produced: input.workflow.produced, consumed: input.workflow.consumed, ...input.workflow.timeout !== void 0 ? { timeout: input.workflow.timeout } : {}, ...input.workflow.retry !== void 0 ? { retry: input.workflow.retry } : {} } } : {}, ...input.trigger ? { trigger: { ruleid: input.trigger.ruleid, kind: input.trigger.kind, enabled: input.trigger.enabled, ...input.trigger.nextfireat !== void 0 ? { nextfireat: input.trigger.nextfireat } : {} } } : {} });
|
|
7784
8634
|
}
|
|
7785
8635
|
function mapresponse(input) {
|
|
7786
8636
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -7917,6 +8767,75 @@ function sessionreport(input) {
|
|
|
7917
8767
|
function workflowreport(input) {
|
|
7918
8768
|
return { version: protocolversion, workflows: input.workflows, runs: input.runs, templates: input.templates, log: input.log ?? [], scopes: input.scopes ?? [], provenance: input.provenance ?? [], control: input.control ?? [] };
|
|
7919
8769
|
}
|
|
8770
|
+
function triggerlist(input) {
|
|
8771
|
+
const names = new Map(input.workflows.map((record2) => [record2.id, record2.name]));
|
|
8772
|
+
const rules = input.rules.map((rule) => {
|
|
8773
|
+
const workflowname = names.get(rule.workflowid);
|
|
8774
|
+
return {
|
|
8775
|
+
id: rule.id,
|
|
8776
|
+
kind: rule.kind,
|
|
8777
|
+
workflowid: rule.workflowid,
|
|
8778
|
+
...workflowname !== void 0 ? { workflowname } : {},
|
|
8779
|
+
label: rule.label,
|
|
8780
|
+
enabled: rule.state.enabled,
|
|
8781
|
+
...rule.state.pausedat !== void 0 ? { paused: true } : {},
|
|
8782
|
+
cooldown: rule.state.cooldown,
|
|
8783
|
+
...rule.state.lastfireat !== void 0 ? { lastfireat: rule.state.lastfireat } : {},
|
|
8784
|
+
...rule.state.nextfireat !== void 0 ? { nextfireat: rule.state.nextfireat } : {},
|
|
8785
|
+
fires: rule.stats.fires,
|
|
8786
|
+
launches: rule.stats.launches,
|
|
8787
|
+
suppressions: rule.stats.suppressions,
|
|
8788
|
+
summary: triggersummaryof(rule)
|
|
8789
|
+
};
|
|
8790
|
+
});
|
|
8791
|
+
return { version: protocolversion, rules, queued: (input.queue ?? []).length };
|
|
8792
|
+
}
|
|
8793
|
+
function triggersummaryof(rule) {
|
|
8794
|
+
const summary = { kind: rule.kind, workflowid: rule.workflowid };
|
|
8795
|
+
if (rule.origins !== void 0) summary.origins = rule.origins;
|
|
8796
|
+
if (rule.pattern !== void 0) summary.pattern = rule.pattern;
|
|
8797
|
+
if (rule.title !== void 0) summary.title = rule.title;
|
|
8798
|
+
if (rule.command !== void 0) summary.command = rule.command;
|
|
8799
|
+
if (rule.key !== void 0) summary.key = rule.key;
|
|
8800
|
+
if (rule.cron !== void 0) summary.cron = rule.cron;
|
|
8801
|
+
if (rule.timezone !== void 0) summary.timezone = rule.timezone;
|
|
8802
|
+
if (rule.period !== void 0) summary.period = rule.period;
|
|
8803
|
+
if (rule.jitter !== void 0) summary.jitter = rule.jitter;
|
|
8804
|
+
if (rule.urls !== void 0) summary.urls = rule.urls;
|
|
8805
|
+
if (rule.events !== void 0) summary.events = rule.events;
|
|
8806
|
+
if (rule.schema !== void 0) summary.fields = rule.schema.length;
|
|
8807
|
+
return summary;
|
|
8808
|
+
}
|
|
8809
|
+
var workflowfileversion = 1;
|
|
8810
|
+
function editorstate(input) {
|
|
8811
|
+
const editor = { versions: input.versions, diffs: input.diffs ?? [], history: input.history, breakpoints: input.breakpoints ?? [], overrides: input.overrides, imports: input.imports, backgroundruns: input.backgroundruns ?? {}, watchdog: { ...input.watchdog.config !== void 0 ? { config: input.watchdog.config } : {}, events: input.watchdog.events } };
|
|
8812
|
+
return { version: protocolversion, editor, ...input.model !== void 0 ? { model: input.model } : {} };
|
|
8813
|
+
}
|
|
8814
|
+
function runhistoryquery(value) {
|
|
8815
|
+
if (value === void 0 || value === null) return {};
|
|
8816
|
+
const candidate = record(value);
|
|
8817
|
+
const query = {};
|
|
8818
|
+
if (candidate.workflowid !== void 0) {
|
|
8819
|
+
if (typeof candidate.workflowid !== "string" || !candidate.workflowid.trim()) throw new Error("The run history workflow filter must be a non-empty string.");
|
|
8820
|
+
query.workflowid = candidate.workflowid;
|
|
8821
|
+
}
|
|
8822
|
+
if (candidate.outcome !== void 0) {
|
|
8823
|
+
if (typeof candidate.outcome !== "string" || !candidate.outcome.trim()) throw new Error("The run history outcome filter must be a non-empty string.");
|
|
8824
|
+
query.outcome = candidate.outcome;
|
|
8825
|
+
}
|
|
8826
|
+
if (candidate.since !== void 0) {
|
|
8827
|
+
if (typeof candidate.since !== "number" || !Number.isFinite(candidate.since)) throw new Error("The run history time floor must be a finite timestamp.");
|
|
8828
|
+
query.since = candidate.since;
|
|
8829
|
+
}
|
|
8830
|
+
if (candidate.limit !== void 0) {
|
|
8831
|
+
if (typeof candidate.limit !== "number" || !Number.isInteger(candidate.limit) || candidate.limit < 1) throw new Error("The run history entry count must be a positive integer with no code ceiling.");
|
|
8832
|
+
query.limit = candidate.limit;
|
|
8833
|
+
}
|
|
8834
|
+
return query;
|
|
8835
|
+
}
|
|
8836
|
+
function runhistoryreport(input) {
|
|
8837
|
+
return { version: protocolversion, entries: input.entries, query: input.query ?? {} };
|
|
8838
|
+
}
|
|
7920
8839
|
|
|
7921
8840
|
// capture.ts
|
|
7922
8841
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -8493,6 +9412,488 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
8493
9412
|
}
|
|
8494
9413
|
}
|
|
8495
9414
|
|
|
9415
|
+
// workfloweditor.ts
|
|
9416
|
+
var palettecategories = ["actions", "controlflow", "waits", "variables", "triggers"];
|
|
9417
|
+
var palettenodes = [
|
|
9418
|
+
{ kind: "click", label: "Click an element", category: "actions", description: "Clicks the reviewed selector target." },
|
|
9419
|
+
{ kind: "type", label: "Type text", category: "actions", description: "Types the reviewed text into the target field." },
|
|
9420
|
+
{ kind: "navigate", label: "Navigate", category: "actions", description: "Navigates the tab to the reviewed url." },
|
|
9421
|
+
{ kind: "readtext", label: "Read text", category: "actions", description: "Reads the text of the target element." },
|
|
9422
|
+
{ kind: "scrapetable", label: "Scrape a table", category: "actions", description: "Extracts the reviewed table into a dataset." },
|
|
9423
|
+
{ kind: "fillform", label: "Fill a form", category: "actions", description: "Fills the reviewed form fields from a saved profile." },
|
|
9424
|
+
{ kind: "querytabs", label: "Query tabs", category: "actions", description: "Lists the tabs matching the reviewed query." },
|
|
9425
|
+
{ kind: "fetchurl", label: "Fetch a url", category: "actions", description: "Fetches the reviewed endpoint behind the call consent." },
|
|
9426
|
+
{ kind: "condition", label: "Condition", category: "controlflow", description: "Evaluates one reviewed boolean expression with no page side effect." },
|
|
9427
|
+
{ kind: "branch", label: "Branch", category: "controlflow", description: "Chooses one reviewed path by page state with a mandatory else path." },
|
|
9428
|
+
{ kind: "loop", label: "Loop a list", category: "controlflow", description: "Iterates a list variable binding the item and index per pass." },
|
|
9429
|
+
{ kind: "repeatuntil", label: "Repeat until", category: "controlflow", description: "Reruns the body until the convergence expression holds." },
|
|
9430
|
+
{ kind: "whileloop", label: "While loop", category: "controlflow", description: "Loops while the condition holds inside the reviewed bound." },
|
|
9431
|
+
{ kind: "foreach", label: "For each element", category: "controlflow", description: "Iterates the elements of the reviewed selector." },
|
|
9432
|
+
{ kind: "parallel", label: "Parallel branches", category: "controlflow", description: "Runs branches concurrently and joins them under the reviewed strategy." },
|
|
9433
|
+
{ kind: "trycatch", label: "Try catch", category: "controlflow", description: "Wraps fragile steps with a catch handler, retries and timeouts." },
|
|
9434
|
+
{ kind: "delay", label: "Delay", category: "waits", description: "Sleeps the reviewed base inside the jitter window." },
|
|
9435
|
+
{ kind: "waitelement", label: "Wait for element", category: "waits", description: "Polls the reviewed selector until appearance or timeout." },
|
|
9436
|
+
{ kind: "wait", label: "Wait", category: "waits", description: "Waits the reviewed duration." },
|
|
9437
|
+
{ kind: "waitfor", label: "Wait for target", category: "waits", description: "Waits until the reviewed target exists." },
|
|
9438
|
+
{ kind: "waittext", label: "Wait for text", category: "waits", description: "Waits until the reviewed text appears." },
|
|
9439
|
+
{ kind: "waitquiet", label: "Wait for quiet", category: "waits", description: "Waits until the page stops mutating." },
|
|
9440
|
+
{ kind: "waitload", label: "Wait for load", category: "waits", description: "Waits until the navigation settles." },
|
|
9441
|
+
{ kind: "compute", label: "Compute", category: "variables", description: "Evaluates one reviewed expression into the result variable." },
|
|
9442
|
+
{ kind: "extractvars", label: "Extract variables", category: "variables", description: "Applies the reviewed regex and stores the named captures." },
|
|
9443
|
+
{ kind: "savetemplate", label: "Save template", category: "variables", description: "Shares the reviewed step as a reusable template." },
|
|
9444
|
+
{ kind: "visitrule", label: "Visit rule", category: "triggers", description: "Fires on navigations to the reviewed origins." },
|
|
9445
|
+
{ kind: "urlrule", label: "Url rule", category: "triggers", description: "Fires when the url matches the reviewed glob pattern." },
|
|
9446
|
+
{ kind: "cronrule", label: "Cron rule", category: "triggers", description: "Fires on the reviewed five field cron schedule." },
|
|
9447
|
+
{ kind: "intervalrule", label: "Interval rule", category: "triggers", description: "Fires every reviewed period with the jitter spread." },
|
|
9448
|
+
{ kind: "webhookrule", label: "Webhook rule", category: "triggers", description: "Fires on a secret verified webhook delivery." },
|
|
9449
|
+
{ kind: "eventrule", label: "Event rule", category: "triggers", description: "Fires on the observed page events of the catalog." }
|
|
9450
|
+
];
|
|
9451
|
+
var optionschemas = {
|
|
9452
|
+
delay: [{ name: "base", kind: "number", required: true }, { name: "jitter", kind: "number" }],
|
|
9453
|
+
waitelement: [{ name: "timeout", kind: "number" }, { name: "poll", kind: "number" }],
|
|
9454
|
+
compute: [{ name: "expression", kind: "string", required: true }],
|
|
9455
|
+
extractvars: [{ name: "rule", kind: "string", required: true }],
|
|
9456
|
+
composeworkflow: [{ name: "name", kind: "string", required: true }, { name: "version", kind: "number" }],
|
|
9457
|
+
runworkflow: [{ name: "workflowid", kind: "string", required: true }, { name: "reviewed", kind: "boolean", required: true }, { name: "variables", kind: "string" }, { name: "background", kind: "boolean" }],
|
|
9458
|
+
dryrun: [{ name: "workflowid", kind: "string", required: true }],
|
|
9459
|
+
loop: [{ name: "loop", kind: "string", required: true }],
|
|
9460
|
+
repeatuntil: [{ name: "repeatuntil", kind: "string", required: true }],
|
|
9461
|
+
whileloop: [{ name: "whileloop", kind: "string", required: true }],
|
|
9462
|
+
foreach: [{ name: "foreach", kind: "string", required: true }],
|
|
9463
|
+
parallel: [{ name: "parallel", kind: "string", required: true }],
|
|
9464
|
+
trycatch: [{ name: "trycatch", kind: "string", required: true }]
|
|
9465
|
+
};
|
|
9466
|
+
function stepcategory(kind) {
|
|
9467
|
+
if (triggerkinds.includes(kind)) return "triggers";
|
|
9468
|
+
if (controlflowkinds.includes(kind)) return "controlflow";
|
|
9469
|
+
if (kind.startsWith("wait") || kind === "spawait" || kind === "delay") return "waits";
|
|
9470
|
+
if (kind === "compute" || kind === "extractvars" || kind === "savetemplate") return "variables";
|
|
9471
|
+
return "actions";
|
|
9472
|
+
}
|
|
9473
|
+
function buildsteplibrary(kinds) {
|
|
9474
|
+
return [...new Set(kinds)].sort().map((kind) => ({ kind, category: stepcategory(kind), optionschema: optionschemas[kind] ?? [] }));
|
|
9475
|
+
}
|
|
9476
|
+
var noderowheight = 96;
|
|
9477
|
+
var blockcolumnwidth = 280;
|
|
9478
|
+
var canvasoriginx = 40;
|
|
9479
|
+
function nodeidof(node) {
|
|
9480
|
+
return node.id ?? (node.step !== void 0 ? node.step.id : node.invocation !== void 0 ? node.invocation.block : "");
|
|
9481
|
+
}
|
|
9482
|
+
function layoutsizeof(nodes) {
|
|
9483
|
+
const width = Math.max(640, ...nodes.map((node) => node.x + blockcolumnwidth)) + 40;
|
|
9484
|
+
const height = Math.max(480, ...nodes.map((node) => node.y + noderowheight)) + 40;
|
|
9485
|
+
return { width, height };
|
|
9486
|
+
}
|
|
9487
|
+
function loadworkflow(record2, layout) {
|
|
9488
|
+
const blocks = record2.blocks.map((block) => ({ ...block, steps: block.steps.map((entry) => ({ ...entry })) }));
|
|
9489
|
+
const blockcolumn = (blockname) => {
|
|
9490
|
+
const index2 = blocks.findIndex((block) => block.name === blockname);
|
|
9491
|
+
return index2 < 0 ? canvasoriginx : canvasoriginx + (index2 + 1) * blockcolumnwidth;
|
|
9492
|
+
};
|
|
9493
|
+
const invocationcount = /* @__PURE__ */ new Map();
|
|
9494
|
+
const nodes = [];
|
|
9495
|
+
const edges = [];
|
|
9496
|
+
let index = 0;
|
|
9497
|
+
while (index < record2.steps.length) {
|
|
9498
|
+
const step = record2.steps[index];
|
|
9499
|
+
for (const binding of step.bindings ?? []) edges.push({ from: binding.stepid, to: step.id, variable: binding.variable, kind: binding.kind, ...binding.path !== void 0 ? { path: binding.path } : {} });
|
|
9500
|
+
if (step.block === void 0) {
|
|
9501
|
+
const { bindings, block, params: params2, ...rest } = step;
|
|
9502
|
+
void bindings;
|
|
9503
|
+
void block;
|
|
9504
|
+
void params2;
|
|
9505
|
+
nodes.push({ step: { ...rest }, x: canvasoriginx, y: 60 + nodes.length * noderowheight });
|
|
9506
|
+
index += 1;
|
|
9507
|
+
continue;
|
|
9508
|
+
}
|
|
9509
|
+
const blockname = step.block;
|
|
9510
|
+
let end = index;
|
|
9511
|
+
while (end < record2.steps.length && record2.steps[end].block === blockname) end += 1;
|
|
9512
|
+
const region = record2.steps.slice(index, end);
|
|
9513
|
+
const count = (invocationcount.get(blockname) ?? 0) + 1;
|
|
9514
|
+
invocationcount.set(blockname, count);
|
|
9515
|
+
const params = region.flatMap((entry) => entry.params ?? []);
|
|
9516
|
+
nodes.push({ id: count === 1 ? blockname : `${blockname}${count}`, invocation: { block: blockname, label: blockname, ...params.length > 0 ? { params: params.map((param) => ({ ...param })) } : {} }, x: blockcolumn(blockname), y: 60 + nodes.length * noderowheight });
|
|
9517
|
+
index = end;
|
|
9518
|
+
}
|
|
9519
|
+
const size = layouttypeof(nodes, layout);
|
|
9520
|
+
const model = { workflowid: record2.id, name: record2.name, version: record2.version, origins: [...record2.origins], nodes, edges, blocks, layout: size, minimap: emptyminimap(), dirty: false };
|
|
9521
|
+
return { ...model, minimap: renderminimap(model).minimap };
|
|
9522
|
+
}
|
|
9523
|
+
function layouttypeof(nodes, layout) {
|
|
9524
|
+
const size = layoutsizeof(nodes);
|
|
9525
|
+
if (!layout) return { width: size.width, height: size.height, viewportx: 0, viewporty: 0, zoom: 1 };
|
|
9526
|
+
return { width: Math.max(size.width, layout.width), height: Math.max(size.height, layout.height), viewportx: layout.viewportx, viewporty: layout.viewporty, zoom: layout.zoom };
|
|
9527
|
+
}
|
|
9528
|
+
function emptyminimap() {
|
|
9529
|
+
return { width: 160, height: 100, scale: 0, zoom: 1, viewport: { x: 0, y: 0, width: 0, height: 0 } };
|
|
9530
|
+
}
|
|
9531
|
+
function saveworkflow(model, input) {
|
|
9532
|
+
if (typeof model.name !== "string" || !model.name.trim()) throw new Error("The workflow name must be a non-empty string.");
|
|
9533
|
+
if (typeof model.version !== "number" || !Number.isInteger(model.version) || model.version < 1) throw new Error("The workflow version must be a positive integer.");
|
|
9534
|
+
if (!Array.isArray(model.origins) || model.origins.length === 0) throw new Error("A workflow needs at least one granted HTTPS origin.");
|
|
9535
|
+
const ids = /* @__PURE__ */ new Set();
|
|
9536
|
+
for (const node of model.nodes) {
|
|
9537
|
+
if (node.step === void 0 === (node.invocation === void 0)) throw new Error("Every canvas node must be exactly one workflow step or one block invocation.");
|
|
9538
|
+
const id = nodeidof(node);
|
|
9539
|
+
if (!id || ids.has(id)) throw new Error(`The canvas node id ${id || "(empty)"} must be unique.`);
|
|
9540
|
+
ids.add(id);
|
|
9541
|
+
}
|
|
9542
|
+
const positionof = /* @__PURE__ */ new Map();
|
|
9543
|
+
let position = 0;
|
|
9544
|
+
for (const node of model.nodes) {
|
|
9545
|
+
if (node.step !== void 0) {
|
|
9546
|
+
positionof.set(node.step.id, position);
|
|
9547
|
+
position += 1;
|
|
9548
|
+
continue;
|
|
9549
|
+
}
|
|
9550
|
+
const block = model.blocks.find((entry) => entry.name === node.invocation?.block);
|
|
9551
|
+
if (!block) throw new Error(`The block ${node.invocation?.block ?? ""} of the canvas has no definition.`);
|
|
9552
|
+
const walk = (entries2) => {
|
|
9553
|
+
for (const entry of entries2) {
|
|
9554
|
+
if ("kind" in entry && "label" in entry && !("block" in entry)) {
|
|
9555
|
+
positionof.set(entry.id, position);
|
|
9556
|
+
position += 1;
|
|
9557
|
+
continue;
|
|
9558
|
+
}
|
|
9559
|
+
const nested = model.blocks.find((candidate) => candidate.name === entry.block);
|
|
9560
|
+
if (!nested) throw new Error(`The block ${entry.block} of the canvas has no definition.`);
|
|
9561
|
+
walk(nested.steps);
|
|
9562
|
+
}
|
|
9563
|
+
};
|
|
9564
|
+
walk(block.steps);
|
|
9565
|
+
}
|
|
9566
|
+
for (const edge of model.edges) {
|
|
9567
|
+
if (!positionof.has(edge.from)) throw new Error(`The edge of ${edge.variable} references the unknown source step ${edge.from}.`);
|
|
9568
|
+
if (!positionof.has(edge.to)) throw new Error(`The edge of ${edge.variable} references the unknown target step ${edge.to}.`);
|
|
9569
|
+
if (positionof.get(edge.from) >= positionof.get(edge.to)) throw new Error(`The edge of ${edge.variable} runs backwards from ${edge.from} into ${edge.to} and would form a cycle.`);
|
|
9570
|
+
}
|
|
9571
|
+
const bindingsof = (stepid) => model.edges.filter((edge) => edge.to === stepid).map((edge) => ({ variable: edge.variable, kind: edge.kind, stepid: edge.from, ...edge.path !== void 0 ? { path: edge.path } : {} }));
|
|
9572
|
+
const entries = [];
|
|
9573
|
+
const attached = /* @__PURE__ */ new Map();
|
|
9574
|
+
for (const node of model.nodes) {
|
|
9575
|
+
if (node.invocation !== void 0) {
|
|
9576
|
+
entries.push({ ...node.invocation });
|
|
9577
|
+
continue;
|
|
9578
|
+
}
|
|
9579
|
+
const step = node.step;
|
|
9580
|
+
const bindings = bindingsof(step.id);
|
|
9581
|
+
const { block, params, ...rest } = { ...step, ...bindings.length > 0 ? { bindings } : {} };
|
|
9582
|
+
void params;
|
|
9583
|
+
const carried = rest;
|
|
9584
|
+
if (block !== void 0) {
|
|
9585
|
+
if (!model.blocks.some((candidate) => candidate.name === block)) throw new Error(`The step ${step.id} attaches to the unknown block ${block}.`);
|
|
9586
|
+
const list = attached.get(block) ?? [];
|
|
9587
|
+
list.push(carried);
|
|
9588
|
+
attached.set(block, list);
|
|
9589
|
+
continue;
|
|
9590
|
+
}
|
|
9591
|
+
entries.push(carried);
|
|
9592
|
+
}
|
|
9593
|
+
const blocks = model.blocks.map((block) => {
|
|
9594
|
+
const snapped = attached.get(block.name) ?? [];
|
|
9595
|
+
const snappedids = new Set(snapped.map((step) => step.id));
|
|
9596
|
+
const carried = [];
|
|
9597
|
+
for (const entry of block.steps) {
|
|
9598
|
+
if ("kind" in entry && "label" in entry && !("block" in entry) && snappedids.has(entry.id)) continue;
|
|
9599
|
+
carried.push(entry);
|
|
9600
|
+
}
|
|
9601
|
+
const steps = [...carried, ...snapped];
|
|
9602
|
+
const withbindings = [];
|
|
9603
|
+
for (const entry of steps) {
|
|
9604
|
+
if (!("kind" in entry && "label" in entry && !("block" in entry))) {
|
|
9605
|
+
withbindings.push(entry);
|
|
9606
|
+
continue;
|
|
9607
|
+
}
|
|
9608
|
+
const bindings = bindingsof(entry.id);
|
|
9609
|
+
const { block: inner, params, ...rest } = { ...entry, ...bindings.length > 0 ? { bindings } : {} };
|
|
9610
|
+
void inner;
|
|
9611
|
+
void params;
|
|
9612
|
+
withbindings.push(rest);
|
|
9613
|
+
}
|
|
9614
|
+
return { ...block, steps: withbindings };
|
|
9615
|
+
});
|
|
9616
|
+
const composed = composeworkflow({ id: model.workflowid, name: model.name, version: model.version, origins: [...model.origins], steps: entries, blocks: blocks.map((block) => ({ ...block })), now: input.now, ...input.kindallowed !== void 0 ? { kindallowed: input.kindallowed } : {}, ...input.riskof !== void 0 ? { riskof: input.riskof } : {} });
|
|
9617
|
+
const checked = validateworkflow(composed, input.kindallowed !== void 0 ? { kindallowed: input.kindallowed } : {});
|
|
9618
|
+
if (!checked.allowed) throw new Error(checked.reason ?? "The canvas model failed the workflow grammar.");
|
|
9619
|
+
return composed;
|
|
9620
|
+
}
|
|
9621
|
+
function renderminimap(model, width = 160, height = 100) {
|
|
9622
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) throw new Error("The mini map size must be positive.");
|
|
9623
|
+
const canvaswidth = Math.max(1, model.layout.width);
|
|
9624
|
+
const canvasheight = Math.max(1, model.layout.height);
|
|
9625
|
+
const scale = Math.min(width / canvaswidth, height / canvasheight);
|
|
9626
|
+
const zoom = model.layout.zoom > 0 ? model.layout.zoom : 1;
|
|
9627
|
+
const visiblewidth = canvaswidth / zoom;
|
|
9628
|
+
const visibleheight = canvasheight / zoom;
|
|
9629
|
+
const viewport = {
|
|
9630
|
+
x: Math.max(0, Math.min(model.layout.viewportx, canvaswidth)) * scale,
|
|
9631
|
+
y: Math.max(0, Math.min(model.layout.viewporty, canvasheight)) * scale,
|
|
9632
|
+
width: visiblewidth * scale,
|
|
9633
|
+
height: visibleheight * scale
|
|
9634
|
+
};
|
|
9635
|
+
const nodes = model.nodes.map((node) => ({ id: nodeidof(node), x: node.x * scale, y: node.y * scale }));
|
|
9636
|
+
return { minimap: { width, height, scale, zoom, viewport }, nodes };
|
|
9637
|
+
}
|
|
9638
|
+
function runtobreakpoint(input) {
|
|
9639
|
+
const cursor = input.cursor !== void 0 && Number.isInteger(input.cursor) && input.cursor >= 0 ? input.cursor : 0;
|
|
9640
|
+
const marked = new Set(input.breakpoints);
|
|
9641
|
+
for (let index = cursor; index < input.record.steps.length; index += 1) {
|
|
9642
|
+
const step = input.record.steps[index];
|
|
9643
|
+
if (step.breakpoint === true || marked.has(step.id)) {
|
|
9644
|
+
return { until: index, pausat: step.id, remaining: input.record.steps.length - index };
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
return { until: input.record.steps.length, pausat: void 0, remaining: 0 };
|
|
9648
|
+
}
|
|
9649
|
+
function diffversions(from, to, now) {
|
|
9650
|
+
const fromsteps = new Map(from.steps.map((step) => [step.id, step]));
|
|
9651
|
+
const tosteps = new Map(to.steps.map((step) => [step.id, step]));
|
|
9652
|
+
const added = [];
|
|
9653
|
+
const removed = [];
|
|
9654
|
+
const changed = [];
|
|
9655
|
+
for (const step of to.steps) {
|
|
9656
|
+
const prior = fromsteps.get(step.id);
|
|
9657
|
+
if (!prior) {
|
|
9658
|
+
added.push({ stepid: step.id, kind: step.kind, label: step.label });
|
|
9659
|
+
continue;
|
|
9660
|
+
}
|
|
9661
|
+
const changes = [];
|
|
9662
|
+
if (prior.label !== step.label) changes.push("label");
|
|
9663
|
+
if (prior.kind !== step.kind) changes.push("kind");
|
|
9664
|
+
if (prior.target !== step.target) changes.push("target");
|
|
9665
|
+
if (prior.value !== step.value) changes.push("value");
|
|
9666
|
+
if (prior.options !== step.options) changes.push("options");
|
|
9667
|
+
if (JSON.stringify(prior.expression) !== JSON.stringify(step.expression)) changes.push("expression");
|
|
9668
|
+
if (JSON.stringify(prior.extract) !== JSON.stringify(step.extract)) changes.push("extract");
|
|
9669
|
+
if (JSON.stringify(prior.bindings) !== JSON.stringify(step.bindings)) changes.push("bindings");
|
|
9670
|
+
if (changes.length > 0) changed.push({ stepid: step.id, kind: step.kind, label: step.label, changes });
|
|
9671
|
+
}
|
|
9672
|
+
for (const step of from.steps) {
|
|
9673
|
+
if (!tosteps.has(step.id)) removed.push({ stepid: step.id, kind: step.kind, label: step.label });
|
|
9674
|
+
}
|
|
9675
|
+
return { workflowid: to.id, from: from.version, to: to.version, added, removed, changed, at: now };
|
|
9676
|
+
}
|
|
9677
|
+
function exportworkflow(record2, format, note, now) {
|
|
9678
|
+
const file = { format: workflowfileversion, exportedat: now ?? Date.now(), workflow: record2, ...note !== void 0 && note.trim() !== "" ? { note } : {}, templates: [] };
|
|
9679
|
+
return { format, contents: serializefile(file, format), file };
|
|
9680
|
+
}
|
|
9681
|
+
function shareworkflow(record2, templates, format, note, now) {
|
|
9682
|
+
const file = { format: workflowfileversion, exportedat: now ?? Date.now(), workflow: record2, ...note !== void 0 && note.trim() !== "" ? { note } : {}, templates: templates.map((template) => ({ ...template })) };
|
|
9683
|
+
return { format, contents: serializefile(file, format), file };
|
|
9684
|
+
}
|
|
9685
|
+
function importworkflow(input) {
|
|
9686
|
+
const format = input.format ?? (input.contents.trimStart().startsWith("{") ? "json" : "yaml");
|
|
9687
|
+
const parsed = parsefile(input.contents, format);
|
|
9688
|
+
if (parsed.format !== workflowfileversion) throw new Error(`The workflow file format ${String(parsed.format)} is not the reviewed format ${workflowfileversion}.`);
|
|
9689
|
+
const candidate = parsed.workflow;
|
|
9690
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("The workflow file carries no workflow record.");
|
|
9691
|
+
const fields = candidate;
|
|
9692
|
+
const stepsvalue = fields.steps;
|
|
9693
|
+
if (!Array.isArray(stepsvalue) || stepsvalue.length === 0) throw new Error("An imported workflow needs at least one step.");
|
|
9694
|
+
const steps = [];
|
|
9695
|
+
for (const entry of stepsvalue) {
|
|
9696
|
+
const step = workflowstepof(entry);
|
|
9697
|
+
if (step) {
|
|
9698
|
+
steps.push(step);
|
|
9699
|
+
continue;
|
|
9700
|
+
}
|
|
9701
|
+
throw new Error("Every imported workflow entry must be a reviewed step.");
|
|
9702
|
+
}
|
|
9703
|
+
const composed = composeworkflow({
|
|
9704
|
+
id: typeof fields.id === "string" && fields.id.trim() !== "" ? fields.id : crypto.randomUUID(),
|
|
9705
|
+
name: typeof fields.name === "string" ? fields.name : "",
|
|
9706
|
+
version: typeof fields.version === "number" ? fields.version : 1,
|
|
9707
|
+
origins: Array.isArray(fields.origins) ? fields.origins.filter((origin) => typeof origin === "string") : [],
|
|
9708
|
+
steps,
|
|
9709
|
+
now: input.now ?? Date.now(),
|
|
9710
|
+
...input.kindallowed !== void 0 ? { kindallowed: input.kindallowed } : {},
|
|
9711
|
+
...input.riskof !== void 0 ? { riskof: input.riskof } : {}
|
|
9712
|
+
});
|
|
9713
|
+
const templatesvalue = parsed.templates;
|
|
9714
|
+
if (templatesvalue !== void 0 && !Array.isArray(templatesvalue)) throw new Error("The packed templates of the workflow file must be a list.");
|
|
9715
|
+
const templates = [];
|
|
9716
|
+
for (const entry of templatesvalue ?? []) {
|
|
9717
|
+
const template = steptemplateof(entry);
|
|
9718
|
+
if (!template) throw new Error("A packed template of the workflow file does not carry one reviewed step.");
|
|
9719
|
+
templates.push(template);
|
|
9720
|
+
}
|
|
9721
|
+
const record2 = { ...composed, reviewstate: "pending" };
|
|
9722
|
+
return { record: record2, templates, file: { ...parsed, workflow: record2 } };
|
|
9723
|
+
}
|
|
9724
|
+
function originmatches(pattern, origin) {
|
|
9725
|
+
if (pattern === origin) return true;
|
|
9726
|
+
const glob = pattern.replace(/\./g, "\\.").replace(/\*/g, "[^.]+");
|
|
9727
|
+
if (!glob.startsWith("https://")) return false;
|
|
9728
|
+
return new RegExp(`^${glob}$`).test(origin);
|
|
9729
|
+
}
|
|
9730
|
+
function applyoverride(record2, override) {
|
|
9731
|
+
const matching = record2.origins.filter((origin) => originmatches(override.pattern, origin));
|
|
9732
|
+
if (matching.length === 0) throw new Error(`The override pattern ${override.pattern} matches none of the workflow origins ${record2.origins.join(", ")}.`);
|
|
9733
|
+
const knobs = /* @__PURE__ */ new Set(["loopbound", "stepms", "runms", "waitms", "delaybase"]);
|
|
9734
|
+
for (const knob of Object.keys(override.deltas)) {
|
|
9735
|
+
if (!knobs.has(knob)) throw new Error(`The override knob ${knob} is not one of the reviewed knobs: ${[...knobs].join(", ")}.`);
|
|
9736
|
+
if (typeof override.deltas[knob] !== "number" || !Number.isFinite(override.deltas[knob]) || override.deltas[knob] <= 0) throw new Error(`The override delta of ${knob} must be a positive number with no code ceiling.`);
|
|
9737
|
+
}
|
|
9738
|
+
const apply = (step) => {
|
|
9739
|
+
if (Object.keys(override.deltas).length === 0) return step;
|
|
9740
|
+
let payload = {};
|
|
9741
|
+
try {
|
|
9742
|
+
payload = step.options !== void 0 ? JSON.parse(step.options) : {};
|
|
9743
|
+
} catch {
|
|
9744
|
+
payload = {};
|
|
9745
|
+
}
|
|
9746
|
+
const bodyof = (key) => payload[key] !== void 0 && typeof payload[key] === "object" && !Array.isArray(payload[key]) ? payload[key] : {};
|
|
9747
|
+
if (override.deltas.loopbound !== void 0 && ["loop", "repeatuntil", "whileloop"].includes(step.kind)) {
|
|
9748
|
+
const body = bodyof(step.kind);
|
|
9749
|
+
body.bound = override.deltas.loopbound;
|
|
9750
|
+
payload[step.kind] = body;
|
|
9751
|
+
}
|
|
9752
|
+
if ((override.deltas.stepms !== void 0 || override.deltas.runms !== void 0) && step.kind === "trycatch") {
|
|
9753
|
+
const body = bodyof("trycatch");
|
|
9754
|
+
const timeout = body.timeout !== void 0 && typeof body.timeout === "object" && !Array.isArray(body.timeout) ? body.timeout : {};
|
|
9755
|
+
if (override.deltas.stepms !== void 0) timeout.stepms = override.deltas.stepms;
|
|
9756
|
+
if (override.deltas.runms !== void 0) timeout.runms = override.deltas.runms;
|
|
9757
|
+
body.timeout = timeout;
|
|
9758
|
+
payload.trycatch = body;
|
|
9759
|
+
}
|
|
9760
|
+
if (override.deltas.waitms !== void 0 && step.kind === "waitelement") {
|
|
9761
|
+
payload.timeout = override.deltas.waitms;
|
|
9762
|
+
}
|
|
9763
|
+
if (override.deltas.delaybase !== void 0 && step.kind === "delay") {
|
|
9764
|
+
payload.base = override.deltas.delaybase;
|
|
9765
|
+
}
|
|
9766
|
+
const changed = Object.keys(payload).length > 0;
|
|
9767
|
+
return changed ? { ...step, options: JSON.stringify(payload) } : step;
|
|
9768
|
+
};
|
|
9769
|
+
return { ...record2, steps: record2.steps.map(apply) };
|
|
9770
|
+
}
|
|
9771
|
+
function serializefile(file, format) {
|
|
9772
|
+
if (format === "json") return JSON.stringify(file, null, 2);
|
|
9773
|
+
return yamlvalue(file, 0).join("\n") + "\n";
|
|
9774
|
+
}
|
|
9775
|
+
function parsefile(contents, format) {
|
|
9776
|
+
if (format === "json") {
|
|
9777
|
+
const parsed = JSON.parse(contents);
|
|
9778
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("The workflow file is not a json object.");
|
|
9779
|
+
return parsed;
|
|
9780
|
+
}
|
|
9781
|
+
const lines = contents.split(/\r?\n/).map((line) => line.replace(/\t/g, " ")).filter((line) => line.trim() !== "" && !line.trim().startsWith("#"));
|
|
9782
|
+
if (lines.length === 0) throw new Error("The yaml workflow file is empty.");
|
|
9783
|
+
const { value, next } = yamlblock(lines, 0, indentof(lines[0]));
|
|
9784
|
+
if (next < lines.length) throw new Error("The yaml workflow file carries content outside the documented subset.");
|
|
9785
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("The yaml workflow file is not a mapping.");
|
|
9786
|
+
return value;
|
|
9787
|
+
}
|
|
9788
|
+
function indentof(line) {
|
|
9789
|
+
const match = /^ */.exec(line);
|
|
9790
|
+
return match ? match[0].length : 0;
|
|
9791
|
+
}
|
|
9792
|
+
function yamlscalar(value) {
|
|
9793
|
+
if (value === null || value === void 0) return "null";
|
|
9794
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
9795
|
+
return JSON.stringify(String(value));
|
|
9796
|
+
}
|
|
9797
|
+
function yamlvalue(value, indent) {
|
|
9798
|
+
const pad = " ".repeat(indent);
|
|
9799
|
+
if (value === null || value === void 0 || typeof value !== "object") return [`${pad}${yamlscalar(value)}`];
|
|
9800
|
+
if (Array.isArray(value)) {
|
|
9801
|
+
if (value.length === 0) return [`${pad}[]`];
|
|
9802
|
+
const lines2 = [];
|
|
9803
|
+
for (const item of value) {
|
|
9804
|
+
if (item !== null && typeof item === "object") {
|
|
9805
|
+
lines2.push(`${pad}-`);
|
|
9806
|
+
lines2.push(...yamlvalue(item, indent + 2));
|
|
9807
|
+
} else {
|
|
9808
|
+
lines2.push(`${pad}- ${yamlscalar(item)}`);
|
|
9809
|
+
}
|
|
9810
|
+
}
|
|
9811
|
+
return lines2;
|
|
9812
|
+
}
|
|
9813
|
+
const entries = Object.entries(value);
|
|
9814
|
+
if (entries.length === 0) return [`${pad}{}`];
|
|
9815
|
+
const lines = [];
|
|
9816
|
+
for (const [key, entry] of entries) {
|
|
9817
|
+
if (entry !== null && typeof entry === "object") {
|
|
9818
|
+
if (Array.isArray(entry) && entry.length === 0) {
|
|
9819
|
+
lines.push(`${pad}${key}: []`);
|
|
9820
|
+
continue;
|
|
9821
|
+
}
|
|
9822
|
+
if (!Array.isArray(entry) && Object.keys(entry).length === 0) {
|
|
9823
|
+
lines.push(`${pad}${key}: {}`);
|
|
9824
|
+
continue;
|
|
9825
|
+
}
|
|
9826
|
+
lines.push(`${pad}${key}:`);
|
|
9827
|
+
lines.push(...yamlvalue(entry, indent + 2));
|
|
9828
|
+
} else {
|
|
9829
|
+
lines.push(`${pad}${key}: ${yamlscalar(entry)}`);
|
|
9830
|
+
}
|
|
9831
|
+
}
|
|
9832
|
+
return lines;
|
|
9833
|
+
}
|
|
9834
|
+
function yamlblock(lines, start, indent) {
|
|
9835
|
+
const first = lines[start];
|
|
9836
|
+
if (/^\s*-\s/.test(first) || /^\s*-$/.test(first)) {
|
|
9837
|
+
const items = [];
|
|
9838
|
+
let index2 = start;
|
|
9839
|
+
while (index2 < lines.length) {
|
|
9840
|
+
const line = lines[index2];
|
|
9841
|
+
if (indentof(line) !== indent || !/^\s*-\s?/.test(line)) break;
|
|
9842
|
+
const rest = line.slice(indent + 1).trim();
|
|
9843
|
+
if (rest !== "") {
|
|
9844
|
+
items.push(yamlscalarvalue(rest));
|
|
9845
|
+
index2 += 1;
|
|
9846
|
+
continue;
|
|
9847
|
+
}
|
|
9848
|
+
const nested = yamlblock(lines, index2 + 1, indent + 2);
|
|
9849
|
+
items.push(nested.value);
|
|
9850
|
+
index2 = nested.next;
|
|
9851
|
+
}
|
|
9852
|
+
return { value: items, next: index2 };
|
|
9853
|
+
}
|
|
9854
|
+
const mapping = {};
|
|
9855
|
+
let index = start;
|
|
9856
|
+
while (index < lines.length) {
|
|
9857
|
+
const line = lines[index];
|
|
9858
|
+
if (indentof(line) !== indent) break;
|
|
9859
|
+
const match = /^([A-Za-z][A-Za-z0-9]*):(?:\s(.*))?$/.exec(line.slice(indent));
|
|
9860
|
+
if (!match) break;
|
|
9861
|
+
const key = match[1];
|
|
9862
|
+
const rest = match[2];
|
|
9863
|
+
if (rest !== void 0 && rest !== "") {
|
|
9864
|
+
if (rest === "[]") {
|
|
9865
|
+
mapping[key] = [];
|
|
9866
|
+
index += 1;
|
|
9867
|
+
continue;
|
|
9868
|
+
}
|
|
9869
|
+
if (rest === "{}") {
|
|
9870
|
+
mapping[key] = {};
|
|
9871
|
+
index += 1;
|
|
9872
|
+
continue;
|
|
9873
|
+
}
|
|
9874
|
+
mapping[key] = yamlscalarvalue(rest);
|
|
9875
|
+
index += 1;
|
|
9876
|
+
continue;
|
|
9877
|
+
}
|
|
9878
|
+
const nested = yamlblock(lines, index + 1, indent + 2);
|
|
9879
|
+
mapping[key] = nested.value;
|
|
9880
|
+
index = nested.next;
|
|
9881
|
+
}
|
|
9882
|
+
if (index === start) throw new Error("The yaml workflow file left the documented subset.");
|
|
9883
|
+
return { value: mapping, next: index };
|
|
9884
|
+
}
|
|
9885
|
+
function yamlscalarvalue(text2) {
|
|
9886
|
+
if (text2.startsWith('"')) {
|
|
9887
|
+
const parsed = JSON.parse(text2);
|
|
9888
|
+
return typeof parsed === "string" ? parsed : text2;
|
|
9889
|
+
}
|
|
9890
|
+
if (text2 === "true") return true;
|
|
9891
|
+
if (text2 === "false") return false;
|
|
9892
|
+
if (text2 === "null") return null;
|
|
9893
|
+
if (/^-?\d+(?:\.\d+)?$/.test(text2)) return Number(text2);
|
|
9894
|
+
return text2;
|
|
9895
|
+
}
|
|
9896
|
+
|
|
8496
9897
|
// extension/pagesession.ts
|
|
8497
9898
|
function capturepagestate(sections) {
|
|
8498
9899
|
const wants = (section) => sections.includes(section);
|
|
@@ -9968,6 +11369,8 @@ async function executewatchstep(step, session, plan, tabid2, origin) {
|
|
|
9968
11369
|
const record2 = entry;
|
|
9969
11370
|
await memory.addmutationevent({ ...record2, sessionid: session.id });
|
|
9970
11371
|
}
|
|
11372
|
+
void evaluatepagetriggers("mutate").catch(() => {
|
|
11373
|
+
});
|
|
9971
11374
|
}
|
|
9972
11375
|
if (step.kind === "watchfocus") {
|
|
9973
11376
|
for (const entry of detailarray(output.details, "events")) {
|
|
@@ -9975,6 +11378,8 @@ async function executewatchstep(step, session, plan, tabid2, origin) {
|
|
|
9975
11378
|
const record2 = entry;
|
|
9976
11379
|
await memory.addfocusevent({ ...record2, sessionid: session.id });
|
|
9977
11380
|
}
|
|
11381
|
+
void evaluatepagetriggers("focus").catch(() => {
|
|
11382
|
+
});
|
|
9978
11383
|
}
|
|
9979
11384
|
if (step.kind === "watchbanner") {
|
|
9980
11385
|
for (const entry of detailarray(output.details, "banners")) {
|
|
@@ -9982,6 +11387,8 @@ async function executewatchstep(step, session, plan, tabid2, origin) {
|
|
|
9982
11387
|
const record2 = entry;
|
|
9983
11388
|
await memory.addbanner({ ...record2, sessionid: session.id });
|
|
9984
11389
|
}
|
|
11390
|
+
void evaluatepagetriggers("banner").catch(() => {
|
|
11391
|
+
});
|
|
9985
11392
|
}
|
|
9986
11393
|
await audit("watch", `Watch ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
9987
11394
|
return { output, watch };
|
|
@@ -10109,6 +11516,8 @@ async function tracktabupdate(tabid2, changeinfo) {
|
|
|
10109
11516
|
}
|
|
10110
11517
|
chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
|
|
10111
11518
|
void tracktabupdate(tabid2, changeinfo);
|
|
11519
|
+
if (typeof changeinfo.url === "string" && changeinfo.url.startsWith("https://")) void evaluatenavigationtriggers(changeinfo.url).catch(() => {
|
|
11520
|
+
});
|
|
10112
11521
|
});
|
|
10113
11522
|
chrome.tabs.onActivated.addListener((activeinfo) => {
|
|
10114
11523
|
void recordtabwatchevent("activated", activeinfo.tabId);
|
|
@@ -12791,6 +14200,10 @@ async function executetimelinestep(step, session, plan, tabid2, origin) {
|
|
|
12791
14200
|
const watcherstate = activetimelinewatchers.get(watchid);
|
|
12792
14201
|
activetimelinewatchers.delete(watchid);
|
|
12793
14202
|
await memory.closewatch(watchid, Date.now());
|
|
14203
|
+
if (step.kind === "watchconsole" || step.kind === "watcherrors") {
|
|
14204
|
+
void evaluatepagetriggers(step.kind === "watchconsole" ? "console" : "error").catch(() => {
|
|
14205
|
+
});
|
|
14206
|
+
}
|
|
12794
14207
|
await audit("timeline", `Watcher ${watchid} detached cleanly after its reviewed window of ${watchwindow} milliseconds.`, extra);
|
|
12795
14208
|
if (watcherstate?.cancelled) {
|
|
12796
14209
|
await audit("timeline", `Watcher ${watchid} cancelled on run cancel or the killswitch; the captured window is discarded.`, extra);
|
|
@@ -14233,12 +15646,24 @@ function workflowstepofentry(value) {
|
|
|
14233
15646
|
async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
|
|
14234
15647
|
const options = stepoptions2(step);
|
|
14235
15648
|
const workflowid = typeof options.workflowid === "string" ? options.workflowid : "";
|
|
14236
|
-
const
|
|
14237
|
-
if (!
|
|
14238
|
-
for (const workfloworigin of
|
|
15649
|
+
const storedrecord = await memory.getworkflowrecord(workflowid);
|
|
15650
|
+
if (!storedrecord) throw new Error(`No composed workflow matches ${workflowid || "the reviewed id"}.`);
|
|
15651
|
+
for (const workfloworigin of storedrecord.origins) {
|
|
14239
15652
|
if (!origingranted(session, workfloworigin)) throw new Error(`The workflow origin ${workfloworigin} falls outside the session grants.`);
|
|
14240
15653
|
}
|
|
14241
|
-
const
|
|
15654
|
+
const reviewgate = runreviewgranted(storedrecord);
|
|
15655
|
+
if (!reviewgate.allowed) throw new Error(reviewgate.reason ?? "The workflow stays unreviewed until its import or rollback review approves the expanded step list.");
|
|
15656
|
+
let record2 = storedrecord;
|
|
15657
|
+
for (const override of await memory.listsiteoverrides(storedrecord.id)) {
|
|
15658
|
+
try {
|
|
15659
|
+
record2 = applyoverride(record2, override);
|
|
15660
|
+
await audit("workflow", `Applied the per site override ${override.id} of the pattern ${override.pattern} to the workflow ${record2.name}: ${Object.entries(override.deltas).map(([knob, delta]) => `${knob} ${delta}`).join(", ") || "no delta"}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15661
|
+
} catch (error) {
|
|
15662
|
+
await audit("workflow", `Skipped the per site override ${override.id} of the pattern ${override.pattern} for the workflow ${storedrecord.name}: ${error instanceof Error ? error.message : String(error)}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15663
|
+
}
|
|
15664
|
+
}
|
|
15665
|
+
const backgroundflag = (await memory.getbackgroundruns())[record2.id] === true || options.background === true;
|
|
15666
|
+
const run = { ...newworkflowrun({ workflowid: record2.id, dryrun: dry, now: Date.now() }), ...backgroundflag === true && !dry ? { background: true } : {} };
|
|
14242
15667
|
await memory.setworkflowrun(run);
|
|
14243
15668
|
await memory.setrunscopes(run.id, runscopes(options.variables));
|
|
14244
15669
|
if (dry) {
|
|
@@ -14250,6 +15675,30 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
|
|
|
14250
15675
|
await audit("workflow", `The dry run of the workflow ${record2.name} evaluated ${evaluated.log.length} step${evaluated.log.length === 1 ? "" : "s"} read only; ${refused} step${refused === 1 ? "" : "s"} refused for lacking a read only projection and nothing was mutated.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
14251
15676
|
return { ok: true, summary: `The dry run evaluated ${evaluated.log.length} steps read only; ${refused} refused for lacking a read only projection.`, details: { runid: run.id, state: evaluated.run.state, dryrun: true, executed: evaluated.log.length - refused, refused, total: record2.steps.length } };
|
|
14252
15677
|
}
|
|
15678
|
+
if (run.background === true) {
|
|
15679
|
+
await audit("workflow", `Started the background run ${run.id} of the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} reviewed steps; the panel may close, every step checkpoints and the worker wake restores the run through the same gates.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15680
|
+
void performworkflowrun({ record: record2, run, step, session, plan, tabid: tabid2, origin, options, dry }).catch(async (error) => {
|
|
15681
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
15682
|
+
await memory.setworkflowrun({ ...run, state: "failed", endedat: Date.now(), failreason: reason });
|
|
15683
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: "failed", steps: run.cursor, total: record2.steps.length, duration: Date.now() - run.startedat, cause: runcauseof(options), startedat: run.startedat, endedat: Date.now() });
|
|
15684
|
+
await audit("error", `The background run ${run.id} of the workflow ${record2.name} failed: ${reason}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15685
|
+
await refreshbadge().catch(() => {
|
|
15686
|
+
});
|
|
15687
|
+
});
|
|
15688
|
+
return { ok: true, summary: `The workflow run ${run.id} started in the background with ${record2.steps.length} reviewed steps and the panel may close; the checkpoints restore it on every worker wake.`, details: { runid: run.id, state: "running", background: true, executed: 0, total: record2.steps.length } };
|
|
15689
|
+
}
|
|
15690
|
+
return await performworkflowrun({ record: record2, run, step, session, plan, tabid: tabid2, origin, options, dry });
|
|
15691
|
+
}
|
|
15692
|
+
function runcauseof(options) {
|
|
15693
|
+
const variables = options.variables;
|
|
15694
|
+
if (variables !== null && typeof variables === "object" && !Array.isArray(variables)) {
|
|
15695
|
+
const cause = variables.triggercause;
|
|
15696
|
+
if (typeof cause === "string" && cause.trim() !== "") return cause;
|
|
15697
|
+
}
|
|
15698
|
+
return "manual";
|
|
15699
|
+
}
|
|
15700
|
+
async function performworkflowrun(input) {
|
|
15701
|
+
const { record: record2, run, step, session, plan, tabid: tabid2, origin, options, dry } = input;
|
|
14253
15702
|
const guards = { cancelled: false };
|
|
14254
15703
|
activeworkflowruns.set(run.id, guards);
|
|
14255
15704
|
await audit("workflow", `Started the run ${run.id} of the workflow ${record2.name} version ${record2.version} with ${record2.steps.length} reviewed steps; every step passes the session, plan review and origin gates.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
@@ -14331,6 +15780,8 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
|
|
|
14331
15780
|
}
|
|
14332
15781
|
const executed = result.run.cursor;
|
|
14333
15782
|
await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "run", detail: `Ran the workflow ${record2.name}`, runid: run.id, executed, total: record2.steps.length }, Date.now()));
|
|
15783
|
+
const endedat = result.run.endedat ?? Date.now();
|
|
15784
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: result.run.state, steps: executed, total: record2.steps.length, duration: Math.max(0, endedat - run.startedat), cause: runcauseof(options), startedat: run.startedat, endedat, ...dry === true ? { dryrun: true } : {} });
|
|
14334
15785
|
await audit("workflow", `The run ${run.id} of the workflow ${record2.name} ended ${result.run.state} after ${executed} of ${record2.steps.length} steps${result.run.failreason !== void 0 ? ` with the failure ${result.run.failreason}` : ""}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
14335
15786
|
await refreshbadge();
|
|
14336
15787
|
return { ok: result.run.state === "done", summary: `The workflow run ended ${result.run.state} after ${executed} of ${record2.steps.length} steps.`, details: { runid: run.id, state: result.run.state, executed, total: record2.steps.length, ...result.run.failreason !== void 0 ? { failreason: result.run.failreason } : {} } };
|
|
@@ -14355,6 +15806,187 @@ async function storetimeoutabort(run, step, message, budget) {
|
|
|
14355
15806
|
await audit("workflow", `The run ${run.id} exceeded its reviewed budget of ${budget} milliseconds and was cancelled with the cancelled error class.`, { planid: step.id });
|
|
14356
15807
|
return { run: aborted, log: [entry] };
|
|
14357
15808
|
}
|
|
15809
|
+
async function executetriggerstep(step, session, plan, tabid2, origin) {
|
|
15810
|
+
const options = stepoptions2(step);
|
|
15811
|
+
const family = triggerfamilyof(step.kind);
|
|
15812
|
+
if (!family) throw new Error(`The ${step.kind} step is not a reviewed trigger kind.`);
|
|
15813
|
+
const workflowid = typeof options.workflowid === "string" ? options.workflowid : "";
|
|
15814
|
+
const record2 = await memory.getworkflowrecord(workflowid);
|
|
15815
|
+
if (!record2) throw new Error(`No composed workflow matches ${workflowid || "the reviewed id"}; trigger rules bind to workflows already composed and reviewed.`);
|
|
15816
|
+
const armed = armrule({ family, workflowid, ...typeof options.label === "string" && options.label.trim() ? { label: options.label } : {}, payload: options.rule, ...typeof options.cooldown === "number" ? { cooldown: options.cooldown } : {}, now: Date.now() });
|
|
15817
|
+
if (!armed) throw new Error(`The ${step.kind} payload does not arm as a reviewed rule.`);
|
|
15818
|
+
if (!ruleoriginsgranted(armed, record2.origins)) throw new Error("The trigger touches origins outside the workflow grant list; review the rule or the workflow origins.");
|
|
15819
|
+
for (const matchorigin of armed.origins ?? []) {
|
|
15820
|
+
if (!origingranted(session, matchorigin)) throw new Error(`The trigger origin ${matchorigin} falls outside the session grants.`);
|
|
15821
|
+
}
|
|
15822
|
+
if (armed.pattern !== void 0) {
|
|
15823
|
+
let patternorigin2 = "";
|
|
15824
|
+
try {
|
|
15825
|
+
patternorigin2 = new URL(armed.pattern).origin;
|
|
15826
|
+
} catch {
|
|
15827
|
+
patternorigin2 = "";
|
|
15828
|
+
}
|
|
15829
|
+
if (patternorigin2 && !origingranted(session, patternorigin2)) throw new Error(`The trigger origin ${patternorigin2} falls outside the session grants.`);
|
|
15830
|
+
}
|
|
15831
|
+
for (const url of armed.urls ?? []) {
|
|
15832
|
+
let listorigin = "";
|
|
15833
|
+
try {
|
|
15834
|
+
listorigin = new URL(url).origin;
|
|
15835
|
+
} catch {
|
|
15836
|
+
listorigin = "";
|
|
15837
|
+
}
|
|
15838
|
+
if (listorigin && !origingranted(session, listorigin)) throw new Error(`The trigger origin ${listorigin} falls outside the session grants.`);
|
|
15839
|
+
}
|
|
15840
|
+
const nextfireat = family === "cron" && armed.cron !== void 0 ? schedulecron({ cron: armed.cron, ...armed.timezone !== void 0 ? { timezone: armed.timezone } : {} }, Date.now()) : family === "interval" && armed.period !== void 0 ? scheduleinterval({ period: armed.period, ...armed.jitter !== void 0 ? { jitter: armed.jitter } : {} }, void 0, Date.now(), hashseed(armed.id)) : void 0;
|
|
15841
|
+
const rule = { ...armed, ...nextfireat !== void 0 ? { state: { ...armed.state, nextfireat } } : {} };
|
|
15842
|
+
await memory.addtriggerule(rule);
|
|
15843
|
+
await registermenurule(rule);
|
|
15844
|
+
await scheduletriggerwakes();
|
|
15845
|
+
await memory.setprogress(recordtrigger(await memory.getprogress(), plan.id, step.id, { family: "arm", detail: `Armed the ${family} rule for the workflow ${record2.name}`, ruleid: rule.id, ...nextfireat !== void 0 ? { nextfireat } : {} }, Date.now()));
|
|
15846
|
+
await audit("trigger", `Armed the ${family} rule ${rule.id} for the workflow ${record2.name} version ${record2.version} behind the explicit arm review; the rule starts ${rule.state.enabled ? "enabled" : "disabled"} with a cooldown of ${rule.state.cooldown} millisecond${rule.state.cooldown === 1 ? "" : "s"}${nextfireat !== void 0 ? ` and the next fire scheduled` : ""}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
|
|
15847
|
+
void origin;
|
|
15848
|
+
void tabid2;
|
|
15849
|
+
return { ok: true, summary: `Armed the ${family} rule for the workflow ${record2.name}${nextfireat !== void 0 ? " with the next fire scheduled" : ""}.`, details: { trigger: { ruleid: rule.id, kind: family, enabled: rule.state.enabled, ...nextfireat !== void 0 ? { nextfireat } : {} } } };
|
|
15850
|
+
}
|
|
15851
|
+
async function runofworkflowactive(workflowid) {
|
|
15852
|
+
if ([...activeworkflowruns.keys()].length === 0) return false;
|
|
15853
|
+
for (const run of await memory.listworkflowruns()) {
|
|
15854
|
+
if (run.workflowid === workflowid && run.state === "running") return true;
|
|
15855
|
+
}
|
|
15856
|
+
return false;
|
|
15857
|
+
}
|
|
15858
|
+
async function launchtriggerrun(fire, rule) {
|
|
15859
|
+
const session = await memory.getsession();
|
|
15860
|
+
const plan = await memory.getplan();
|
|
15861
|
+
const now = Date.now();
|
|
15862
|
+
if (!session || session.stoppedat || session.pausedat || session.expiresat <= now) return { launched: false, reason: "The trigger launch needs a live, unpaused browser session." };
|
|
15863
|
+
if (!plan || plan.state !== "approved") return { launched: false, reason: "The trigger launch needs the approved plan review." };
|
|
15864
|
+
const record2 = await memory.getworkflowrecord(rule.workflowid);
|
|
15865
|
+
if (!record2) return { launched: false, reason: "The workflow of the trigger rule is no longer composed; evaluation skips the rule." };
|
|
15866
|
+
for (const workfloworigin of record2.origins) {
|
|
15867
|
+
if (!origingranted(session, workfloworigin)) return { launched: false, reason: `The workflow origin ${workfloworigin} falls outside the session grants.` };
|
|
15868
|
+
}
|
|
15869
|
+
const variables = { triggercause: fire.cause, triggerrule: rule.id };
|
|
15870
|
+
if (fire.url !== void 0) variables.triggerurl = fire.url;
|
|
15871
|
+
if (fire.title !== void 0) variables.triggertitle = fire.title;
|
|
15872
|
+
if (fire.payload !== void 0) variables.triggerpayload = JSON.stringify(fire.payload);
|
|
15873
|
+
const runstep2 = { id: `trigger${fire.id}`, kind: "runworkflow", summary: `The ${rule.kind} trigger run of ${record2.name}`, risk: "sensitive", options: JSON.stringify({ workflowid: rule.workflowid, reviewed: true, variables }) };
|
|
15874
|
+
const output = await executeworkflowstep(runstep2, session, plan, session.tabid, session.origin).catch((error) => ({ ok: false, summary: error instanceof Error ? error.message : String(error), details: {} }));
|
|
15875
|
+
const launched = Boolean(output.ok);
|
|
15876
|
+
const runid = output.details?.runid;
|
|
15877
|
+
await memory.settriggerule(updaterule(rule, { state: { lastfireat: fire.at, ...nextfireof(rule, fire.at) !== void 0 ? { nextfireat: nextfireof(rule, fire.at) } : {} }, stats: { launches: rule.stats.launches + (launched ? 1 : 0) } }));
|
|
15878
|
+
await memory.addtriggerfire(fire);
|
|
15879
|
+
await audit("trigger", `The ${rule.kind} rule ${rule.id} fired on ${fire.cause}${fire.url !== void 0 ? ` at ${fire.url}` : ""} and ${launched ? `launched the run ${runid ?? ""} of the workflow ${record2.name}` : `refused the launch: ${output.ok === false ? output.summary : "the run ended"}`}; the arm review, the live session, the approved plan and the origin gates all re-passed.`, { sessionid: session.id, planid: plan.id });
|
|
15880
|
+
if (launched) await notifytriggerfired(fire, rule, runid);
|
|
15881
|
+
await refreshbadge();
|
|
15882
|
+
return { launched, ...runid !== void 0 ? { runid } : {}, ...launched ? {} : { reason: output.ok === false ? output.summary : "the run ended" } };
|
|
15883
|
+
}
|
|
15884
|
+
function nextfireof(rule, after) {
|
|
15885
|
+
if (rule.kind === "cron" && rule.cron !== void 0) return schedulecron({ cron: rule.cron, ...rule.timezone !== void 0 ? { timezone: rule.timezone } : {} }, after);
|
|
15886
|
+
if (rule.kind === "interval" && rule.period !== void 0) return scheduleinterval({ period: rule.period, ...rule.jitter !== void 0 ? { jitter: rule.jitter } : {} }, after, rule.createdat, hashseed(rule.id));
|
|
15887
|
+
return void 0;
|
|
15888
|
+
}
|
|
15889
|
+
async function firetrigger(rule, cause, url, title, payload) {
|
|
15890
|
+
const now = Date.now();
|
|
15891
|
+
const record2 = await memory.getworkflowrecord(rule.workflowid);
|
|
15892
|
+
const runactive = await runofworkflowactive(rule.workflowid);
|
|
15893
|
+
const decision = evaluatetrigger({ rule, now, cause, ...url !== void 0 ? { url } : {}, ...title !== void 0 ? { title } : {}, ...payload !== void 0 ? { payload } : {}, runactive: false, workflowreviewed: record2 !== void 0 });
|
|
15894
|
+
if (!decision.fired || decision.fire === void 0) {
|
|
15895
|
+
await memory.settriggerule(updaterule(rule, { stats: { suppressions: rule.stats.suppressions + 1 } }));
|
|
15896
|
+
await audit("trigger", `The ${rule.kind} rule ${rule.id} suppressed a ${cause} fire inside the ${decision.suppressed ?? "review"} gate${decision.remaining !== void 0 ? ` with ${decision.remaining} millisecond${decision.remaining === 1 ? "" : "s"} of cooldown left` : ""}.`, {});
|
|
15897
|
+
return { fired: false, ...decision.suppressed !== void 0 ? { suppressed: decision.suppressed } : {} };
|
|
15898
|
+
}
|
|
15899
|
+
if (runactive) {
|
|
15900
|
+
const queued = queuefire(await memory.gettriggerqueue(), decision.fire);
|
|
15901
|
+
await memory.settriggerqueue(queued.queue);
|
|
15902
|
+
await memory.settriggerule(updaterule(rule, { stats: { suppressions: rule.stats.suppressions + 1 } }));
|
|
15903
|
+
await audit("trigger", `The ${rule.kind} rule ${rule.id} fired on ${cause} while its workflow run was busy${queued.deduped ? " and the queue kept the pending fire already waiting" : " and the fire queued for the run to settle"}.`, {});
|
|
15904
|
+
await refreshbadge();
|
|
15905
|
+
return { fired: true, queued: true };
|
|
15906
|
+
}
|
|
15907
|
+
await memory.settriggerule(updaterule(rule, { stats: { fires: rule.stats.fires + 1 } }));
|
|
15908
|
+
const launch = await launchtriggerrun(decision.fire, rule);
|
|
15909
|
+
return { fired: true, ...launch.launched ? {} : { suppressed: launch.reason } };
|
|
15910
|
+
}
|
|
15911
|
+
async function draintriggerqueue() {
|
|
15912
|
+
const queue = await memory.gettriggerqueue();
|
|
15913
|
+
if (queue.length === 0) return { launched: 0, remaining: 0 };
|
|
15914
|
+
const result = await drainqueue(queue, async (fire) => {
|
|
15915
|
+
const rule = await memory.gettriggerule(fire.ruleid);
|
|
15916
|
+
if (!rule) return;
|
|
15917
|
+
await memory.settriggerule(updaterule(rule, { stats: { fires: rule.stats.fires + 1 } }));
|
|
15918
|
+
await launchtriggerrun(fire, rule);
|
|
15919
|
+
});
|
|
15920
|
+
await memory.settriggerqueue(result.remaining);
|
|
15921
|
+
await audit("trigger", `The trigger queue drained ${result.launched} queued fire${result.launched === 1 ? "" : "s"} with ${result.remaining} still waiting.`, {});
|
|
15922
|
+
await refreshbadge();
|
|
15923
|
+
return { launched: result.launched, remaining: result.remaining.length };
|
|
15924
|
+
}
|
|
15925
|
+
async function evaluatenavigationtriggers(url, title) {
|
|
15926
|
+
if (!url.startsWith("https://")) return;
|
|
15927
|
+
const session = await memory.getsession();
|
|
15928
|
+
if (!session || session.stoppedat) return;
|
|
15929
|
+
for (const { rule } of await memory.listtriggers()) {
|
|
15930
|
+
if (!rule.state.enabled || rule.state.pausedat !== void 0) continue;
|
|
15931
|
+
if (rule.kind === "visit" && rule.origins !== void 0 && visitmatch(rule.origins, url)) await firetrigger(rule, "visit", url, title);
|
|
15932
|
+
if (rule.kind === "url" && rule.pattern !== void 0 && matchurl(rule.pattern, url)) await firetrigger(rule, "url", url, title);
|
|
15933
|
+
}
|
|
15934
|
+
await evaluatepagetriggers("navigate", url, title);
|
|
15935
|
+
}
|
|
15936
|
+
async function evaluatepagetriggers(event, url, title) {
|
|
15937
|
+
const session = await memory.getsession();
|
|
15938
|
+
if (!session || session.stoppedat || session.pausedat) return;
|
|
15939
|
+
for (const rule of observeevents(await memory.gettriggerules(), event)) {
|
|
15940
|
+
await firetrigger(rule, "event", url, title, { event });
|
|
15941
|
+
}
|
|
15942
|
+
}
|
|
15943
|
+
async function evaluatelistedtriggers() {
|
|
15944
|
+
const rules = await memory.gettriggerules();
|
|
15945
|
+
const due = listdue(rules, Date.now());
|
|
15946
|
+
for (const entry of due) {
|
|
15947
|
+
await firetrigger(entry.rule, "schedule");
|
|
15948
|
+
}
|
|
15949
|
+
await scheduletriggerwakes();
|
|
15950
|
+
return { due: due.length };
|
|
15951
|
+
}
|
|
15952
|
+
async function registermenurule(rule) {
|
|
15953
|
+
if (rule.kind !== "menu" || rule.title === void 0) return;
|
|
15954
|
+
const menus = chrome.contextMenus;
|
|
15955
|
+
if (typeof menus?.create !== "function") {
|
|
15956
|
+
await audit("trigger", `The menu rule ${rule.id} stays armed without its context menu entry because the browser exposes no context menus api under the current permission set; the manual run path stays available in the panel.`, {});
|
|
15957
|
+
return;
|
|
15958
|
+
}
|
|
15959
|
+
try {
|
|
15960
|
+
menus.create({ id: `devthinktrigger${rule.id}`, title: rule.title, contexts: ["page", "selection", "link"] });
|
|
15961
|
+
} catch {
|
|
15962
|
+
}
|
|
15963
|
+
}
|
|
15964
|
+
async function registermenurules() {
|
|
15965
|
+
for (const { rule } of await memory.listtriggers()) await registermenurule(rule);
|
|
15966
|
+
}
|
|
15967
|
+
async function scheduletriggerwakes() {
|
|
15968
|
+
const alarms = chrome.alarms;
|
|
15969
|
+
if (typeof alarms?.create !== "function") return;
|
|
15970
|
+
try {
|
|
15971
|
+
const rules = await memory.gettriggerules();
|
|
15972
|
+
const scheduled = rules.filter((rule) => rule.state.enabled && rule.state.pausedat === void 0 && rule.state.nextfireat !== void 0);
|
|
15973
|
+
if (scheduled.length === 0) return;
|
|
15974
|
+
const next = Math.min(...scheduled.map((rule) => rule.state.nextfireat));
|
|
15975
|
+
const oneminute = 6e4;
|
|
15976
|
+
alarms.create("devthinktriggerwake", { when: Math.max(Date.now() + oneminute, next) });
|
|
15977
|
+
} catch {
|
|
15978
|
+
}
|
|
15979
|
+
}
|
|
15980
|
+
async function notifytriggerfired(fire, rule, runid) {
|
|
15981
|
+
const notifications = chrome.notifications;
|
|
15982
|
+
if (typeof notifications?.create === "function") {
|
|
15983
|
+
try {
|
|
15984
|
+
notifications.create(`devthinktrigger${fire.id}`, { type: "basic", iconUrl: "icon128.png", title: "Devthink trigger fired", message: `The ${rule.kind} rule fired on ${fire.cause}${runid !== void 0 ? ` and launched run ${runid}` : ""}.` });
|
|
15985
|
+
} catch {
|
|
15986
|
+
}
|
|
15987
|
+
}
|
|
15988
|
+
void runid;
|
|
15989
|
+
}
|
|
14358
15990
|
async function auditcontroldecision(runid, decision, sessionid, planid) {
|
|
14359
15991
|
if (decision.kind === "branch" && decision.branch !== void 0) await audit("workflow", `The branch step ${decision.stepid} of the run ${runid} chose the path ${decision.branch.path}: ${decision.branch.reason}`, { sessionid, planid });
|
|
14360
15992
|
else if (decision.kind === "loop" && decision.loops !== void 0) await audit("workflow", `The loop step ${decision.stepid} of the run ${runid} recorded ${decision.loops.length} iteration counter${decision.loops.length === 1 ? "" : "s"} with the paths ${decision.loops.map((counter) => counter.path).join(", ")}.`, { sessionid, planid });
|
|
@@ -14448,6 +16080,9 @@ async function executeaction(step, session, plan, tabid2, origin, settings, verd
|
|
|
14448
16080
|
} else if (isworkflowkind(step.kind)) {
|
|
14449
16081
|
if (!session || !plan || plan.state !== "approved") throw new Error("Workflow kinds refuse to run outside an approved session plan.");
|
|
14450
16082
|
output = await executeworkflowstep(step, session, plan, tabid2, origin);
|
|
16083
|
+
} else if (istriggeraction(step.kind)) {
|
|
16084
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Trigger kinds refuse to run outside an approved session plan.");
|
|
16085
|
+
output = await executetriggerstep(step, session, plan, tabid2, origin);
|
|
14451
16086
|
} else {
|
|
14452
16087
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
14453
16088
|
const fresh = await snapshot(tabid2);
|
|
@@ -14548,7 +16183,9 @@ async function pausesession() {
|
|
|
14548
16183
|
if (session.pausedat) throw new Error("The browser session is already paused.");
|
|
14549
16184
|
const paused = { ...session, pausedat: Date.now() };
|
|
14550
16185
|
await memory.setsession(paused);
|
|
16186
|
+
await memory.settriggerules(pauseall(await memory.gettriggerules(), Date.now()));
|
|
14551
16187
|
await audit("pause", "The user paused the browser session; no action or preview can run.", { sessionid: session.id });
|
|
16188
|
+
await audit("trigger", "The session pause suspended every armed trigger rule; fires that arrive while paused queue for the resume drain.", { sessionid: session.id });
|
|
14552
16189
|
return paused;
|
|
14553
16190
|
}
|
|
14554
16191
|
async function resumesession() {
|
|
@@ -14558,7 +16195,11 @@ async function resumesession() {
|
|
|
14558
16195
|
if (!session.pausedat) throw new Error("The browser session is not paused.");
|
|
14559
16196
|
const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat, ...session.grants ? { grants: session.grants } : {} };
|
|
14560
16197
|
await memory.setsession(resumed);
|
|
16198
|
+
await memory.settriggerules(resumeall(await memory.gettriggerules()));
|
|
14561
16199
|
await audit("resume", "The user resumed the browser session; reviewed actions can run again.", { sessionid: session.id });
|
|
16200
|
+
await audit("trigger", "The session resume released the suspended trigger rules and drained the queued fires through the same gates.", { sessionid: session.id });
|
|
16201
|
+
void draintriggerqueue().catch(() => {
|
|
16202
|
+
});
|
|
14562
16203
|
return resumed;
|
|
14563
16204
|
}
|
|
14564
16205
|
async function grantcapability(permission) {
|
|
@@ -14696,7 +16337,7 @@ async function handlerequest(message, sender) {
|
|
|
14696
16337
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
14697
16338
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
14698
16339
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
14699
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), sessionmemory: sessionreport({ records: sessionrecords, events: await memory.getsessionevents(), folders: await memory.getsessionfolders(), diffs: await memory.getsessiondiffs(), ...autosnapshot !== void 0 ? { auto: autosnapshot.interval } : {}, ...crashed ? { crashed: true } : {} }), autosnapshotstate: autosnapshot, sessionretention: runsettings?.sessionretention, workflow: workflowreport({ workflows: await memory.listworkflows(), runs: await memory.listworkflowruns(), templates: await memory.getsteptemplates(), ...newestworkflowrun !== void 0 ? { log: await memory.getrunlog(newestworkflowrun.id), scopes: await memory.getrunscopes(newestworkflowrun.id), provenance: await memory.getworkflowprovenance(newestworkflowrun.id), control: await memory.listcontroldecisions(newestworkflowrun.id) } : {} }), runlogretention: runsettings?.runlogretention, ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
|
|
16340
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), sessionmemory: sessionreport({ records: sessionrecords, events: await memory.getsessionevents(), folders: await memory.getsessionfolders(), diffs: await memory.getsessiondiffs(), ...autosnapshot !== void 0 ? { auto: autosnapshot.interval } : {}, ...crashed ? { crashed: true } : {} }), autosnapshotstate: autosnapshot, sessionretention: runsettings?.sessionretention, trigger: triggerlist({ rules: await memory.gettriggerules(), workflows: await memory.listworkflows(), queue: await memory.gettriggerqueue() }), triggerretention: runsettings?.triggerretention, workflow: workflowreport({ workflows: await memory.listworkflows(), runs: await memory.listworkflowruns(), templates: await memory.getsteptemplates(), ...newestworkflowrun !== void 0 ? { log: await memory.getrunlog(newestworkflowrun.id), scopes: await memory.getrunscopes(newestworkflowrun.id), provenance: await memory.getworkflowprovenance(newestworkflowrun.id), control: await memory.listcontroldecisions(newestworkflowrun.id) } : {} }), runlogretention: runsettings?.runlogretention, runhistoryretention: runsettings?.runhistoryretention, editor: editorstate({ versions: await memory.listworkflowversions(), diffs: await memory.listversiondiffs(), history: await memory.gethistory(), overrides: await memory.listsiteoverrides(), imports: (await memory.listworkflowimports()).map((entry) => ({ id: entry.id, workflowid: entry.record.id, name: entry.record.name, version: entry.record.version, steps: entry.record.steps.length, risk: entry.record.risk, importedat: entry.importedat, ...entry.filename !== void 0 ? { filename: entry.filename } : {} })), backgroundruns: await memory.getbackgroundruns(), watchdog: { ...runsettings?.watchdog !== void 0 ? { config: runsettings.watchdog } : {}, events: await memory.listwatchdogevents() } }), ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
|
|
14700
16341
|
}
|
|
14701
16342
|
case "capabilities":
|
|
14702
16343
|
return refreshcapabilities();
|
|
@@ -14743,7 +16384,8 @@ async function handlerequest(message, sender) {
|
|
|
14743
16384
|
const network = outcome.details?.network;
|
|
14744
16385
|
const timeline = outcome.details?.timeline;
|
|
14745
16386
|
const sessionblock = outcome.details?.session;
|
|
14746
|
-
|
|
16387
|
+
const triggerblock = outcome.details?.trigger;
|
|
16388
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...triggerblock !== void 0 && triggerblock.ruleid !== void 0 ? { trigger: { ruleid: triggerblock.ruleid, kind: triggerblock.kind ?? "", enabled: triggerblock.enabled ?? true, ...triggerblock.nextfireat !== void 0 ? { nextfireat: triggerblock.nextfireat } : {} } } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {}, ...timeline ? { timeline } : {}, ...sessionblock !== void 0 ? { session: { recordid: sessionblock.recordid ?? "", sections: sessionblock.sections ?? 0, ...sessionblock.matches !== void 0 ? { matches: sessionblock.matches } : {}, ...sessionblock.restored !== void 0 ? { restored: sessionblock.restored } : {}, ...sessionblock.skipped !== void 0 ? { skipped: sessionblock.skipped } : {}, ...sessionblock.cursor !== void 0 ? { cursor: sessionblock.cursor } : {}, ...sessionblock.bytes !== void 0 ? { bytes: sessionblock.bytes } : {} } } : {} }));
|
|
14747
16389
|
}
|
|
14748
16390
|
case "map": {
|
|
14749
16391
|
const plan = await memory.getplan();
|
|
@@ -15917,8 +17559,13 @@ async function handlerequest(message, sender) {
|
|
|
15917
17559
|
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Workflow runs need an active browser session behind the consent gates.");
|
|
15918
17560
|
const record2 = await memory.getworkflowrecord(inputapprove.workflowid?.trim() ?? "");
|
|
15919
17561
|
if (!record2) throw new Error(`No composed workflow matches ${inputapprove.workflowid ?? ""}.`);
|
|
15920
|
-
|
|
15921
|
-
|
|
17562
|
+
if (record2.reviewstate === "pending") {
|
|
17563
|
+
await memory.addworkflowrecord({ ...record2, reviewstate: "approved" });
|
|
17564
|
+
await memory.removeworkflowversion(record2.id, record2.version).catch(() => {
|
|
17565
|
+
});
|
|
17566
|
+
}
|
|
17567
|
+
await audit("workflow", `The user approved the run review of the workflow ${record2.name} version ${record2.version} with its ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"} shown${record2.reviewstate === "pending" ? "; the pending import or rollback review cleared and the workflow may run again" : ""}; the run still passes every consent gate per step.`, { sessionid: session.id });
|
|
17568
|
+
return { approved: true, steps: record2.steps.length, risk: record2.risk, ...record2.reviewstate === "pending" ? { reviewstate: "approved" } : {} };
|
|
15922
17569
|
}
|
|
15923
17570
|
case "executeworkflowstep": {
|
|
15924
17571
|
const inputsingle = message;
|
|
@@ -15964,8 +17611,8 @@ async function handlerequest(message, sender) {
|
|
|
15964
17611
|
const guards = activeworkflowruns.get(stored.id);
|
|
15965
17612
|
if (guards) guards.cancelled = true;
|
|
15966
17613
|
const paused = pauserun(stored, Date.now());
|
|
15967
|
-
await memory.setworkflowrun(paused);
|
|
15968
|
-
await audit("workflow", `Paused the workflow run ${paused.id} at the checkpoint of step cursor ${paused.cursor}; the resume continues exactly there.`, {});
|
|
17614
|
+
await memory.setworkflowrun({ ...paused, pausekind: "user" });
|
|
17615
|
+
await audit("workflow", `Paused the workflow run ${paused.id} at the checkpoint of step cursor ${paused.cursor}; the resume continues exactly there and the worker wake never auto-resumes a user pause.`, {});
|
|
15969
17616
|
await refreshbadge();
|
|
15970
17617
|
return { runid: paused.id, state: paused.state, cursor: paused.cursor };
|
|
15971
17618
|
}
|
|
@@ -15990,6 +17637,27 @@ async function handlerequest(message, sender) {
|
|
|
15990
17637
|
}
|
|
15991
17638
|
return await dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin });
|
|
15992
17639
|
};
|
|
17640
|
+
const debugbreakpoints = Array.isArray(message.breakpoints) ? message.breakpoints.filter((id) => typeof id === "string") : void 0;
|
|
17641
|
+
if (debugbreakpoints !== void 0) {
|
|
17642
|
+
const startcursor = stored.run.cursor;
|
|
17643
|
+
let segment = runtobreakpoint({ record: record2, cursor: startcursor, breakpoints: debugbreakpoints });
|
|
17644
|
+
if (segment.until === startcursor && startcursor < record2.steps.length) segment = runtobreakpoint({ record: record2, cursor: startcursor + 1, breakpoints: debugbreakpoints });
|
|
17645
|
+
if (segment.until === startcursor) {
|
|
17646
|
+
await memory.setworkflowrun({ ...stored.run, state: "paused", pausedat: Date.now(), cursor: startcursor });
|
|
17647
|
+
return { runid: stored.run.id, state: "paused", cursor: startcursor, ...segment.pausat !== void 0 ? { pausat: segment.pausat } : {}, remaining: segment.remaining };
|
|
17648
|
+
}
|
|
17649
|
+
const segmentresult = await runworkflowsegment({ record: record2, run: stored.run, startcursor, until: segment.until, session, plan, tabid: tab.id, origin, scopes, log, storedentries, execute: executeresume });
|
|
17650
|
+
if (segmentresult.failed !== void 0) {
|
|
17651
|
+
await memory.setworkflowrun({ ...stored.run, state: "failed", endedat: Date.now(), cursor: segmentresult.run.cursor, failreason: segmentresult.failed });
|
|
17652
|
+
await audit("workflow", `The debug resume of the run ${stored.run.id} failed at step cursor ${segmentresult.run.cursor}: ${segmentresult.failed}.`, { sessionid: session.id, planid: plan.id });
|
|
17653
|
+
await refreshbadge();
|
|
17654
|
+
return { runid: stored.run.id, state: "failed", cursor: segmentresult.run.cursor, failreason: segmentresult.failed };
|
|
17655
|
+
}
|
|
17656
|
+
await memory.setworkflowrun({ ...stored.run, state: "paused", pausedat: Date.now(), cursor: segment.until });
|
|
17657
|
+
await audit("workflow", `The debug resume of the run ${stored.run.id} ran ${segment.until - startcursor} step${segment.until - startcursor === 1 ? "" : "s"} and paused at the breakpoint ${segment.pausat ?? "end"} of step cursor ${segment.until}; ${segment.remaining} step${segment.remaining === 1 ? "" : "s"} remain.`, { sessionid: session.id, planid: plan.id });
|
|
17658
|
+
await refreshbadge();
|
|
17659
|
+
return { runid: stored.run.id, state: "paused", cursor: segment.until, ...segment.pausat !== void 0 ? { pausat: segment.pausat } : {}, remaining: segment.remaining };
|
|
17660
|
+
}
|
|
15993
17661
|
const resumed = await runworkflow({ record: record2, run: stored.run, scopes, log, execute: executeresume, now: Date.now(), gates: { sessionactive: Boolean(session && !session.stoppedat && !session.pausedat && session.expiresat > Date.now()), planapproved: plan.state === "approved", origingranted: (workfloworigin) => origingranted(session, workfloworigin) }, oncheckpoint: async (state) => {
|
|
15994
17662
|
await memory.setworkflowrun(state.run);
|
|
15995
17663
|
for (const entry of state.log.slice(storedentries)) await memory.addrunlogentry(state.run.id, entry);
|
|
@@ -16029,10 +17697,547 @@ async function handlerequest(message, sender) {
|
|
|
16029
17697
|
if (!stored) throw new Error(`No workflow run matches ${inputoutcome.runid ?? ""}.`);
|
|
16030
17698
|
return workflowoutcome({ run: stored.run, entries: stored.log, ...inputoutcome.stepid !== void 0 && inputoutcome.stepid !== "" ? { stepid: inputoutcome.stepid } : {} });
|
|
16031
17699
|
}
|
|
17700
|
+
case "triggerreview": {
|
|
17701
|
+
const triggers = await memory.listtriggers();
|
|
17702
|
+
const runs = await memory.listworkflowruns();
|
|
17703
|
+
const queue = await memory.gettriggerqueue();
|
|
17704
|
+
const rules = await Promise.all(triggers.map(async (entry) => {
|
|
17705
|
+
const record2 = await memory.getworkflowrecord(entry.rule.workflowid);
|
|
17706
|
+
return {
|
|
17707
|
+
id: entry.rule.id,
|
|
17708
|
+
kind: entry.rule.kind,
|
|
17709
|
+
workflowid: entry.rule.workflowid,
|
|
17710
|
+
...entry.workflowname !== void 0 ? { workflowname: entry.workflowname } : {},
|
|
17711
|
+
label: entry.rule.label,
|
|
17712
|
+
enabled: entry.rule.state.enabled,
|
|
17713
|
+
paused: entry.rule.state.pausedat !== void 0,
|
|
17714
|
+
cooldown: entry.rule.state.cooldown,
|
|
17715
|
+
...entry.rule.state.lastfireat !== void 0 ? { lastfireat: entry.rule.state.lastfireat } : {},
|
|
17716
|
+
...entry.rule.state.nextfireat !== void 0 ? { nextfireat: entry.rule.state.nextfireat } : {},
|
|
17717
|
+
fires: entry.rule.stats.fires,
|
|
17718
|
+
launches: entry.rule.stats.launches,
|
|
17719
|
+
suppressions: entry.rule.stats.suppressions,
|
|
17720
|
+
summary: triggersummary(entry.rule),
|
|
17721
|
+
steps: record2 ? record2.steps.map((inner) => ({ id: inner.id, kind: inner.kind, label: inner.label })) : [],
|
|
17722
|
+
...entry.rule.kind === "webhook" ? { deliveries: (await memory.listwebhookpayloads(entry.rule.id)).length } : {}
|
|
17723
|
+
};
|
|
17724
|
+
}));
|
|
17725
|
+
const active = runs.filter((run) => run.state === "running").length;
|
|
17726
|
+
return { rules, queued: queue.length, active, fires: (await memory.listtriggerfires()).slice(0, 50) };
|
|
17727
|
+
}
|
|
17728
|
+
case "toggletrigger": {
|
|
17729
|
+
const inputtoggle = message;
|
|
17730
|
+
const rule = await memory.gettriggerule(inputtoggle.ruleid ?? "");
|
|
17731
|
+
if (!rule) throw new Error(`No armed trigger rule matches ${inputtoggle.ruleid ?? ""}.`);
|
|
17732
|
+
if (typeof inputtoggle.enabled !== "boolean") throw new Error("The trigger toggle needs the reviewed enabled flag.");
|
|
17733
|
+
const updated = updaterule(rule, { state: { enabled: inputtoggle.enabled } });
|
|
17734
|
+
await memory.settriggerule(updated);
|
|
17735
|
+
await audit("trigger", `The user ${inputtoggle.enabled ? "enabled" : "disabled"} the ${rule.kind} rule ${rule.id} of the workflow ${rule.workflowid}; disabled rules never fire until reenabled.`, {});
|
|
17736
|
+
await scheduletriggerwakes();
|
|
17737
|
+
return { ruleid: rule.id, enabled: updated.state.enabled };
|
|
17738
|
+
}
|
|
17739
|
+
case "createtrigger": {
|
|
17740
|
+
const inputcreate = message;
|
|
17741
|
+
const session = await memory.getsession();
|
|
17742
|
+
const plan = await memory.getplan();
|
|
17743
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Arming a trigger rule needs an active browser session behind the consent gates.");
|
|
17744
|
+
if (!plan || plan.state !== "approved") throw new Error("Arming a trigger rule needs the approved plan review.");
|
|
17745
|
+
const family = triggerfamilyof(`${inputcreate.family ?? ""}rule`);
|
|
17746
|
+
if (!family) throw new Error("The trigger family is not a reviewed family.");
|
|
17747
|
+
const step = { id: `panel${Date.now()}`, kind: `${family}rule`, summary: `The reviewed ${family} rule of the panel`, risk: "sensitive", options: JSON.stringify({ workflowid: inputcreate.workflowid ?? "", reviewed: true, ...typeof inputcreate.label === "string" ? { label: inputcreate.label } : {}, ...inputcreate.cooldown !== void 0 ? { cooldown: inputcreate.cooldown } : {}, rule: inputcreate.payload ?? {} }) };
|
|
17748
|
+
const verdict = canexecute({ session, plan, step, tabid: session.tabid, origin: session.origin, now: Date.now() });
|
|
17749
|
+
if (!verdict.allowed) throw new Error(verdict.reason ?? "The trigger rule was refused by the consent gates.");
|
|
17750
|
+
return await executetriggerstep(step, session, plan, session.tabid, session.origin);
|
|
17751
|
+
}
|
|
17752
|
+
case "createvisitrule": {
|
|
17753
|
+
const inputvisit = message;
|
|
17754
|
+
const session = await memory.getsession();
|
|
17755
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Creating a visit rule needs an active browser session behind the consent gates.");
|
|
17756
|
+
const tab = await chrome.tabs.get(session.tabid).catch(() => void 0);
|
|
17757
|
+
const url = tab?.url ?? "";
|
|
17758
|
+
let origin = "";
|
|
17759
|
+
try {
|
|
17760
|
+
origin = new URL(url).origin;
|
|
17761
|
+
} catch {
|
|
17762
|
+
origin = "";
|
|
17763
|
+
}
|
|
17764
|
+
if (!origin.startsWith("https://")) throw new Error("The current page carries no HTTPS origin to bind a visit rule to.");
|
|
17765
|
+
return await handlerequest({ kind: "createtrigger", workflowid: inputvisit.workflowid ?? "", family: "visit", payload: { origins: [origin] }, label: `Visit ${origin}` }, {});
|
|
17766
|
+
}
|
|
17767
|
+
case "duplicatetrigger": {
|
|
17768
|
+
const inputduplicate = message;
|
|
17769
|
+
const rule = await memory.gettriggerule(inputduplicate.ruleid ?? "");
|
|
17770
|
+
if (!rule) throw new Error(`No armed trigger rule matches ${inputduplicate.ruleid ?? ""}.`);
|
|
17771
|
+
const record2 = await memory.getworkflowrecord(inputduplicate.workflowid ?? "");
|
|
17772
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputduplicate.workflowid ?? ""}.`);
|
|
17773
|
+
const { state: rulestate, stats: rulestats, createdat: rulecreatedat, id: ruleid, workflowid: ruleworkflowid, label: rulelabel, cooldown: rulecooldown, ...familypayload } = rule;
|
|
17774
|
+
void rulestate;
|
|
17775
|
+
void rulestats;
|
|
17776
|
+
void rulecreatedat;
|
|
17777
|
+
void ruleid;
|
|
17778
|
+
void ruleworkflowid;
|
|
17779
|
+
void rulelabel;
|
|
17780
|
+
const armed = armrule({ family: rule.kind, workflowid: record2.id, label: `${rule.label} for ${record2.name}`, payload: familypayload, ...rulecooldown !== void 0 ? { cooldown: rulecooldown } : {}, now: Date.now() });
|
|
17781
|
+
if (!armed) throw new Error("The duplicated rule payload failed its family grammar.");
|
|
17782
|
+
const nextfireat = rule.kind === "cron" && rule.cron !== void 0 ? schedulecron({ cron: rule.cron, ...rule.timezone !== void 0 ? { timezone: rule.timezone } : {} }, Date.now()) : rule.kind === "interval" && rule.period !== void 0 ? scheduleinterval({ period: rule.period, ...rule.jitter !== void 0 ? { jitter: rule.jitter } : {} }, void 0, Date.now(), hashseed(armed.id)) : void 0;
|
|
17783
|
+
const duplicate = { ...armed, ...nextfireat !== void 0 ? { state: { ...armed.state, nextfireat } } : {} };
|
|
17784
|
+
if (!ruleoriginsgranted(duplicate, record2.origins)) throw new Error("The duplicated rule touches origins outside the workflow grant list of its new workflow.");
|
|
17785
|
+
await memory.addtriggerule(duplicate);
|
|
17786
|
+
await registermenurule(duplicate);
|
|
17787
|
+
await scheduletriggerwakes();
|
|
17788
|
+
await audit("trigger", `The user duplicated the ${rule.kind} rule ${rule.id} to the workflow ${record2.name} as the rule ${duplicate.id}; the duplicated rule arms enabled with zeroed counters.`, {});
|
|
17789
|
+
return { ruleid: duplicate.id, workflowid: duplicate.workflowid };
|
|
17790
|
+
}
|
|
17791
|
+
case "manualrun": {
|
|
17792
|
+
const inputmanual = message;
|
|
17793
|
+
const record2 = await memory.getworkflowrecord(inputmanual.workflowid ?? "");
|
|
17794
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputmanual.workflowid ?? ""}.`);
|
|
17795
|
+
const preview = manualpreview(record2, Date.now());
|
|
17796
|
+
await memory.addmanualrun(preview);
|
|
17797
|
+
await audit("trigger", `The user opened the manual run preview of the workflow ${record2.name} with its ${preview.preview.length} expanded step${preview.preview.length === 1 ? "" : "s"}; nothing runs before the confirmation.`, {});
|
|
17798
|
+
return { manualrun: preview, workflowname: record2.name };
|
|
17799
|
+
}
|
|
17800
|
+
case "confirmmanualrun": {
|
|
17801
|
+
const inputconfirm = message;
|
|
17802
|
+
const stored = (await memory.listmanualruns()).find((entry) => entry.id === (inputconfirm.previewid ?? ""));
|
|
17803
|
+
if (!stored) throw new Error(`No manual run preview matches ${inputconfirm.previewid ?? ""}.`);
|
|
17804
|
+
if (typeof inputconfirm.confirmed !== "boolean") throw new Error("The manual run confirmation needs the reviewed confirmed flag.");
|
|
17805
|
+
const session = await memory.getsession();
|
|
17806
|
+
const plan = await memory.getplan();
|
|
17807
|
+
if (!session || session.stoppedat || session.pausedat || session.expiresat <= Date.now()) throw new Error("Confirming a manual run needs a live, unpaused browser session behind the consent gates.");
|
|
17808
|
+
if (!plan || plan.state !== "approved") throw new Error("Confirming a manual run needs the approved plan review.");
|
|
17809
|
+
const record2 = await memory.getworkflowrecord(stored.workflowid);
|
|
17810
|
+
if (!record2) throw new Error("The workflow of the manual run preview is no longer composed in the library.");
|
|
17811
|
+
const decided = confirmmanualrun(stored, inputconfirm.confirmed, Date.now());
|
|
17812
|
+
await memory.addmanualrun(decided);
|
|
17813
|
+
if (!inputconfirm.confirmed) {
|
|
17814
|
+
await audit("trigger", `The user cancelled the manual run of the workflow ${record2.name} after its step preview; nothing ran.`, { sessionid: session.id, planid: plan.id });
|
|
17815
|
+
return { confirmed: false };
|
|
17816
|
+
}
|
|
17817
|
+
const runstep2 = { id: `manual${decided.id}`, kind: "runworkflow", summary: `The manual run of ${record2.name}`, risk: "sensitive", options: JSON.stringify({ workflowid: record2.id, reviewed: true }) };
|
|
17818
|
+
const output = await executeworkflowstep(runstep2, session, plan, session.tabid, session.origin).catch((error) => ({ ok: false, summary: error instanceof Error ? error.message : String(error), details: {} }));
|
|
17819
|
+
await audit("trigger", `The user approved the manual run of the workflow ${record2.name} after its ${decided.preview.length} step preview; the run ended ${output.ok ? "done" : "failed"}.`, { sessionid: session.id, planid: plan.id });
|
|
17820
|
+
return { confirmed: true, runid: output.details?.runid, state: output.ok ? "done" : "failed" };
|
|
17821
|
+
}
|
|
17822
|
+
case "firetrigger": {
|
|
17823
|
+
const inputfire = message;
|
|
17824
|
+
const rule = await memory.gettriggerule(inputfire.ruleid ?? "");
|
|
17825
|
+
if (!rule) throw new Error(`No armed trigger rule matches ${inputfire.ruleid ?? ""}.`);
|
|
17826
|
+
const result = await firetrigger(rule, "manual");
|
|
17827
|
+
return { fired: result.fired, ...result.queued !== void 0 ? { queued: result.queued } : {}, ...result.suppressed !== void 0 ? { suppressed: result.suppressed } : {} };
|
|
17828
|
+
}
|
|
17829
|
+
case "receivewebhook": {
|
|
17830
|
+
const inputwebhook = message;
|
|
17831
|
+
const rule = await memory.gettriggerule(inputwebhook.ruleid ?? "");
|
|
17832
|
+
if (!rule || rule.kind !== "webhook") throw new Error(`No webhook rule matches ${inputwebhook.ruleid ?? ""}.`);
|
|
17833
|
+
const verification = verifywebhook({ rule, secret: inputwebhook.secret ?? "", payload: inputwebhook.payload });
|
|
17834
|
+
if (!verification.verified) {
|
|
17835
|
+
await memory.settriggerule(updaterule(rule, { stats: { suppressions: rule.stats.suppressions + 1 } }));
|
|
17836
|
+
await audit("trigger", `A webhook delivery for the rule ${rule.id} was refused: ${verification.reason}; only secret verified payloads ever persist.`, {});
|
|
17837
|
+
return { verified: false, reason: verification.reason };
|
|
17838
|
+
}
|
|
17839
|
+
await memory.addwebhookpayload(rule.id, inputwebhook.payload, Date.now());
|
|
17840
|
+
const result = await firetrigger(rule, "webhook", void 0, void 0, inputwebhook.payload);
|
|
17841
|
+
return { verified: true, fired: result.fired, ...result.queued !== void 0 ? { queued: result.queued } : {}, ...result.suppressed !== void 0 ? { suppressed: result.suppressed } : {} };
|
|
17842
|
+
}
|
|
17843
|
+
case "rotatetriggersecret": {
|
|
17844
|
+
const inputrotate = message;
|
|
17845
|
+
const rule = await memory.gettriggerule(inputrotate.ruleid ?? "");
|
|
17846
|
+
if (!rule || rule.kind !== "webhook") throw new Error(`No webhook rule matches ${inputrotate.ruleid ?? ""}.`);
|
|
17847
|
+
const bytes = crypto.getRandomValues(new Uint8Array(24));
|
|
17848
|
+
const secret = [...bytes].map((byte) => (byte % 36).toString(36)).join("").padEnd(24, "7x9k2m").slice(0, 24);
|
|
17849
|
+
const rotated = { ...rule, secret, state: { ...rule.state }, stats: { ...rule.stats } };
|
|
17850
|
+
await memory.settriggerule(rotated);
|
|
17851
|
+
await audit("trigger", `The user rotated the shared secret of the webhook rule ${rule.id}; the previous secret stops verifying immediately and the new secret was shown once.`, {});
|
|
17852
|
+
return { ruleid: rule.id, secret };
|
|
17853
|
+
}
|
|
17854
|
+
case "settriggerretention": {
|
|
17855
|
+
const inputretention = message;
|
|
17856
|
+
const settings = await memory.getsettings();
|
|
17857
|
+
const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
|
|
17858
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { triggerretention: retention } : {} });
|
|
17859
|
+
await audit("configure", `The user set the trigger fire retention to ${retention === void 0 ? "keep every fire record" : `${retention} fire record${retention === 1 ? "" : "s"}`}; the rule counters always survive and no code ceiling applies.`);
|
|
17860
|
+
return { triggerretention: retention };
|
|
17861
|
+
}
|
|
17862
|
+
case "triggerhistory": {
|
|
17863
|
+
const inputhistory = message;
|
|
17864
|
+
const fires = await memory.listtriggerfires(inputhistory.ruleid);
|
|
17865
|
+
return { fires };
|
|
17866
|
+
}
|
|
17867
|
+
case "buttontrigger": {
|
|
17868
|
+
const rules = (await memory.gettriggerules()).filter((rule) => rule.kind === "button" && rule.state.enabled && rule.state.pausedat === void 0);
|
|
17869
|
+
if (rules.length === 0) return { fired: false, reason: "No enabled button rule is armed." };
|
|
17870
|
+
const results = [];
|
|
17871
|
+
for (const rule of rules) results.push({ ruleid: rule.id, ...await firetrigger(rule, "button") });
|
|
17872
|
+
return { fired: results.some((entry) => entry.fired), rules: results };
|
|
17873
|
+
}
|
|
17874
|
+
case "triggerfiredreport": {
|
|
17875
|
+
const fires = await memory.listtriggerfires();
|
|
17876
|
+
return triggerlist({ rules: await memory.gettriggerules(), workflows: await memory.listworkflows(), queue: await memory.gettriggerqueue() });
|
|
17877
|
+
}
|
|
17878
|
+
case "runtobreakpoint": {
|
|
17879
|
+
const inputdebug = message;
|
|
17880
|
+
const session = await memory.getsession();
|
|
17881
|
+
const plan = await memory.getplan();
|
|
17882
|
+
if (!session || session.stoppedat || session.pausedat || session.expiresat <= Date.now()) throw new Error("Debug runs need a live, unpaused browser session behind the consent gates.");
|
|
17883
|
+
if (!plan || plan.state !== "approved") throw new Error("Debug runs need the approved plan review.");
|
|
17884
|
+
const record2 = await memory.getworkflowrecord(inputdebug.workflowid?.trim() ?? "");
|
|
17885
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputdebug.workflowid ?? ""}.`);
|
|
17886
|
+
for (const workfloworigin of record2.origins) {
|
|
17887
|
+
if (!origingranted(session, workfloworigin)) throw new Error(`The workflow origin ${workfloworigin} falls outside the session grants.`);
|
|
17888
|
+
}
|
|
17889
|
+
const reviewgate = runreviewgranted(record2);
|
|
17890
|
+
if (!reviewgate.allowed) throw new Error(reviewgate.reason ?? "The workflow stays unreviewed until its import or rollback review approves the expanded step list.");
|
|
17891
|
+
const marked = [.../* @__PURE__ */ new Set([...await memory.getworkflowbreakpoints(record2.id), ...record2.steps.filter((step) => step.breakpoint === true).map((step) => step.id)])];
|
|
17892
|
+
const segment = runtobreakpoint({ record: record2, cursor: 0, breakpoints: marked });
|
|
17893
|
+
if (segment.pausat === void 0) throw new Error("No breakpoint marks any step of the workflow; mark one on the canvas before the debug run starts.");
|
|
17894
|
+
const run = newworkflowrun({ workflowid: record2.id, now: Date.now() });
|
|
17895
|
+
await memory.setworkflowrun(run);
|
|
17896
|
+
await memory.setrunscopes(run.id, []);
|
|
17897
|
+
if (segment.until === 0) {
|
|
17898
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), cursor: 0 });
|
|
17899
|
+
await audit("workflow", `The debug run ${run.id} of the workflow ${record2.name} paused at the first breakpoint ${segment.pausat} before any step ran; ${record2.steps.length} step${record2.steps.length === 1 ? "" : "s"} remain.`, { sessionid: session.id, planid: plan.id });
|
|
17900
|
+
await refreshbadge();
|
|
17901
|
+
return { runid: run.id, state: "paused", cursor: 0, pausat: segment.pausat, remaining: record2.steps.length };
|
|
17902
|
+
}
|
|
17903
|
+
const { tab, origin } = await activecontext();
|
|
17904
|
+
const executedebug = async (dispatched, stepcontext) => {
|
|
17905
|
+
if (iscontrolflowkind(dispatched.kind)) {
|
|
17906
|
+
const controlled = await runcontrolstep({ step: dispatched, scopes: stepcontext.scopes, outputs: stepcontext.outputs ?? {}, execute: executedebug, now: Date.now(), runid: run.id, pagestate: await pagestateof(tab.id), resolveelements: async (selector) => await resolveelements(tab.id, selector) });
|
|
17907
|
+
return { ...controlled.output, scopes: controlled.scopes, log: controlled.log };
|
|
17908
|
+
}
|
|
17909
|
+
return await dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin });
|
|
17910
|
+
};
|
|
17911
|
+
const segmentresult = await runworkflowsegment({ record: record2, run, startcursor: 0, until: segment.until, session, plan, tabid: tab.id, origin, scopes: [{ name: "root", variables: [] }], log: [], storedentries: 0, execute: executedebug });
|
|
17912
|
+
if (segmentresult.failed !== void 0) {
|
|
17913
|
+
await memory.setworkflowrun({ ...run, state: "failed", endedat: Date.now(), cursor: segmentresult.run.cursor, failreason: segmentresult.failed });
|
|
17914
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: "failed", steps: segmentresult.run.cursor, total: record2.steps.length, duration: Date.now() - run.startedat, cause: "debug", startedat: run.startedat, endedat: Date.now() });
|
|
17915
|
+
await audit("workflow", `The debug run ${run.id} of the workflow ${record2.name} failed at step cursor ${segmentresult.run.cursor} before the breakpoint ${segment.pausat}: ${segmentresult.failed}.`, { sessionid: session.id, planid: plan.id });
|
|
17916
|
+
await refreshbadge();
|
|
17917
|
+
return { runid: run.id, state: "failed", cursor: segmentresult.run.cursor, failreason: segmentresult.failed };
|
|
17918
|
+
}
|
|
17919
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), cursor: segment.until });
|
|
17920
|
+
await memory.addrunhistory({ runid: run.id, workflowid: record2.id, outcome: "paused", steps: segment.until, total: record2.steps.length, duration: Date.now() - run.startedat, cause: "debug", startedat: run.startedat, endedat: Date.now() });
|
|
17921
|
+
await audit("workflow", `The debug run ${run.id} of the workflow ${record2.name} ran ${segment.until} step${segment.until === 1 ? "" : "s"} and paused at the breakpoint ${segment.pausat}; ${segment.remaining} step${segment.remaining === 1 ? "" : "s"} remain and the resume continues exactly there.`, { sessionid: session.id, planid: plan.id });
|
|
17922
|
+
await refreshbadge();
|
|
17923
|
+
return { runid: run.id, state: "paused", cursor: segment.until, pausat: segment.pausat, remaining: segment.remaining };
|
|
17924
|
+
}
|
|
17925
|
+
case "editormodel": {
|
|
17926
|
+
const inputmodel = message;
|
|
17927
|
+
const record2 = await memory.getworkflowrecord(inputmodel.workflowid?.trim() ?? "");
|
|
17928
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputmodel.workflowid ?? ""}.`);
|
|
17929
|
+
const layout = await memory.geteditorlayout(record2.id);
|
|
17930
|
+
const model = loadworkflow(record2, layout);
|
|
17931
|
+
const breakpoints = await memory.getworkflowbreakpoints(record2.id);
|
|
17932
|
+
const nodes = model.nodes.map((node) => node.step !== void 0 && breakpoints.includes(node.step.id) && node.step.breakpoint !== true ? { step: { ...node.step, breakpoint: true }, x: node.x, y: node.y } : node);
|
|
17933
|
+
return { model: { ...model, nodes }, record: record2, versions: await memory.listworkflowversions(record2.id), breakpoints };
|
|
17934
|
+
}
|
|
17935
|
+
case "editorsave": {
|
|
17936
|
+
const inputsave = message;
|
|
17937
|
+
const session = await memory.getsession();
|
|
17938
|
+
const plan = await memory.getplan();
|
|
17939
|
+
const model = inputsave.model;
|
|
17940
|
+
if (!model) throw new Error("The editor save needs the canvas model.");
|
|
17941
|
+
const gate = editorsavegate({ session, plan, model, now: Date.now() });
|
|
17942
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The editor save failed its consent gate.");
|
|
17943
|
+
const record2 = saveworkflow(model, { now: Date.now(), kindallowed: (kind) => {
|
|
17944
|
+
try {
|
|
17945
|
+
actionrisk(kind);
|
|
17946
|
+
return true;
|
|
17947
|
+
} catch {
|
|
17948
|
+
return false;
|
|
17949
|
+
}
|
|
17950
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
17951
|
+
await memory.addworkflowrecord(record2);
|
|
17952
|
+
await memory.addworkflowversion({ workflowid: record2.id, version: record2.version, createdat: Date.now(), note: typeof inputsave.note === "string" && inputsave.note.trim() !== "" ? inputsave.note.trim() : `Edited on the canvas with ${record2.steps.length} expanded steps`, steps: record2.steps.length, risk: record2.risk });
|
|
17953
|
+
await memory.seteditorlayout(record2.id, model.layout);
|
|
17954
|
+
await memory.setworkflowbreakpoints(record2.id, model.nodes.flatMap((node) => node.step?.breakpoint === true ? [node.step.id] : []));
|
|
17955
|
+
await audit("workflow", `The user saved the workflow ${record2.name} as version ${record2.version} from the canvas editor: ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"} graded ${record2.risk}; the composition ran the full workflow grammar and every older version survives for the timeline.`, { ...session !== void 0 ? { sessionid: session.id } : {}, ...plan !== void 0 ? { planid: plan.id } : {} });
|
|
17956
|
+
return { workflowid: record2.id, version: record2.version, steps: record2.steps.length, risk: record2.risk };
|
|
17957
|
+
}
|
|
17958
|
+
case "seteditorlayout": {
|
|
17959
|
+
const inputlayout = message;
|
|
17960
|
+
const workflowid = inputlayout.workflowid?.trim() ?? "";
|
|
17961
|
+
const layout = inputlayout.layout;
|
|
17962
|
+
if (!workflowid || !layout || !Number.isFinite(layout.width) || !Number.isFinite(layout.height) || !Number.isFinite(layout.zoom) || layout.zoom <= 0) throw new Error("The layout save needs the workflow id and a positive canvas layout.");
|
|
17963
|
+
await memory.seteditorlayout(workflowid, layout);
|
|
17964
|
+
return { workflowid, saved: true };
|
|
17965
|
+
}
|
|
17966
|
+
case "setworkflowbreakpoints": {
|
|
17967
|
+
const inputbreakpoints = message;
|
|
17968
|
+
const workflowid = inputbreakpoints.workflowid?.trim() ?? "";
|
|
17969
|
+
if (!workflowid) throw new Error("The breakpoint save needs the workflow id.");
|
|
17970
|
+
const stepids = Array.isArray(inputbreakpoints.stepids) ? inputbreakpoints.stepids.filter((id) => typeof id === "string" && id.trim() !== "") : [];
|
|
17971
|
+
await memory.setworkflowbreakpoints(workflowid, stepids);
|
|
17972
|
+
await audit("workflow", `The user set ${stepids.length} breakpoint${stepids.length === 1 ? "" : "s"} on the workflow ${workflowid}; a debug run pauses before every marked step.`, {});
|
|
17973
|
+
return { workflowid, breakpoints: stepids.length };
|
|
17974
|
+
}
|
|
17975
|
+
case "steplibrarystore": {
|
|
17976
|
+
return { categories: palettecategories, palette: palettenodes, library: buildsteplibrary(reviewedkinds()) };
|
|
17977
|
+
}
|
|
17978
|
+
case "importworkflow": {
|
|
17979
|
+
const inputimport = message;
|
|
17980
|
+
const session = await memory.getsession();
|
|
17981
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Importing a workflow file needs an active browser session behind the consent gates.");
|
|
17982
|
+
if (typeof inputimport.contents !== "string" || inputimport.contents.trim() === "") throw new Error("The workflow import needs the file contents.");
|
|
17983
|
+
const format = inputimport.format === "yaml" ? "yaml" : inputimport.format === "json" ? "json" : inputimport.contents.trimStart().startsWith("{") ? "json" : "yaml";
|
|
17984
|
+
const loaded = importworkflow({ contents: inputimport.contents, format, now: Date.now(), kindallowed: (kind) => {
|
|
17985
|
+
try {
|
|
17986
|
+
actionrisk(kind);
|
|
17987
|
+
return true;
|
|
17988
|
+
} catch {
|
|
17989
|
+
return false;
|
|
17990
|
+
}
|
|
17991
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
17992
|
+
const importid = randomid();
|
|
17993
|
+
await memory.addworkflowrecord(loaded.record);
|
|
17994
|
+
await memory.addworkflowimport({ id: importid, record: loaded.record, importedat: Date.now(), ...typeof inputimport.filename === "string" && inputimport.filename.trim() !== "" ? { filename: inputimport.filename.trim() } : {} });
|
|
17995
|
+
for (const template of loaded.templates) await memory.addsteptemplate(template);
|
|
17996
|
+
await audit("workflow", `Imported the workflow ${loaded.record.name} version ${loaded.record.version} from a ${format} file with ${loaded.record.steps.length} expanded step${loaded.record.steps.length === 1 ? "" : "s"} and ${loaded.templates.length} packed template${loaded.templates.length === 1 ? "" : "s"}; the record stays unreviewed until the import review approves its step list.`, { sessionid: session.id });
|
|
17997
|
+
return { importid, workflowid: loaded.record.id, name: loaded.record.name, version: loaded.record.version, steps: loaded.record.steps.length, risk: loaded.record.risk, reviewstate: "pending", templates: loaded.templates.length };
|
|
17998
|
+
}
|
|
17999
|
+
case "approveimport": {
|
|
18000
|
+
const inputapproveimport = message;
|
|
18001
|
+
const session = await memory.getsession();
|
|
18002
|
+
const pending = (await memory.listworkflowimports()).find((entry) => entry.id === (inputapproveimport.importid ?? ""));
|
|
18003
|
+
if (!pending) throw new Error(`No pending workflow import matches ${inputapproveimport.importid ?? ""}.`);
|
|
18004
|
+
const approved = { ...pending.record, reviewstate: "approved" };
|
|
18005
|
+
await memory.addworkflowrecord(approved);
|
|
18006
|
+
await memory.removeworkflowimport(pending.id);
|
|
18007
|
+
await audit("workflow", `The user approved the import review of the workflow ${approved.name} version ${approved.version} with its ${approved.steps.length} expanded step${approved.steps.length === 1 ? "" : "s"} shown; the workflow may now run behind the same gates as every composed workflow.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18008
|
+
return { workflowid: approved.id, version: approved.version, reviewstate: "approved" };
|
|
18009
|
+
}
|
|
18010
|
+
case "rejectimport": {
|
|
18011
|
+
const inputreject = message;
|
|
18012
|
+
const session = await memory.getsession();
|
|
18013
|
+
const pending = (await memory.listworkflowimports()).find((entry) => entry.id === (inputreject.importid ?? ""));
|
|
18014
|
+
if (!pending) throw new Error(`No pending workflow import matches ${inputreject.importid ?? ""}.`);
|
|
18015
|
+
await memory.removeworkflowversion(pending.record.id, pending.record.version);
|
|
18016
|
+
await memory.removeworkflowimport(pending.id);
|
|
18017
|
+
await audit("workflow", `The user rejected the import review of the workflow ${pending.record.name} version ${pending.record.version}; the pending record left the library and nothing of the import runs.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18018
|
+
return { rejected: true, importid: pending.id };
|
|
18019
|
+
}
|
|
18020
|
+
case "exportworkflow": {
|
|
18021
|
+
const inputexport = message;
|
|
18022
|
+
const session = await memory.getsession();
|
|
18023
|
+
const record2 = await memory.getworkflowrecord(inputexport.workflowid?.trim() ?? "");
|
|
18024
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputexport.workflowid ?? ""}.`);
|
|
18025
|
+
const format = inputexport.format === "yaml" ? "yaml" : "json";
|
|
18026
|
+
const exported = exportworkflow(record2, format, typeof inputexport.note === "string" ? inputexport.note : void 0, Date.now());
|
|
18027
|
+
const review = exportcontentreview(exported.file);
|
|
18028
|
+
if (!review.allowed) throw new Error(review.reason ?? "The export carries a secret field and secrets never leave the browser.");
|
|
18029
|
+
await audit("workflow", `Exported the workflow ${record2.name} version ${record2.version} as a ${format} file with ${record2.steps.length} expanded step${record2.steps.length === 1 ? "" : "s"}; the export content review verified that no secret field leaves the browser.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18030
|
+
return { format, contents: exported.contents, filename: `${record2.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-v${record2.version}.${format}` };
|
|
18031
|
+
}
|
|
18032
|
+
case "shareworkflow": {
|
|
18033
|
+
const inputshare = message;
|
|
18034
|
+
const session = await memory.getsession();
|
|
18035
|
+
const record2 = await memory.getworkflowrecord(inputshare.workflowid?.trim() ?? "");
|
|
18036
|
+
if (!record2) throw new Error(`No composed workflow matches ${inputshare.workflowid ?? ""}.`);
|
|
18037
|
+
const format = inputshare.format === "yaml" ? "yaml" : "json";
|
|
18038
|
+
const templates = await memory.getsteptemplates();
|
|
18039
|
+
const shared = shareworkflow(record2, templates, format, typeof inputshare.note === "string" ? inputshare.note : void 0, Date.now());
|
|
18040
|
+
const review = exportcontentreview(shared.file);
|
|
18041
|
+
if (!review.allowed) throw new Error(review.reason ?? "The share bundle carries a secret field and secrets never leave the browser.");
|
|
18042
|
+
await audit("workflow", `Packed the workflow ${record2.name} version ${record2.version} with ${templates.length} shared step template${templates.length === 1 ? "" : "s"} into one ${format} share file; the export content review verified that no secret field leaves the browser.`, { ...session !== void 0 ? { sessionid: session.id } : {} });
|
|
18043
|
+
return { format, contents: shared.contents, filename: `${record2.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-share-v${record2.version}.${format}` };
|
|
18044
|
+
}
|
|
18045
|
+
case "rollbackversion": {
|
|
18046
|
+
const inputrollback = message;
|
|
18047
|
+
const session = await memory.getsession();
|
|
18048
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("A version rollback needs an active browser session behind the consent gates.");
|
|
18049
|
+
const versions = await memory.getworkflowrecordversions();
|
|
18050
|
+
const target = versions.find((entry) => entry.id === (inputrollback.workflowid?.trim() ?? "") && entry.version === inputrollback.version);
|
|
18051
|
+
if (!target) throw new Error(`No stored workflow version matches ${inputrollback.workflowid ?? ""} version ${String(inputrollback.version ?? "")}.`);
|
|
18052
|
+
const latest = versions.filter((entry) => entry.id === target.id).reduce((max, entry) => Math.max(max, entry.version), 0);
|
|
18053
|
+
const rolledback = composeworkflow({ id: target.id, name: target.name, version: latest + 1, origins: [...target.origins], steps: target.steps.map((step) => ({ ...step })), blocks: target.blocks.map((block) => ({ ...block })), now: Date.now(), kindallowed: (kind) => {
|
|
18054
|
+
try {
|
|
18055
|
+
actionrisk(kind);
|
|
18056
|
+
return true;
|
|
18057
|
+
} catch {
|
|
18058
|
+
return false;
|
|
18059
|
+
}
|
|
18060
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
18061
|
+
const pendingrollback = { ...rolledback, reviewstate: "pending" };
|
|
18062
|
+
await memory.addworkflowrecord(pendingrollback);
|
|
18063
|
+
await memory.addworkflowversion({ workflowid: rolledback.id, version: rolledback.version, createdat: Date.now(), note: `Rolled back to version ${target.version} of ${target.steps.length} steps`, steps: rolledback.steps.length, risk: rolledback.risk, rollback: true });
|
|
18064
|
+
await audit("workflow", `The user rolled the workflow ${target.name} back to version ${target.version}; the restored steps stored as version ${rolledback.version} and the rollback review gates its first run like a fresh import.`, { sessionid: session.id });
|
|
18065
|
+
return { workflowid: rolledback.id, version: rolledback.version, restoredfrom: target.version, steps: rolledback.steps.length, reviewstate: "pending" };
|
|
18066
|
+
}
|
|
18067
|
+
case "diffversions": {
|
|
18068
|
+
const inputdiff = message;
|
|
18069
|
+
const versions = await memory.getworkflowrecordversions();
|
|
18070
|
+
const from = versions.find((entry) => entry.id === (inputdiff.workflowid?.trim() ?? "") && entry.version === inputdiff.from);
|
|
18071
|
+
const to = versions.find((entry) => entry.id === (inputdiff.workflowid?.trim() ?? "") && entry.version === inputdiff.to);
|
|
18072
|
+
if (!from || !to) throw new Error(`The version diff needs two stored versions of ${inputdiff.workflowid ?? ""}.`);
|
|
18073
|
+
const diff = diffversions(from, to, Date.now());
|
|
18074
|
+
await memory.addversiondiff(diff);
|
|
18075
|
+
return diff;
|
|
18076
|
+
}
|
|
18077
|
+
case "runhistory": {
|
|
18078
|
+
const query = runhistoryquery(message);
|
|
18079
|
+
return runhistoryreport({ entries: await memory.gethistory(query), query });
|
|
18080
|
+
}
|
|
18081
|
+
case "setrunhistoryretention": {
|
|
18082
|
+
const inputretention = message;
|
|
18083
|
+
const settings = await memory.getsettings();
|
|
18084
|
+
const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
|
|
18085
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { runhistoryretention: retention } : {} });
|
|
18086
|
+
await audit("configure", `The user set the run history retention to ${retention === void 0 ? "keep every entry" : `${retention} entr${retention === 1 ? "y" : "ies"}`}; no code ceiling applies.`);
|
|
18087
|
+
return { runhistoryretention: retention };
|
|
18088
|
+
}
|
|
18089
|
+
case "setbackgroundrun": {
|
|
18090
|
+
const inputbackground = message;
|
|
18091
|
+
const workflowid = inputbackground.workflowid?.trim() ?? "";
|
|
18092
|
+
if (!workflowid) throw new Error("The background run toggle needs the workflow id.");
|
|
18093
|
+
const flags = await memory.getbackgroundruns();
|
|
18094
|
+
const enabled = inputbackground.enabled === true;
|
|
18095
|
+
await memory.setbackgroundruns({ ...flags, [workflowid]: enabled });
|
|
18096
|
+
await audit("workflow", `The user ${enabled ? "enabled" : "disabled"} the background run toggle of the workflow ${workflowid}${enabled ? "; its runs keep executing in the service worker with the panel closed and every worker wake restores an interrupted run through the same gates" : ""}.`, {});
|
|
18097
|
+
return { workflowid, enabled };
|
|
18098
|
+
}
|
|
18099
|
+
case "setwatchdog": {
|
|
18100
|
+
const inputwatchdog = message;
|
|
18101
|
+
const config = inputwatchdog.config;
|
|
18102
|
+
if (!config) throw new Error("The watchdog save needs the configuration.");
|
|
18103
|
+
const check = watchdogconfigvalid(config);
|
|
18104
|
+
if (!check.allowed) throw new Error(check.reason ?? "The watchdog configuration failed its review.");
|
|
18105
|
+
const settings = await memory.getsettings();
|
|
18106
|
+
await memory.setsettings({ ...settings, watchdog: config });
|
|
18107
|
+
await audit("configure", `The user configured the workflow watchdog: stall threshold ${config.stallthreshold} ms, recovery ${config.action}${config.zombiewindow !== void 0 ? `, zombie window ${config.zombiewindow} ms` : ""}; both stay user values with no code ceiling.`, {});
|
|
18108
|
+
return { watchdog: config };
|
|
18109
|
+
}
|
|
18110
|
+
case "watchdogscan": {
|
|
18111
|
+
const events = await runwatchdog();
|
|
18112
|
+
return { events, config: (await memory.getsettings())?.watchdog };
|
|
18113
|
+
}
|
|
18114
|
+
case "setsiteoverride": {
|
|
18115
|
+
const inputoverride = message;
|
|
18116
|
+
const session = await memory.getsession();
|
|
18117
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("A per site override needs an active browser session behind the consent gates.");
|
|
18118
|
+
const workflowid = inputoverride.workflowid?.trim() ?? "";
|
|
18119
|
+
if (!workflowid) throw new Error("The per site override needs the workflow id.");
|
|
18120
|
+
const record2 = await memory.getworkflowrecord(workflowid);
|
|
18121
|
+
if (!record2) throw new Error(`No composed workflow matches ${workflowid}.`);
|
|
18122
|
+
const deltas = inputoverride.deltas !== void 0 && typeof inputoverride.deltas === "object" && !Array.isArray(inputoverride.deltas) ? inputoverride.deltas : {};
|
|
18123
|
+
const check = validatesiteoverride({ pattern: inputoverride.pattern ?? "", deltas });
|
|
18124
|
+
if (!check.allowed) throw new Error(check.reason ?? "The per site override failed its review.");
|
|
18125
|
+
const override = { id: typeof inputoverride.id === "string" && inputoverride.id.trim() !== "" ? inputoverride.id : randomid(), workflowid, pattern: inputoverride.pattern.trim(), deltas, createdat: Date.now() };
|
|
18126
|
+
await memory.addsiteoverride(override);
|
|
18127
|
+
await audit("workflow", `The user attached the per site override ${override.pattern} to the workflow ${record2.name}: ${Object.entries(deltas).map(([knob, delta]) => `${knob} ${delta}`).join(", ") || "no delta"}; the override adjusts only the reviewed knobs.`, { sessionid: session.id });
|
|
18128
|
+
return { override };
|
|
18129
|
+
}
|
|
18130
|
+
case "removesiteoverride": {
|
|
18131
|
+
const inputremove = message;
|
|
18132
|
+
const removed = (await memory.listsiteoverrides()).find((entry) => entry.id === (inputremove.id ?? ""));
|
|
18133
|
+
if (!removed) throw new Error(`No per site override matches ${inputremove.id ?? ""}.`);
|
|
18134
|
+
await memory.removesiteoverride(removed.id);
|
|
18135
|
+
await audit("workflow", `The user removed the per site override ${removed.pattern} of the workflow ${removed.workflowid}.`, {});
|
|
18136
|
+
return { removed: removed.id };
|
|
18137
|
+
}
|
|
16032
18138
|
default:
|
|
16033
18139
|
throw new Error("Unknown Devthink request.");
|
|
16034
18140
|
}
|
|
16035
18141
|
}
|
|
18142
|
+
async function runworkflowsegment(input) {
|
|
18143
|
+
const segmentids = new Set(input.record.steps.slice(input.startcursor, input.until).map((entry) => entry.id));
|
|
18144
|
+
const steps = input.record.steps.slice(input.startcursor, input.until).map((entry) => {
|
|
18145
|
+
const bindings = (entry.bindings ?? []).filter((binding) => segmentids.has(binding.stepid));
|
|
18146
|
+
return { ...entry, ...bindings.length > 0 ? { bindings } : {} };
|
|
18147
|
+
});
|
|
18148
|
+
const segmentrecord = composeworkflow({ id: input.record.id, name: input.record.name, version: input.record.version, origins: [...input.record.origins], steps, blocks: [], now: Date.now(), kindallowed: (kind) => {
|
|
18149
|
+
try {
|
|
18150
|
+
actionrisk(kind);
|
|
18151
|
+
return true;
|
|
18152
|
+
} catch {
|
|
18153
|
+
return false;
|
|
18154
|
+
}
|
|
18155
|
+
}, riskof: (kind) => actionrisk(kind) });
|
|
18156
|
+
const { pausedat, ...baserun } = input.run;
|
|
18157
|
+
void pausedat;
|
|
18158
|
+
const segmentrun = { ...baserun, state: "running", cursor: 0 };
|
|
18159
|
+
let storedentries = input.storedentries;
|
|
18160
|
+
const result = await runworkflow({ record: segmentrecord, run: segmentrun, scopes: input.scopes, log: input.log, execute: input.execute, now: Date.now(), gates: { sessionactive: Boolean(input.session && !input.session.stoppedat && !input.session.pausedat && input.session.expiresat > Date.now()), planapproved: input.plan.state === "approved", origingranted: (workfloworigin) => origingranted(input.session, workfloworigin) }, oncheckpoint: async (state) => {
|
|
18161
|
+
await memory.setworkflowrun({ ...state.run, cursor: input.startcursor + state.run.cursor, ...state.run.background === true ? { background: true } : {} });
|
|
18162
|
+
for (const entry of state.log.slice(storedentries)) await memory.addrunlogentry(state.run.id, entry);
|
|
18163
|
+
storedentries = state.log.length;
|
|
18164
|
+
await memory.setrunscopes(state.run.id, state.scopes);
|
|
18165
|
+
} });
|
|
18166
|
+
for (const entry of result.log.slice(storedentries)) await memory.addrunlogentry(input.run.id, entry);
|
|
18167
|
+
for (const entry of result.log.slice(input.log.length)) {
|
|
18168
|
+
const control = entry.details?.control;
|
|
18169
|
+
if (control && typeof control === "object" && !Array.isArray(control)) {
|
|
18170
|
+
const decision = control;
|
|
18171
|
+
await memory.addcontroldecision(input.run.id, decision);
|
|
18172
|
+
await auditcontroldecision(input.run.id, decision, input.session.id, input.plan.id);
|
|
18173
|
+
}
|
|
18174
|
+
}
|
|
18175
|
+
await memory.setrunscopes(input.run.id, result.scopes);
|
|
18176
|
+
const mapped = { ...input.run, state: result.run.state, cursor: input.startcursor + result.run.cursor, ...result.run.failreason !== void 0 ? { failreason: result.run.failreason } : {}, ...result.run.endedat !== void 0 ? { endedat: result.run.endedat } : {} };
|
|
18177
|
+
return { run: mapped, scopes: result.scopes, log: result.log, ...result.run.state === "failed" ? { failed: result.run.failreason ?? "The segment failed." } : {} };
|
|
18178
|
+
}
|
|
18179
|
+
async function runwatchdog() {
|
|
18180
|
+
const config = (await memory.getsettings())?.watchdog;
|
|
18181
|
+
if (!config || !config.enabled) return [];
|
|
18182
|
+
const runs = await memory.listworkflowruns();
|
|
18183
|
+
const lastcompletedat = {};
|
|
18184
|
+
for (const run of runs) {
|
|
18185
|
+
const log = await memory.getrunlog(run.id);
|
|
18186
|
+
const last = [...log].reverse().find((entry) => entry.state === "done");
|
|
18187
|
+
lastcompletedat[run.id] = last !== void 0 ? last.startedat + last.duration : run.startedat;
|
|
18188
|
+
}
|
|
18189
|
+
const verdicts = watchdogpass({ runs, lastcompletedat, liveexecutors: [...activeworkflowruns.keys()], config, now: Date.now() });
|
|
18190
|
+
const events = [];
|
|
18191
|
+
for (const verdict of verdicts) {
|
|
18192
|
+
if (verdict.verdict === "healthy") continue;
|
|
18193
|
+
const run = runs.find((entry) => entry.id === verdict.runid);
|
|
18194
|
+
if (!run) continue;
|
|
18195
|
+
let outcome = "";
|
|
18196
|
+
if (verdict.action === "pause") {
|
|
18197
|
+
const guards = activeworkflowruns.get(run.id);
|
|
18198
|
+
if (guards) guards.cancelled = true;
|
|
18199
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), pausekind: "watchdog" });
|
|
18200
|
+
outcome = `Paused at the checkpoint of step cursor ${run.cursor}; a user resume continues exactly there.`;
|
|
18201
|
+
} else if (verdict.action === "cancel" || verdict.action === "reap") {
|
|
18202
|
+
const guards = activeworkflowruns.get(run.id);
|
|
18203
|
+
if (guards) guards.cancelled = true;
|
|
18204
|
+
const cancelled = cancelrun(run, verdict.action === "reap" ? "The watchdog reaped the run as a zombie of a browser shutdown." : "The watchdog cancelled the stalled run.", Date.now());
|
|
18205
|
+
await memory.setworkflowrun(cancelled);
|
|
18206
|
+
outcome = verdict.action === "reap" ? "Reaped as a zombie of a browser shutdown at its last checkpoint." : "Cancelled as stalled beyond the reviewed threshold.";
|
|
18207
|
+
} else if (verdict.action === "retry") {
|
|
18208
|
+
await handlerequest({ kind: "resumeworkflowrun", runid: run.id }, {}).catch(() => {
|
|
18209
|
+
});
|
|
18210
|
+
outcome = `Retried the stalled step at cursor ${run.cursor} through the run gates.`;
|
|
18211
|
+
}
|
|
18212
|
+
const event = { id: randomid(), runid: run.id, verdict: verdict.verdict, action: verdict.action, outcome: `${verdict.reason} ${outcome}`.trim(), at: Date.now() };
|
|
18213
|
+
await memory.addwatchdogevent(event);
|
|
18214
|
+
await audit("workflow", `The watchdog marked the run ${run.id} ${verdict.verdict} and recovered it with ${verdict.action}: ${outcome}`, {});
|
|
18215
|
+
events.push(event);
|
|
18216
|
+
}
|
|
18217
|
+
await refreshbadge().catch(() => {
|
|
18218
|
+
});
|
|
18219
|
+
return events;
|
|
18220
|
+
}
|
|
18221
|
+
async function restorebackgroundruns() {
|
|
18222
|
+
const session = await memory.getsession();
|
|
18223
|
+
const plan = await memory.getplan();
|
|
18224
|
+
if (!session || session.stoppedat || session.pausedat || session.expiresat <= Date.now() || !plan || plan.state !== "approved") return;
|
|
18225
|
+
const flags = await memory.getbackgroundruns();
|
|
18226
|
+
for (const run of await memory.listworkflowruns()) {
|
|
18227
|
+
if (run.state !== "paused" || run.background !== true || run.pausekind === "user" || run.pausekind === "watchdog") continue;
|
|
18228
|
+
if (flags[run.workflowid] !== true) continue;
|
|
18229
|
+
const record2 = await memory.getworkflowrecord(run.workflowid);
|
|
18230
|
+
if (!record2) continue;
|
|
18231
|
+
let granted = true;
|
|
18232
|
+
for (const workfloworigin of record2.origins) {
|
|
18233
|
+
if (!origingranted(session, workfloworigin)) granted = false;
|
|
18234
|
+
}
|
|
18235
|
+
if (!granted) continue;
|
|
18236
|
+
const resumed = await handlerequest({ kind: "resumeworkflowrun", runid: run.id }, {}).catch(() => void 0);
|
|
18237
|
+
if (resumed === void 0) continue;
|
|
18238
|
+
await audit("workflow", `The worker wake restored the background run ${run.id} of the workflow ${record2.name} from its checkpoint at step cursor ${run.cursor}; the session, plan and origin gates re-passed.`, { sessionid: session.id, planid: plan.id });
|
|
18239
|
+
}
|
|
18240
|
+
}
|
|
16036
18241
|
chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
|
|
16037
18242
|
handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
16038
18243
|
return true;
|
|
@@ -16053,12 +18258,15 @@ async function detectcrash() {
|
|
|
16053
18258
|
}
|
|
16054
18259
|
chrome.runtime.onStartup.addListener(() => {
|
|
16055
18260
|
void detectcrash();
|
|
16056
|
-
void pauseinterruptedworkflowruns()
|
|
18261
|
+
void pauseinterruptedworkflowruns().then(() => restorebackgroundruns()).catch(() => {
|
|
18262
|
+
});
|
|
18263
|
+
void runwatchdog().catch(() => {
|
|
18264
|
+
});
|
|
16057
18265
|
});
|
|
16058
18266
|
async function pauseinterruptedworkflowruns() {
|
|
16059
18267
|
for (const run of await memory.listworkflowruns()) {
|
|
16060
18268
|
if (run.state !== "running") continue;
|
|
16061
|
-
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now() });
|
|
18269
|
+
await memory.setworkflowrun({ ...run, state: "paused", pausedat: Date.now(), pausekind: "interrupt" });
|
|
16062
18270
|
await audit("workflow", `The service worker restart paused the workflow run ${run.id} at its last checkpoint of step cursor ${run.cursor}; the resume continues exactly there.`, {});
|
|
16063
18271
|
}
|
|
16064
18272
|
}
|
|
@@ -16128,4 +18336,82 @@ chrome.runtime.onConnect.addListener((port) => {
|
|
|
16128
18336
|
handlerequest(message, port.sender ?? {}).then((value) => port.postMessage({ ok: true, value })).catch((error) => port.postMessage({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
16129
18337
|
});
|
|
16130
18338
|
});
|
|
18339
|
+
{
|
|
18340
|
+
const webnavigation = chrome.webNavigation;
|
|
18341
|
+
try {
|
|
18342
|
+
webnavigation?.onCommitted?.addListener((details) => {
|
|
18343
|
+
if (details.frameId !== 0 || !details.url.startsWith("https://")) return;
|
|
18344
|
+
void evaluatenavigationtriggers(details.url).catch(() => {
|
|
18345
|
+
});
|
|
18346
|
+
});
|
|
18347
|
+
} catch {
|
|
18348
|
+
}
|
|
18349
|
+
}
|
|
18350
|
+
{
|
|
18351
|
+
const menus = chrome.contextMenus;
|
|
18352
|
+
try {
|
|
18353
|
+
menus?.onClicked?.addListener((info) => {
|
|
18354
|
+
const menuid = typeof info.menuItemId === "string" ? info.menuItemId : "";
|
|
18355
|
+
if (!menuid.startsWith("devthinktrigger")) return;
|
|
18356
|
+
void (async () => {
|
|
18357
|
+
const rule = await memory.gettriggerule(menuid.slice("devthinktrigger".length));
|
|
18358
|
+
if (!rule) return;
|
|
18359
|
+
await firetrigger(rule, "menu");
|
|
18360
|
+
})().catch(() => {
|
|
18361
|
+
});
|
|
18362
|
+
});
|
|
18363
|
+
} catch {
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
{
|
|
18367
|
+
const commands = chrome.commands;
|
|
18368
|
+
try {
|
|
18369
|
+
commands?.onCommand?.addListener((command) => {
|
|
18370
|
+
void (async () => {
|
|
18371
|
+
for (const { rule } of await memory.listtriggers()) {
|
|
18372
|
+
if (rule.kind !== "key" || rule.command !== command) continue;
|
|
18373
|
+
await firetrigger(rule, "key");
|
|
18374
|
+
}
|
|
18375
|
+
})().catch(() => {
|
|
18376
|
+
});
|
|
18377
|
+
});
|
|
18378
|
+
} catch {
|
|
18379
|
+
}
|
|
18380
|
+
}
|
|
18381
|
+
{
|
|
18382
|
+
const action = chrome.action;
|
|
18383
|
+
try {
|
|
18384
|
+
action?.onClicked?.addListener(() => {
|
|
18385
|
+
void handlerequest({ kind: "buttontrigger" }, {}).catch(() => {
|
|
18386
|
+
});
|
|
18387
|
+
});
|
|
18388
|
+
} catch {
|
|
18389
|
+
}
|
|
18390
|
+
}
|
|
18391
|
+
{
|
|
18392
|
+
const alarms = chrome.alarms;
|
|
18393
|
+
try {
|
|
18394
|
+
alarms?.onAlarm?.addListener((alarm) => {
|
|
18395
|
+
if (alarm.name !== "devthinktriggerwake") return;
|
|
18396
|
+
void evaluatelistedtriggers().then(() => draintriggerqueue()).catch(() => {
|
|
18397
|
+
});
|
|
18398
|
+
});
|
|
18399
|
+
} catch {
|
|
18400
|
+
}
|
|
18401
|
+
}
|
|
18402
|
+
setInterval(() => {
|
|
18403
|
+
void evaluatelistedtriggers().then(() => draintriggerqueue()).then(() => runwatchdog()).then(() => restorebackgroundruns()).catch(() => {
|
|
18404
|
+
});
|
|
18405
|
+
}, 3e4);
|
|
18406
|
+
async function restoretriggers() {
|
|
18407
|
+
await registermenurules();
|
|
18408
|
+
await evaluatelistedtriggers();
|
|
18409
|
+
await draintriggerqueue();
|
|
18410
|
+
await runwatchdog().catch(() => {
|
|
18411
|
+
});
|
|
18412
|
+
await restorebackgroundruns().catch(() => {
|
|
18413
|
+
});
|
|
18414
|
+
}
|
|
18415
|
+
restoretriggers().catch(() => {
|
|
18416
|
+
});
|
|
16131
18417
|
//# sourceMappingURL=background.js.map
|