@wenathlan/extension 1.1.50 → 1.1.52

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.
@@ -2191,6 +2191,97 @@ var sessionmemory = class {
2191
2191
  async getsteptemplates() {
2192
2192
  return await this.adapter.get("steptemplates") ?? [];
2193
2193
  }
2194
+ /** Records one control flow decision of a run — a branch choice with its reason, the loop counters of an iteration trail, the retry attempts with their backoff durations, the timeout aborts with the exceeded budget, the join record with its strategy and conflicts or the catch handler execution — so the audit trail keeps every control flow turn. */
2195
+ async addcontroldecision(runid, decision) {
2196
+ const decisions = await this.listcontroldecisions(runid);
2197
+ await this.adapter.set(`controldecisions${runid}`, [...decisions, { ...decision, runid }]);
2198
+ }
2199
+ /** Returns every stored control flow decision of one run, oldest first. */
2200
+ async listcontroldecisions(runid) {
2201
+ return await this.adapter.get(`controldecisions${runid}`) ?? [];
2202
+ }
2203
+ /** Returns the past branch decisions of one workflow across every stored run, oldest first, so review can compare branch paths over time. */
2204
+ async getbranchhistory(workflowid) {
2205
+ const runs = await this.listworkflowruns();
2206
+ const ordered = [...runs].reverse().filter((run) => run.workflowid === workflowid);
2207
+ const history2 = [];
2208
+ for (const run of ordered) {
2209
+ const decisions = await this.listcontroldecisions(run.id);
2210
+ for (const decision of decisions) if (decision.kind === "branch" && decision.branch !== void 0) history2.push(decision.branch);
2211
+ }
2212
+ return history2;
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
+ }
2194
2285
  };
2195
2286
  function mediakindof(record2) {
2196
2287
  if ("pages" in record2) return "pdf";
@@ -3177,6 +3268,678 @@ function teardowncdpsession(input) {
3177
3268
  };
3178
3269
  }
3179
3270
 
3271
+ // controlflow.ts
3272
+ var controlflowkinds = ["condition", "branch", "loop", "repeatuntil", "whileloop", "foreach", "parallel", "trycatch"];
3273
+ var defaultloopbound = 1e3;
3274
+ function iscontrolflowkind(kind) {
3275
+ return controlflowkinds.includes(kind);
3276
+ }
3277
+ var cancellederror = class extends Error {
3278
+ constructor(message) {
3279
+ super(message);
3280
+ this.name = "cancellederror";
3281
+ }
3282
+ };
3283
+ function controlname(value) {
3284
+ return typeof value === "string" && /^[a-z][a-z0-9]*$/.test(value) ? value : void 0;
3285
+ }
3286
+ function controlstepslist(value) {
3287
+ if (!Array.isArray(value) || value.length === 0) return void 0;
3288
+ const steps = [];
3289
+ for (const entry of value) {
3290
+ const parsed = workflowstepof(entry);
3291
+ if (!parsed) return void 0;
3292
+ steps.push(parsed);
3293
+ }
3294
+ return steps;
3295
+ }
3296
+ function controlbound(value) {
3297
+ if (value === void 0) return void 0;
3298
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
3299
+ }
3300
+ function conditionof(value) {
3301
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3302
+ const candidate = value;
3303
+ const expression = expressionof(candidate.expression);
3304
+ if (!expression) return void 0;
3305
+ if (expression.resultkind !== "boolean") return void 0;
3306
+ return { expression };
3307
+ }
3308
+ function elseof(value) {
3309
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3310
+ const candidate = value;
3311
+ const name = controlname(candidate.name);
3312
+ if (!name) return void 0;
3313
+ if (candidate.when !== void 0) return void 0;
3314
+ if (!Array.isArray(candidate.steps)) return void 0;
3315
+ const steps = [];
3316
+ for (const entry of candidate.steps) {
3317
+ const parsed = workflowstepof(entry);
3318
+ if (!parsed) return void 0;
3319
+ steps.push(parsed);
3320
+ }
3321
+ return { name, steps };
3322
+ }
3323
+ function branchof(value) {
3324
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3325
+ const candidate = value;
3326
+ if (!Array.isArray(candidate.paths) || candidate.paths.length === 0) return void 0;
3327
+ const paths = [];
3328
+ for (const entry of candidate.paths) {
3329
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return void 0;
3330
+ const path = entry;
3331
+ const name = controlname(path.name);
3332
+ if (!name) return void 0;
3333
+ const when = path.when === void 0 ? void 0 : expressionof(path.when);
3334
+ if (path.when !== void 0 && when === void 0) return void 0;
3335
+ if (when !== void 0 && when.resultkind !== "boolean") return void 0;
3336
+ const steps = controlstepslist(path.steps);
3337
+ if (!steps) return void 0;
3338
+ paths.push({ name, ...when !== void 0 ? { when } : {}, steps });
3339
+ }
3340
+ const names = paths.map((path) => path.name);
3341
+ if (new Set(names).size !== names.length) return void 0;
3342
+ const elsepath = elseof(candidate.else);
3343
+ if (!elsepath) return void 0;
3344
+ if (names.includes(elsepath.name)) return void 0;
3345
+ return { paths, else: elsepath };
3346
+ }
3347
+ function loopof(value) {
3348
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3349
+ const candidate = value;
3350
+ const list = controlname(candidate.list);
3351
+ const item = controlname(candidate.item);
3352
+ const index = controlname(candidate.index);
3353
+ if (!list || !item || !index) return void 0;
3354
+ if (item === list || index === list || item === index) return void 0;
3355
+ const bound = controlbound(candidate.bound);
3356
+ if (candidate.bound !== void 0 && bound === void 0) return void 0;
3357
+ const steps = controlstepslist(candidate.steps);
3358
+ if (!steps) return void 0;
3359
+ return { list, item, index, ...bound !== void 0 ? { bound } : {}, steps };
3360
+ }
3361
+ function repeatuntilof(value) {
3362
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3363
+ const candidate = value;
3364
+ const until = expressionof(candidate.until);
3365
+ if (!until || until.resultkind !== "boolean") return void 0;
3366
+ const bound = controlbound(candidate.bound);
3367
+ if (candidate.bound !== void 0 && bound === void 0) return void 0;
3368
+ const steps = controlstepslist(candidate.steps);
3369
+ if (!steps) return void 0;
3370
+ return { until, ...bound !== void 0 ? { bound } : {}, steps };
3371
+ }
3372
+ function whileof(value) {
3373
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3374
+ const candidate = value;
3375
+ const condition = expressionof(candidate.while);
3376
+ if (!condition || condition.resultkind !== "boolean") return void 0;
3377
+ const bound = controlbound(candidate.bound);
3378
+ if (bound === void 0) return void 0;
3379
+ const steps = controlstepslist(candidate.steps);
3380
+ if (!steps) return void 0;
3381
+ return { while: condition, bound, steps };
3382
+ }
3383
+ function foreachof(value) {
3384
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3385
+ const candidate = value;
3386
+ if (typeof candidate.selector !== "string" || !candidate.selector.trim()) return void 0;
3387
+ const item = controlname(candidate.item);
3388
+ const index = controlname(candidate.index);
3389
+ if (!item || !index || item === index) return void 0;
3390
+ const steps = controlstepslist(candidate.steps);
3391
+ if (!steps) return void 0;
3392
+ return { selector: candidate.selector, item, index, steps };
3393
+ }
3394
+ function parallelof(value) {
3395
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3396
+ const candidate = value;
3397
+ if (!Array.isArray(candidate.branches) || candidate.branches.length === 0) return void 0;
3398
+ const branches = [];
3399
+ for (const entry of candidate.branches) {
3400
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return void 0;
3401
+ const branch = entry;
3402
+ const id = controlname(branch.id);
3403
+ if (!id) return void 0;
3404
+ const steps = controlstepslist(branch.steps);
3405
+ if (!steps) return void 0;
3406
+ branches.push({ id, steps });
3407
+ }
3408
+ if (new Set(branches.map((branch) => branch.id)).size !== branches.length) return void 0;
3409
+ const join = candidate.join && typeof candidate.join === "object" && !Array.isArray(candidate.join) ? candidate.join : void 0;
3410
+ if (!join) return void 0;
3411
+ if (join.strategy !== "first" && join.strategy !== "last" && join.strategy !== "fail") return void 0;
3412
+ if (join.onfail !== "cancel" && join.onfail !== "continue") return void 0;
3413
+ return { branches, join: { strategy: join.strategy, onfail: join.onfail } };
3414
+ }
3415
+ function tryof(value) {
3416
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3417
+ const candidate = value;
3418
+ const steps = controlstepslist(candidate.steps);
3419
+ if (!steps) return void 0;
3420
+ const catchcandidate = candidate.catch && typeof candidate.catch === "object" && !Array.isArray(candidate.catch) ? candidate.catch : void 0;
3421
+ if (!catchcandidate) return void 0;
3422
+ const catchsteps = controlstepslist(catchcandidate.steps);
3423
+ if (!catchsteps) return void 0;
3424
+ if (catchcandidate.rerun !== void 0 && typeof catchcandidate.rerun !== "boolean") return void 0;
3425
+ const catchvalue = { steps: catchsteps, ...catchcandidate.rerun === true ? { rerun: true } : {} };
3426
+ let retry;
3427
+ if (candidate.retry !== void 0) {
3428
+ const retrycandidate = candidate.retry && typeof candidate.retry === "object" && !Array.isArray(candidate.retry) ? candidate.retry : void 0;
3429
+ if (!retrycandidate) return void 0;
3430
+ if (typeof retrycandidate.attempts !== "number" || !Number.isInteger(retrycandidate.attempts) || retrycandidate.attempts < 1) return void 0;
3431
+ const backoff = retrycandidate.backoff && typeof retrycandidate.backoff === "object" && !Array.isArray(retrycandidate.backoff) ? retrycandidate.backoff : void 0;
3432
+ if (!backoff) return void 0;
3433
+ if (backoff.shape !== "fixed" && backoff.shape !== "exponential") return void 0;
3434
+ if (typeof backoff.base !== "number" || !Number.isFinite(backoff.base) || backoff.base < 0) return void 0;
3435
+ if (typeof backoff.jitter !== "number" || !Number.isFinite(backoff.jitter) || backoff.jitter < 0) return void 0;
3436
+ if (!Array.isArray(retrycandidate.retryable) || !retrycandidate.retryable.every((entry) => typeof entry === "string" && entry.trim())) return void 0;
3437
+ retry = { attempts: retrycandidate.attempts, backoff: { shape: backoff.shape, base: backoff.base, jitter: backoff.jitter }, retryable: retrycandidate.retryable };
3438
+ }
3439
+ let timeout;
3440
+ if (candidate.timeout !== void 0) {
3441
+ const timeoutcandidate = candidate.timeout && typeof candidate.timeout === "object" && !Array.isArray(candidate.timeout) ? candidate.timeout : void 0;
3442
+ if (!timeoutcandidate) return void 0;
3443
+ const stepms = timeoutcandidate.stepms === void 0 ? void 0 : typeof timeoutcandidate.stepms === "number" && Number.isFinite(timeoutcandidate.stepms) && timeoutcandidate.stepms > 0 ? timeoutcandidate.stepms : void 0;
3444
+ const runms = timeoutcandidate.runms === void 0 ? void 0 : typeof timeoutcandidate.runms === "number" && Number.isFinite(timeoutcandidate.runms) && timeoutcandidate.runms > 0 ? timeoutcandidate.runms : void 0;
3445
+ if (stepms === void 0 && runms === void 0) return void 0;
3446
+ if (timeoutcandidate.stepms !== void 0 && stepms === void 0) return void 0;
3447
+ if (timeoutcandidate.runms !== void 0 && runms === void 0) return void 0;
3448
+ timeout = { ...stepms !== void 0 ? { stepms } : {}, ...runms !== void 0 ? { runms } : {} };
3449
+ }
3450
+ return { steps, catch: catchvalue, ...retry !== void 0 ? { retry } : {}, ...timeout !== void 0 ? { timeout } : {} };
3451
+ }
3452
+ function controloptions(step) {
3453
+ if (step.options === void 0) throw new Error(`The ${step.kind} step needs its reviewed control payload in options.`);
3454
+ let parsed;
3455
+ try {
3456
+ parsed = JSON.parse(step.options);
3457
+ } catch {
3458
+ throw new Error(`The ${step.kind} control payload must be a JSON object.`);
3459
+ }
3460
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`The ${step.kind} control payload must be a JSON object.`);
3461
+ return parsed;
3462
+ }
3463
+ function validatecontrolpayload(step) {
3464
+ if (!iscontrolflowkind(step.kind)) return;
3465
+ const payload = controloptions(step);
3466
+ if (step.kind === "condition" && conditionof(payload.condition) === void 0) throw new Error("The condition step needs a reviewed boolean expression in its options.");
3467
+ if (step.kind === "branch" && branchof(payload.branch) === void 0) throw new Error("The branch step needs reviewed unique paths with boolean match expressions and an else path in its options.");
3468
+ if (step.kind === "loop" && loopof(payload.loop) === void 0) throw new Error("The loop step needs a reviewed list variable, distinct item and index variables, an optional positive safety bound and a non-empty body in its options.");
3469
+ if (step.kind === "repeatuntil" && repeatuntilof(payload.repeatuntil) === void 0) throw new Error("The repeat until step needs a reviewed convergence expression, an optional positive safety bound and a non-empty body in its options.");
3470
+ if (step.kind === "whileloop" && whileof(payload.while) === void 0) throw new Error("The while step needs a reviewed condition, a mandatory positive safety bound and a non-empty body in its options.");
3471
+ if (step.kind === "foreach" && foreachof(payload.foreach) === void 0) throw new Error("The foreach step needs a reviewed non-empty selector, distinct item and index variables and a non-empty body in its options.");
3472
+ if (step.kind === "parallel" && parallelof(payload.parallel) === void 0) throw new Error("The parallel step needs uniquely identified branches with bodies and a join policy of the first, last or fail strategy with cancel or continue on branch failure in its options.");
3473
+ if (step.kind === "trycatch" && tryof(payload.try) === void 0) throw new Error("The try step needs a fragile body, a catch handler and optional retry and timeout policies in its options.");
3474
+ }
3475
+ function controlsteps(step) {
3476
+ if (!iscontrolflowkind(step.kind)) return [];
3477
+ let payload;
3478
+ try {
3479
+ payload = controloptions(step);
3480
+ } catch {
3481
+ return [];
3482
+ }
3483
+ const children = [];
3484
+ const collect = (steps) => {
3485
+ for (const child of steps) {
3486
+ children.push(child);
3487
+ collect(controlsteps(child));
3488
+ }
3489
+ };
3490
+ if (step.kind === "condition") return children;
3491
+ if (step.kind === "branch") {
3492
+ const branch = branchof(payload.branch);
3493
+ if (!branch) return children;
3494
+ for (const path of branch.paths) collect(path.steps);
3495
+ collect(branch.else.steps);
3496
+ return children;
3497
+ }
3498
+ if (step.kind === "loop") {
3499
+ const loop = loopof(payload.loop);
3500
+ if (loop) collect(loop.steps);
3501
+ return children;
3502
+ }
3503
+ if (step.kind === "repeatuntil") {
3504
+ const repeat = repeatuntilof(payload.repeatuntil);
3505
+ if (repeat) collect(repeat.steps);
3506
+ return children;
3507
+ }
3508
+ if (step.kind === "whileloop") {
3509
+ const condition = whileof(payload.while);
3510
+ if (condition) collect(condition.steps);
3511
+ return children;
3512
+ }
3513
+ if (step.kind === "foreach") {
3514
+ const foreach = foreachof(payload.foreach);
3515
+ if (foreach) collect(foreach.steps);
3516
+ return children;
3517
+ }
3518
+ if (step.kind === "parallel") {
3519
+ const parallel = parallelof(payload.parallel);
3520
+ if (parallel) for (const branch of parallel.branches) collect(branch.steps);
3521
+ return children;
3522
+ }
3523
+ const fragile = tryof(payload.try);
3524
+ if (fragile) {
3525
+ collect(fragile.steps);
3526
+ collect(fragile.catch.steps);
3527
+ }
3528
+ return children;
3529
+ }
3530
+ function controlsummary(step) {
3531
+ if (!iscontrolflowkind(step.kind)) return void 0;
3532
+ let payload;
3533
+ try {
3534
+ payload = controloptions(step);
3535
+ } catch {
3536
+ return { kind: step.kind };
3537
+ }
3538
+ if (step.kind === "condition") {
3539
+ const condition = conditionof(payload.condition);
3540
+ return { kind: step.kind, ...condition ? { expression: `${condition.expression.operator} into ${condition.expression.result}` } : {} };
3541
+ }
3542
+ if (step.kind === "branch") {
3543
+ const branch = branchof(payload.branch);
3544
+ return { kind: step.kind, ...branch ? { paths: branch.paths.map((path) => path.name), elsepath: branch.else.name } : {} };
3545
+ }
3546
+ if (step.kind === "loop") {
3547
+ const loop = loopof(payload.loop);
3548
+ return { kind: step.kind, ...loop ? { list: loop.list, item: loop.item, index: loop.index, ...loop.bound !== void 0 ? { bound: loop.bound } : { bound: defaultloopbound } } : {} };
3549
+ }
3550
+ if (step.kind === "repeatuntil") {
3551
+ const repeat = repeatuntilof(payload.repeatuntil);
3552
+ return { kind: step.kind, ...repeat ? { bound: repeat.bound ?? defaultloopbound } : {} };
3553
+ }
3554
+ if (step.kind === "whileloop") {
3555
+ const condition = whileof(payload.while);
3556
+ return { kind: step.kind, ...condition ? { bound: condition.bound } : {} };
3557
+ }
3558
+ if (step.kind === "foreach") {
3559
+ const foreach = foreachof(payload.foreach);
3560
+ return { kind: step.kind, ...foreach ? { selector: foreach.selector, item: foreach.item, index: foreach.index } : {} };
3561
+ }
3562
+ if (step.kind === "parallel") {
3563
+ const parallel = parallelof(payload.parallel);
3564
+ return { kind: step.kind, ...parallel ? { branches: parallel.branches.map((branch) => branch.id), strategy: parallel.join.strategy, onfail: parallel.join.onfail } : {} };
3565
+ }
3566
+ const fragile = tryof(payload.try);
3567
+ return { kind: step.kind, ...fragile ? { ...fragile.retry !== void 0 ? { attempts: fragile.retry.attempts, backoff: `${fragile.retry.backoff.shape} base ${fragile.retry.backoff.base} jitter ${fragile.retry.backoff.jitter}` } : {}, ...fragile.catch.rerun === true ? { rerun: true } : {}, ...fragile.timeout?.stepms !== void 0 ? { stepms: fragile.timeout.stepms } : {}, ...fragile.timeout?.runms !== void 0 ? { runms: fragile.timeout.runms } : {} } : {} };
3568
+ }
3569
+ function evaluatecondition(condition, scopes) {
3570
+ const value = expressioneval(condition.expression, scopes);
3571
+ if (typeof value !== "boolean") throw new Error("The condition expression must resolve to a boolean.");
3572
+ return value;
3573
+ }
3574
+ function choosebranch(input) {
3575
+ let scopes = input.scopes;
3576
+ if (input.pagestate !== void 0) {
3577
+ const parent = scopes.length > 0 ? scopes[scopes.length - 1].name : void 0;
3578
+ scopes = pushscope(scopes, `pagestate${input.stepid}`, parent);
3579
+ if (input.pagestate.url !== void 0) scopes = setvariable(scopes, "pageurl", "string", input.pagestate.url, input.now);
3580
+ if (input.pagestate.title !== void 0) scopes = setvariable(scopes, "pagetitle", "string", input.pagestate.title, input.now);
3581
+ if (input.pagestate.ready !== void 0) scopes = setvariable(scopes, "pageready", "boolean", input.pagestate.ready, input.now);
3582
+ }
3583
+ for (const path of input.branch.paths) {
3584
+ if (path.when === void 0) return { outcome: { stepid: input.stepid, path: path.name, reason: `The path ${path.name} matches unconditionally.`, at: input.now }, steps: path.steps };
3585
+ const value = expressioneval(path.when, scopes);
3586
+ if (typeof value !== "boolean") throw new Error(`The branch path ${path.name} needs a boolean expression.`);
3587
+ if (value) return { outcome: { stepid: input.stepid, path: path.name, reason: `The condition of the path ${path.name} holds.`, at: input.now }, steps: path.steps };
3588
+ }
3589
+ return { outcome: { stepid: input.stepid, path: input.branch.else.name, reason: "No path condition held and the else path ran.", at: input.now }, steps: input.branch.else.steps };
3590
+ }
3591
+ async function runbody(input) {
3592
+ let scopes = input.scopes;
3593
+ const outputs = { ...input.outputs };
3594
+ const log = [];
3595
+ for (const child of input.steps) {
3596
+ if (iscontrolflowkind(child.kind)) {
3597
+ const result = await runcontrolstep({ step: child, scopes, outputs, execute: input.execute, now: input.now, ...input.path !== void 0 ? { path: `${input.path}.${child.id}` } : {} });
3598
+ scopes = result.scopes;
3599
+ log.push(...result.log);
3600
+ outputs[child.id] = { stepid: child.id, ok: result.output.ok, summary: result.output.summary, ...result.output.details !== void 0 ? { details: result.output.details } : {}, at: input.now };
3601
+ if (!result.output.ok) return { ok: false, scopes, log, outputs, failure: result.output };
3602
+ continue;
3603
+ }
3604
+ const executed = await runstep({ step: child, scopes, outputs, execute: input.execute, now: input.now });
3605
+ scopes = executed.scopes;
3606
+ if (executed.childlog !== void 0) log.push(...executed.childlog);
3607
+ log.push(executed.log);
3608
+ outputs[child.id] = { stepid: child.id, ok: executed.output.ok, summary: executed.output.summary, ...executed.output.details !== void 0 ? { details: executed.output.details } : {}, at: input.now };
3609
+ if (!executed.output.ok) return { ok: false, scopes, log, outputs, failure: executed.output };
3610
+ }
3611
+ return { ok: true, scopes, log, outputs };
3612
+ }
3613
+ function deepcopy(value) {
3614
+ if (Array.isArray(value)) return value.map(deepcopy);
3615
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, deepcopy(entry)]));
3616
+ return value;
3617
+ }
3618
+ function iterationentry(step, iteration, total, ok, now) {
3619
+ return { stepid: step.id, label: `${step.label} iteration ${iteration + 1}`, state: ok ? "done" : "failed", startedat: now, duration: 0, summary: `Iteration ${iteration + 1} of ${total}.`, details: { iteration, total } };
3620
+ }
3621
+ async function runloop(input) {
3622
+ const list = resolvevariable(input.scopes, input.loop.list);
3623
+ if (!list) throw new Error(`The loop references the undefined list variable ${input.loop.list}.`);
3624
+ if (list.kind !== "list") throw new Error(`The loop variable ${input.loop.list} is not a list.`);
3625
+ const items = list.value;
3626
+ const bound = input.loop.bound ?? defaultloopbound;
3627
+ const loops = [];
3628
+ const decision = { runid: "", stepid: input.step.id, kind: "loop", at: input.now, loops };
3629
+ if (items.length > bound) {
3630
+ return { ok: false, scopes: input.scopes, log: [], summary: `The loop list holds ${items.length} items and exceeds the reviewed safety bound of ${bound} iterations; nothing ran.`, decision };
3631
+ }
3632
+ let scopes = input.scopes;
3633
+ const log = [];
3634
+ for (let index = 0; index < items.length; index += 1) {
3635
+ scopes = setvariable(scopes, input.loop.item, "string", deepcopy(items[index]), input.now);
3636
+ scopes = setvariable(scopes, input.loop.index, "number", index, input.now);
3637
+ const body = await runbody({ step: input.step, scopes, outputs: input.outputs, execute: input.execute, now: input.now, steps: input.loop.steps, path: `${input.path ?? input.step.id}[${index}]` });
3638
+ scopes = body.scopes;
3639
+ const ok = body.ok;
3640
+ loops.push({ stepid: input.step.id, path: `${input.path ?? input.step.id}[${index}]`, iteration: index, ok, at: input.now });
3641
+ log.push(...body.log, iterationentry(input.step, index, items.length, ok, input.now));
3642
+ if (!ok) return { ok: false, scopes, log, summary: `The loop failed at iteration ${index + 1} of ${items.length}: ${body.failure?.summary ?? "the body step failed."}`, decision };
3643
+ }
3644
+ return { ok: true, scopes, log, summary: `The loop ran ${items.length} iteration${items.length === 1 ? "" : "s"} over ${input.loop.list} inside the reviewed safety bound of ${bound}.`, decision };
3645
+ }
3646
+ async function runrepeatuntil(input) {
3647
+ const bound = input.repeat.bound ?? defaultloopbound;
3648
+ let scopes = input.scopes;
3649
+ const log = [];
3650
+ const loops = [];
3651
+ const decision = { runid: "", stepid: input.step.id, kind: "loop", at: input.now, loops };
3652
+ for (let iteration = 0; iteration < bound; iteration += 1) {
3653
+ const body = await runbody({ step: input.step, scopes, outputs: input.outputs, execute: input.execute, now: input.now, steps: input.repeat.steps, path: `${input.path ?? input.step.id}[${iteration}]` });
3654
+ scopes = body.scopes;
3655
+ log.push(...body.log);
3656
+ const converged = evaluatecondition({ expression: input.repeat.until }, scopes);
3657
+ loops.push({ stepid: input.step.id, path: `${input.path ?? input.step.id}[${iteration}]`, iteration, ok: body.ok, at: input.now });
3658
+ if (!body.ok) return { ok: false, scopes, log, summary: `The repeat until failed at iteration ${iteration + 1}: ${body.failure?.summary ?? "the body step failed."}`, decision };
3659
+ log.push(iterationentry(input.step, iteration, bound, true, input.now));
3660
+ if (converged) return { ok: true, scopes, log, summary: `The repeat until converged after ${iteration + 1} iteration${iteration === 0 ? "" : "s"} inside the reviewed safety bound of ${bound}.`, decision };
3661
+ }
3662
+ return { ok: false, scopes, log, summary: `The repeat until never converged within the reviewed safety bound of ${bound} iterations.`, decision };
3663
+ }
3664
+ async function runwhile(input) {
3665
+ let scopes = input.scopes;
3666
+ const log = [];
3667
+ const loops = [];
3668
+ const decision = { runid: "", stepid: input.step.id, kind: "loop", at: input.now, loops };
3669
+ for (let iteration = 0; iteration < input.condition.bound; iteration += 1) {
3670
+ if (!evaluatecondition({ expression: input.condition.while }, scopes)) {
3671
+ return { ok: true, scopes, log, summary: `The while loop ended after ${iteration} iteration${iteration === 1 ? "" : "s"} because its condition stopped holding inside the reviewed safety bound of ${input.condition.bound}.`, decision };
3672
+ }
3673
+ const body = await runbody({ step: input.step, scopes, outputs: input.outputs, execute: input.execute, now: input.now, steps: input.condition.steps, path: `${input.path ?? input.step.id}[${iteration}]` });
3674
+ scopes = body.scopes;
3675
+ log.push(...body.log);
3676
+ loops.push({ stepid: input.step.id, path: `${input.path ?? input.step.id}[${iteration}]`, iteration, ok: body.ok, at: input.now });
3677
+ if (!body.ok) return { ok: false, scopes, log, summary: `The while loop failed at iteration ${iteration + 1}: ${body.failure?.summary ?? "the body step failed."}`, decision };
3678
+ log.push(iterationentry(input.step, iteration, input.condition.bound, true, input.now));
3679
+ }
3680
+ if (evaluatecondition({ expression: input.condition.while }, scopes)) {
3681
+ return { ok: false, scopes, log, summary: `The while loop hit its reviewed safety bound of ${input.condition.bound} iterations while its condition still held; the overflow is reported instead of looping forever.`, decision };
3682
+ }
3683
+ return { ok: true, scopes, log, summary: `The while loop ended after ${input.condition.bound} iteration${input.condition.bound === 1 ? "" : "s"} inside the reviewed safety bound.`, decision };
3684
+ }
3685
+ async function runforeach(input) {
3686
+ if (!input.resolveelements) throw new Error("The foreach step needs the element resolver of the executor seam.");
3687
+ const elements = await input.resolveelements(input.foreach.selector);
3688
+ const loops = [];
3689
+ const decision = { runid: "", stepid: input.step.id, kind: "loop", at: input.now, loops };
3690
+ if (elements.length === 0) return { ok: true, scopes: input.scopes, log: [], summary: `The selector ${input.foreach.selector} matched no element and the foreach ran zero iterations.`, decision };
3691
+ let scopes = input.scopes;
3692
+ const log = [];
3693
+ for (let index = 0; index < elements.length; index += 1) {
3694
+ scopes = setvariable(scopes, input.foreach.item, "element", deepcopy(elements[index]), input.now);
3695
+ scopes = setvariable(scopes, input.foreach.index, "number", index, input.now);
3696
+ const body = await runbody({ step: input.step, scopes, outputs: input.outputs, execute: input.execute, now: input.now, steps: input.foreach.steps, path: `${input.path ?? input.step.id}[${index}]` });
3697
+ scopes = body.scopes;
3698
+ const ok = body.ok;
3699
+ loops.push({ stepid: input.step.id, path: `${input.path ?? input.step.id}[${index}]`, iteration: index, ok, at: input.now });
3700
+ log.push(...body.log, iterationentry(input.step, index, elements.length, ok, input.now));
3701
+ if (!ok) return { ok: false, scopes, log, summary: `The foreach failed at iteration ${index + 1} of ${elements.length}: ${body.failure?.summary ?? "the body step failed."}`, decision };
3702
+ }
3703
+ return { ok: true, scopes, log, summary: `The foreach ran ${elements.length} iteration${elements.length === 1 ? "" : "s"} over the elements of ${input.foreach.selector}.`, decision };
3704
+ }
3705
+ function joinbranches(input) {
3706
+ const contributing = input.branches.filter((branch) => !branch.cancelled);
3707
+ const byname = /* @__PURE__ */ new Map();
3708
+ for (const branch of contributing) for (const variable of branch.variables) {
3709
+ const entries = byname.get(variable.name) ?? [];
3710
+ entries.push({ order: branch.order, value: variable });
3711
+ byname.set(variable.name, entries);
3712
+ }
3713
+ const conflicts = [...byname.entries()].filter(([, entries]) => entries.length > 1).map(([name]) => name);
3714
+ const record2 = { stepid: input.stepid, strategy: input.strategy, conflicts, merged: [], at: input.now };
3715
+ if (conflicts.length > 0 && input.strategy === "fail") {
3716
+ return { ok: false, conflicts, merged: [], record: record2, summary: `The join refused the conflicting writes of ${conflicts.join(", ")} under the fail strategy.` };
3717
+ }
3718
+ const merged = [];
3719
+ for (const [name, entries] of byname) {
3720
+ void name;
3721
+ const winner = input.strategy === "first" ? entries.reduce((left, right) => left.order <= right.order ? left : right) : entries.reduce((left, right) => left.order >= right.order ? left : right);
3722
+ merged.push({ ...winner.value, setat: input.now });
3723
+ }
3724
+ record2.merged = merged.map((variable) => variable.name);
3725
+ return { ok: true, conflicts, merged, record: record2, summary: `The join merged ${merged.length} variable${merged.length === 1 ? "" : "s"} under the ${input.strategy} strategy${conflicts.length > 0 ? ` with the conflicts ${conflicts.join(", ")} resolved by the strategy` : " with no conflict"}.` };
3726
+ }
3727
+ async function runparallel(input) {
3728
+ let finished = 0;
3729
+ let firstfailure = Number.POSITIVE_INFINITY;
3730
+ const log = [];
3731
+ const launches = input.parallel.branches.map((branch, order) => (async () => {
3732
+ const parent = input.scopes.length > 0 ? input.scopes[input.scopes.length - 1].name : void 0;
3733
+ const isolated = pushscope(input.scopes, `branch${branch.id}`, parent);
3734
+ const body = await runbody({ step: input.step, scopes: isolated, outputs: input.outputs, execute: input.execute, now: input.now, steps: branch.steps });
3735
+ const finishedat = finished;
3736
+ finished += 1;
3737
+ if (!body.ok && finishedat < firstfailure) firstfailure = finishedat;
3738
+ const scope = body.scopes[body.scopes.length - 1];
3739
+ return { branch, order, finishedat, ok: body.ok, scopes: body.scopes, variables: scope.name === `branch${branch.id}` ? scope.variables : [], log: body.log, failure: body.failure };
3740
+ })());
3741
+ const settled = await Promise.all(launches);
3742
+ for (const entry of settled) log.push(...entry.log);
3743
+ const cancelmode = input.parallel.join.onfail === "cancel";
3744
+ const outcomes = settled.map((entry) => ({ branchid: entry.branch.id, ok: entry.ok, summary: entry.ok ? `The branch ${entry.branch.id} completed.` : entry.failure?.summary ?? `The branch ${entry.branch.id} failed.`, ...cancelmode && firstfailure !== Number.POSITIVE_INFINITY && entry.finishedat > firstfailure ? { cancelled: true } : {} }));
3745
+ const join = joinbranches({ stepid: input.step.id, branches: settled.map((entry, order) => ({ id: entry.branch.id, order, ok: entry.ok, cancelled: outcomes[order]?.cancelled === true, variables: entry.variables })), strategy: input.parallel.join.strategy, now: input.now });
3746
+ const decision = { runid: "", stepid: input.step.id, kind: "join", at: input.now, join: join.record, branches: outcomes };
3747
+ if (!join.ok) return { ok: false, scopes: input.scopes, log, summary: join.summary, decision };
3748
+ let scopes = input.scopes;
3749
+ for (const variable of join.merged) scopes = setvariable(scopes, variable.name, variable.kind, variable.value, input.now);
3750
+ const failedbranches = settled.filter((entry, order) => !entry.ok && outcomes[order]?.cancelled !== true).map((entry) => entry.branch.id);
3751
+ if (cancelmode && failedbranches.length > 0) {
3752
+ return { ok: false, scopes, log, summary: `The parallel block failed on branch ${failedbranches.join(", ")} and the join policy cancelled the siblings still running; ${join.summary}`, decision };
3753
+ }
3754
+ return { ok: true, scopes, log, summary: `The parallel block ran ${input.parallel.branches.length} concurrent branch${input.parallel.branches.length === 1 ? "" : "es"}; ${join.summary}`, decision };
3755
+ }
3756
+ function errorclassof(output) {
3757
+ const errorclass = output.details?.errorclass;
3758
+ return typeof errorclass === "string" && errorclass.trim() ? errorclass : "stepfailed";
3759
+ }
3760
+ function backoffdelay(policy, attempt, seed) {
3761
+ const base = policy.backoff.shape === "exponential" ? policy.backoff.base * 2 ** (attempt - 1) : policy.backoff.base;
3762
+ if (policy.backoff.jitter <= 0) return Math.max(0, base);
3763
+ return Math.max(0, base - policy.backoff.jitter / 2 + seededrandom(seed + attempt) * policy.backoff.jitter);
3764
+ }
3765
+ function waitsome(milliseconds) {
3766
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
3767
+ }
3768
+ async function applyretry(input) {
3769
+ const seed = input.seed ?? 0;
3770
+ let value = await input.run();
3771
+ const attempts = [];
3772
+ let attempt = 1;
3773
+ while (!value.ok && attempt < input.policy.attempts) {
3774
+ const errorclass = input.errorclass(value);
3775
+ if (!input.policy.retryable.includes(errorclass)) break;
3776
+ const delay = backoffdelay(input.policy, attempt, seed);
3777
+ attempts.push({ stepid: input.stepid, attempt: attempt + 1, delay, errorclass, at: input.now });
3778
+ if (delay > 0) await waitsome(delay);
3779
+ attempt += 1;
3780
+ value = await input.run();
3781
+ }
3782
+ return { value, attempts, exhausted: !value.ok && attempts.length > 0 && attempt >= input.policy.attempts };
3783
+ }
3784
+ async function applytimeout(input) {
3785
+ let timer;
3786
+ const guard = new Promise((resolve) => {
3787
+ timer = setTimeout(() => resolve("cancelled"), Math.max(0, input.budgetms));
3788
+ });
3789
+ const raced = await Promise.race([input.run().then((value) => ({ kind: "done", value })), guard.then((marker) => ({ kind: "cancelled", marker }))]);
3790
+ if (timer !== void 0) clearTimeout(timer);
3791
+ if (raced.kind === "done") return { aborted: false, value: raced.value };
3792
+ return { aborted: true, output: { ok: false, summary: `The ${input.stepid} step exceeded its reviewed budget of ${input.budgetms} milliseconds and was cancelled.`, details: { errorclass: "timeout", budget: input.budgetms, cancelled: true } }, abort: { stepid: input.stepid, budget: input.budgetms, scope: "step", at: Date.now() } };
3793
+ }
3794
+ async function applyruntimeout(input) {
3795
+ let timer;
3796
+ const guard = new Promise((resolve) => {
3797
+ timer = setTimeout(() => resolve("cancelled"), Math.max(0, input.budgetms));
3798
+ });
3799
+ const raced = await Promise.race([input.run().then((value) => ({ kind: "done", value })), guard.then((marker) => ({ kind: "cancelled", marker }))]);
3800
+ if (timer !== void 0) clearTimeout(timer);
3801
+ if (raced.kind === "done") return { cancelled: false, value: raced.value };
3802
+ return { cancelled: true, error: new cancellederror(`The run exceeded its reviewed budget of ${input.budgetms} milliseconds and was cancelled.`) };
3803
+ }
3804
+ async function runcatch(input) {
3805
+ const body = await runbody({ step: { id: "catch", kind: "trycatch", label: "catch handler" }, scopes: input.scopes, outputs: input.outputs, execute: input.execute, now: input.now, steps: input.handler.steps });
3806
+ return { ok: body.ok, scopes: body.scopes, log: body.log };
3807
+ }
3808
+ async function runtry(input) {
3809
+ const timeouts = [];
3810
+ const retries = [];
3811
+ const runonce = async (child, scopes) => {
3812
+ if (iscontrolflowkind(child.kind)) {
3813
+ const result = await runcontrolstep({ step: child, scopes, outputs: input.outputs, execute: input.execute, now: input.now });
3814
+ return { ok: result.output.ok, scopes: result.scopes, log: result.log, output: result.output };
3815
+ }
3816
+ const executed = await runstep({ step: child, scopes, outputs: input.outputs, execute: input.execute, now: input.now });
3817
+ return { ok: executed.output.ok, scopes: executed.scopes, log: [...executed.childlog ?? [], executed.log], output: executed.output };
3818
+ };
3819
+ const runchild = async (child, scopes) => {
3820
+ const attempt = async () => {
3821
+ if (input.fragile.retry === void 0) return await runonce(child, scopes);
3822
+ const retried = await applyretry({ stepid: child.id, policy: input.fragile.retry, run: () => runonce(child, scopes), errorclass: (value) => errorclassof(value.output), now: input.now, seed: seedof(child.id) });
3823
+ retries.push(...retried.attempts);
3824
+ return retried.value;
3825
+ };
3826
+ if (input.fragile.timeout?.stepms === void 0) return await attempt();
3827
+ const guarded = await applytimeout({ stepid: child.id, budgetms: input.fragile.timeout.stepms, run: attempt });
3828
+ if (!guarded.aborted) return guarded.value;
3829
+ if (guarded.abort) timeouts.push(guarded.abort);
3830
+ return { ok: false, scopes, log: [], output: guarded.output };
3831
+ };
3832
+ const runbodyof = async (scopes) => {
3833
+ let current = scopes;
3834
+ const log = [];
3835
+ for (const child of input.fragile.steps) {
3836
+ const executed = await runchild(child, current);
3837
+ current = executed.scopes;
3838
+ log.push(...executed.log);
3839
+ if (!executed.ok) return { ok: false, scopes: current, log, failure: executed.output };
3840
+ }
3841
+ return { ok: true, scopes: current, log };
3842
+ };
3843
+ let body;
3844
+ if (input.fragile.timeout?.runms !== void 0) {
3845
+ const guarded = await applytimeout({ stepid: input.step.id, budgetms: input.fragile.timeout.runms, run: () => runbodyof(input.scopes) });
3846
+ if (guarded.aborted) {
3847
+ if (guarded.abort) timeouts.push({ ...guarded.abort, scope: "run" });
3848
+ body = { ok: false, scopes: input.scopes, log: [], failure: guarded.output ?? { ok: false, summary: "The try block exceeded its reviewed run budget and was cancelled.", details: { errorclass: "timeout", cancelled: true } } };
3849
+ } else {
3850
+ body = guarded.value;
3851
+ }
3852
+ } else {
3853
+ body = await runbodyof(input.scopes);
3854
+ }
3855
+ if (body.ok) {
3856
+ const summary = `The try block completed its ${input.fragile.steps.length} step${input.fragile.steps.length === 1 ? "" : "s"}${retries.length > 0 ? ` after ${retries.length} retry attempt${retries.length === 1 ? "" : "s"}` : ""}.`;
3857
+ if (retries.length === 0 && timeouts.length === 0) return { ok: true, scopes: body.scopes, log: body.log, summary };
3858
+ return { ok: true, scopes: body.scopes, log: body.log, summary, decision: { runid: "", stepid: input.step.id, kind: "retry", at: input.now, ...retries.length > 0 ? { retries } : {}, ...timeouts.length > 0 ? { timeouts } : {} } };
3859
+ }
3860
+ const handler = await runcatch({ handler: input.fragile.catch, scopes: body.scopes, outputs: input.outputs, execute: input.execute, now: input.now });
3861
+ const errorclass = errorclassof(body.failure ?? { ok: false, summary: "" });
3862
+ const decision = { runid: "", stepid: input.step.id, kind: "catch", at: input.now, ...retries.length > 0 ? { retries } : {}, ...timeouts.length > 0 ? { timeouts } : {}, catch: { errorclass, message: body.failure?.summary ?? "The fragile body step failed.", rerun: input.fragile.catch.rerun === true } };
3863
+ if (!handler.ok) return { ok: false, scopes: handler.scopes, log: [...body.log, ...handler.log], summary: `The catch handler of the try block failed after the ${errorclass} failure.`, decision };
3864
+ if (input.fragile.catch.rerun === true) {
3865
+ const rerun = await runbodyof(handler.scopes);
3866
+ if (rerun.ok) return { ok: true, scopes: rerun.scopes, log: [...body.log, ...handler.log, ...rerun.log], summary: `The catch handler ran after the ${errorclass} failure and the rerun of the try body succeeded.`, decision };
3867
+ return { ok: false, scopes: rerun.scopes, log: [...body.log, ...handler.log, ...rerun.log], summary: `The catch handler ran and the rerun of the try body failed again with ${errorclassof(rerun.failure ?? { ok: false, summary: "" })}.`, decision };
3868
+ }
3869
+ return { ok: true, scopes: handler.scopes, log: [...body.log, ...handler.log], summary: `The catch handler ran ${input.fragile.catch.steps.length} step${input.fragile.catch.steps.length === 1 ? "" : "s"} after the ${errorclass} failure.`, decision };
3870
+ }
3871
+ function seedof(text2) {
3872
+ let hash = 2166136261;
3873
+ for (let index = 0; index < text2.length; index += 1) {
3874
+ hash ^= text2.charCodeAt(index);
3875
+ hash = Math.imul(hash, 16777619) >>> 0;
3876
+ }
3877
+ return hash >>> 0;
3878
+ }
3879
+ async function runcontrolstep(input) {
3880
+ const payload = controloptions(input.step);
3881
+ const base = { step: input.step, scopes: input.scopes, outputs: input.outputs, execute: input.execute, now: input.now, ...input.path !== void 0 ? { path: input.path } : {} };
3882
+ let result;
3883
+ switch (input.step.kind) {
3884
+ case "condition": {
3885
+ const condition = conditionof(payload.condition);
3886
+ if (!condition) throw new Error("The condition step needs a reviewed boolean expression in its options.");
3887
+ const value = evaluatecondition(condition, input.scopes);
3888
+ const scopes = setvariable(input.scopes, condition.expression.result, condition.expression.resultkind, value, input.now);
3889
+ const summary = `The condition ${condition.expression.result} ${value ? "holds" : "does not hold"} over the extracted values.`;
3890
+ return { scopes, log: [], output: { ok: true, summary, details: { condition: { result: condition.expression.result, value } } } };
3891
+ }
3892
+ case "branch": {
3893
+ const branch = branchof(payload.branch);
3894
+ if (!branch) throw new Error("The branch step needs reviewed unique paths with boolean match expressions and an else path in its options.");
3895
+ const chosen = choosebranch({ stepid: input.step.id, branch, scopes: input.scopes, ...input.pagestate !== void 0 ? { pagestate: input.pagestate } : {}, now: input.now });
3896
+ const body = await runbody({ ...base, steps: chosen.steps });
3897
+ result = body.ok ? { ok: true, scopes: body.scopes, log: body.log, summary: `The branch chose the path ${chosen.outcome.path}: ${chosen.outcome.reason}`, decision: { runid: input.runid ?? "", stepid: input.step.id, kind: "branch", at: input.now, branch: chosen.outcome } } : { ok: false, scopes: body.scopes, log: body.log, summary: `The branch chose the path ${chosen.outcome.path} and its body failed: ${body.failure?.summary ?? "the body step failed."}`, decision: { runid: input.runid ?? "", stepid: input.step.id, kind: "branch", at: input.now, branch: chosen.outcome } };
3898
+ break;
3899
+ }
3900
+ case "loop": {
3901
+ const loop = loopof(payload.loop);
3902
+ if (!loop) throw new Error("The loop step needs a reviewed list variable, distinct item and index variables, an optional positive safety bound and a non-empty body in its options.");
3903
+ result = await runloop({ ...base, loop });
3904
+ break;
3905
+ }
3906
+ case "repeatuntil": {
3907
+ const repeat = repeatuntilof(payload.repeatuntil);
3908
+ if (!repeat) throw new Error("The repeat until step needs a reviewed convergence expression, an optional positive safety bound and a non-empty body in its options.");
3909
+ result = await runrepeatuntil({ ...base, repeat });
3910
+ break;
3911
+ }
3912
+ case "whileloop": {
3913
+ const condition = whileof(payload.while);
3914
+ if (!condition) throw new Error("The while step needs a reviewed condition, a mandatory positive safety bound and a non-empty body in its options.");
3915
+ result = await runwhile({ ...base, condition });
3916
+ break;
3917
+ }
3918
+ case "foreach": {
3919
+ const foreach = foreachof(payload.foreach);
3920
+ if (!foreach) throw new Error("The foreach step needs a reviewed non-empty selector, distinct item and index variables and a non-empty body in its options.");
3921
+ result = await runforeach({ ...base, foreach, ...input.resolveelements !== void 0 ? { resolveelements: input.resolveelements } : {} });
3922
+ break;
3923
+ }
3924
+ case "parallel": {
3925
+ const parallel = parallelof(payload.parallel);
3926
+ if (!parallel) throw new Error("The parallel step needs uniquely identified branches with bodies and a join policy of the first, last or fail strategy with cancel or continue on branch failure in its options.");
3927
+ result = await runparallel({ ...base, parallel });
3928
+ break;
3929
+ }
3930
+ case "trycatch": {
3931
+ const fragile = tryof(payload.try);
3932
+ if (!fragile) throw new Error("The try step needs a fragile body, a catch handler and optional retry and timeout policies in its options.");
3933
+ result = await runtry({ ...base, fragile });
3934
+ break;
3935
+ }
3936
+ default:
3937
+ throw new Error(`The ${input.step.kind} step is not a control flow kind.`);
3938
+ }
3939
+ if (result.decision !== void 0 && input.runid !== void 0) result.decision.runid = input.runid;
3940
+ return { scopes: result.scopes, log: result.log, output: { ok: result.ok, summary: result.summary, ...result.decision !== void 0 ? { details: { control: result.decision } } : {} } };
3941
+ }
3942
+
3180
3943
  // workflow.ts
3181
3944
  var workflowkinds = ["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars"];
3182
3945
  function workflowstepof(value) {
@@ -3324,12 +4087,19 @@ function composeworkflow(input) {
3324
4087
  const steps = expandblocks(input.steps, blocks);
3325
4088
  for (const step of steps) {
3326
4089
  if (input.kindallowed && !input.kindallowed(step.kind)) throw new Error(`The workflow step kind ${step.kind} is not a reviewed action kind.`);
4090
+ if (iscontrolflowkind(step.kind)) {
4091
+ validatecontrolpayload(step);
4092
+ for (const child of controlsteps(step)) {
4093
+ if (input.kindallowed && !input.kindallowed(child.kind)) throw new Error(`The workflow step kind ${child.kind} inside the control payload of ${step.id} is not a reviewed action kind.`);
4094
+ }
4095
+ }
3327
4096
  if (step.bindings) for (const binding of step.bindings) {
3328
4097
  if (!steps.some((other) => other.id === binding.stepid)) throw new Error(`The binding of ${binding.variable} references the unknown step ${binding.stepid}.`);
3329
4098
  }
3330
4099
  }
3331
4100
  const riskof = input.riskof ?? (() => "sensitive");
3332
- const risk = steps.some((step) => riskof(step.kind) === "sensitive") ? "sensitive" : steps.some((step) => riskof(step.kind) === "interaction") ? "interaction" : "read";
4101
+ const gradedkinds = steps.flatMap((step) => [step.kind, ...controlsteps(step).map((child) => child.kind)]);
4102
+ const risk = gradedkinds.some((kind) => riskof(kind) === "sensitive") ? "sensitive" : gradedkinds.some((kind) => riskof(kind) === "interaction") ? "interaction" : "read";
3333
4103
  const record2 = { id: input.id ?? crypto.randomUUID(), name: input.name, version: input.version, origins: [...new Set(origins)], steps, blocks, risk, createdat: input.now };
3334
4104
  return deepfreeze(record2);
3335
4105
  }
@@ -3610,21 +4380,24 @@ async function runstep(input) {
3610
4380
  }
3611
4381
  stepvalue = interpolated.text;
3612
4382
  }
3613
- const target = input.step.target !== void 0 ? interpolate(input.step.target, scopes) : void 0;
4383
+ const controlled = iscontrolflowkind(input.step.kind);
4384
+ const target = !controlled && input.step.target !== void 0 ? interpolate(input.step.target, scopes) : void 0;
3614
4385
  if (target) consumed.push(...target.consumed);
3615
- const value = stepvalue !== void 0 ? interpolate(stepvalue, scopes) : void 0;
4386
+ const value = !controlled && stepvalue !== void 0 ? interpolate(stepvalue, scopes) : void 0;
3616
4387
  if (value) consumed.push(...value.consumed);
3617
- const options = input.step.options !== void 0 ? interpolate(input.step.options, scopes) : void 0;
4388
+ const options = !controlled && input.step.options !== void 0 ? interpolate(input.step.options, scopes) : void 0;
3618
4389
  if (options) consumed.push(...options.consumed);
3619
4390
  const dispatchable = { ...input.step, ...target !== void 0 ? { target: target.text } : {}, ...value !== void 0 ? { value: value.text } : {}, ...options !== void 0 ? { options: options.text } : {} };
3620
- const output = await input.execute(dispatchable, { scopes, ...input.block !== void 0 ? { block: input.block } : {} });
4391
+ const output = await input.execute(dispatchable, { scopes, outputs: input.outputs, ...input.block !== void 0 ? { block: input.block } : {} });
4392
+ if (output.scopes !== void 0) scopes = output.scopes;
4393
+ const childlog = output.log;
3621
4394
  if (input.step.bindings) {
3622
4395
  const bound = bindvariables(scopes, input.step.bindings, { ...input.outputs, [input.step.id]: { stepid: input.step.id, ok: output.ok, summary: output.summary, ...output.details !== void 0 ? { details: output.details } : {}, at: input.now } }, input.now);
3623
4396
  scopes = bound.scopes;
3624
4397
  produced = [.../* @__PURE__ */ new Set([...produced, ...bound.produced])];
3625
4398
  }
3626
4399
  const duration = Date.now() - startedat;
3627
- return { scopes, log: runlogof(input.step, output.ok ? "done" : "failed", startedat, duration, output.summary, { ...input.block !== void 0 ? { block: input.block } : {}, ...consumed.length > 0 ? { consumed } : {}, ...produced.length > 0 ? { produced } : {}, ...output.details !== void 0 ? { details: output.details } : {}, ...output.ok ? { checkpoint: true } : {} }), output };
4400
+ return { scopes, log: runlogof(input.step, output.ok ? "done" : "failed", startedat, duration, output.summary, { ...input.block !== void 0 ? { block: input.block } : {}, ...consumed.length > 0 ? { consumed } : {}, ...produced.length > 0 ? { produced } : {}, ...output.details !== void 0 ? { details: output.details } : {}, ...output.ok ? { checkpoint: true } : {} }), ...childlog !== void 0 ? { childlog } : {}, output };
3628
4401
  } catch (error) {
3629
4402
  const duration = Date.now() - startedat;
3630
4403
  const summary = error instanceof Error ? error.message : String(error);
@@ -3656,6 +4429,7 @@ async function runworkflow(input) {
3656
4429
  }
3657
4430
  const executed = await runstep({ step, scopes, outputs, execute: input.execute, now: Date.now(), ...step.block !== void 0 ? { block: step.block } : {} });
3658
4431
  scopes = executed.scopes;
4432
+ if (executed.childlog !== void 0) log.push(...executed.childlog);
3659
4433
  log.push(executed.log);
3660
4434
  outputs[step.id] = { stepid: step.id, ok: executed.output.ok, summary: executed.output.summary, ...executed.output.details !== void 0 ? { details: executed.output.details } : {}, at: Date.now() };
3661
4435
  if (!executed.output.ok) {
@@ -3682,6 +4456,394 @@ function dryrunworkflow(input) {
3682
4456
  return { run: { ...run, state: "done", cursor: input.record.steps.length, endedat: input.now }, scopes, log };
3683
4457
  }
3684
4458
 
4459
+ // trigger.ts
4460
+ var triggerkinds = ["visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"];
4461
+ var triggerfamilies = ["visit", "url", "menu", "key", "button", "cron", "interval", "urllist", "webhook", "event"];
4462
+ var triggereventcatalog = ["mutate", "focus", "banner", "console", "error", "navigate"];
4463
+ var defaulttriggercooldown = 1e4;
4464
+ function triggerfamilyof(kind) {
4465
+ const index = triggerkinds.indexOf(kind);
4466
+ return index >= 0 ? triggerfamilies[index] : void 0;
4467
+ }
4468
+ function triggerlabel(value) {
4469
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
4470
+ }
4471
+ function positivewindow(value) {
4472
+ if (value === void 0) return void 0;
4473
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
4474
+ }
4475
+ function jitterwindow(value) {
4476
+ if (value === void 0) return void 0;
4477
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
4478
+ }
4479
+ function httpsorigin(value) {
4480
+ if (typeof value !== "string" || !value.trim()) return void 0;
4481
+ try {
4482
+ const parsed = new URL(value.trim());
4483
+ if (parsed.protocol !== "https:") return void 0;
4484
+ return parsed.origin;
4485
+ } catch {
4486
+ return void 0;
4487
+ }
4488
+ }
4489
+ function webhookfieldof(value) {
4490
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4491
+ const candidate = value;
4492
+ if (typeof candidate.name !== "string" || !/^[a-z][a-z0-9]*$/i.test(candidate.name)) return void 0;
4493
+ if (candidate.kind !== "string" && candidate.kind !== "number" && candidate.kind !== "boolean") return void 0;
4494
+ if (candidate.required !== void 0 && typeof candidate.required !== "boolean") return void 0;
4495
+ return { name: candidate.name, kind: candidate.kind, ...candidate.required === true ? { required: true } : {} };
4496
+ }
4497
+ function triggerpayloadof(family, value) {
4498
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4499
+ const candidate = value;
4500
+ if (family === "visit") {
4501
+ if (!Array.isArray(candidate.origins) || candidate.origins.length === 0) return void 0;
4502
+ const origins = candidate.origins.map((origin) => httpsorigin(origin));
4503
+ if (origins.some((origin) => origin === void 0)) return void 0;
4504
+ return { origins: [...new Set(origins)] };
4505
+ }
4506
+ if (family === "url") {
4507
+ if (typeof candidate.pattern !== "string" || !candidate.pattern.trim()) return void 0;
4508
+ if (httpsorigin(candidate.pattern) === void 0) return void 0;
4509
+ return { pattern: candidate.pattern.trim() };
4510
+ }
4511
+ if (family === "menu") {
4512
+ const title = triggerlabel(candidate.title);
4513
+ if (!title) return void 0;
4514
+ return { title };
4515
+ }
4516
+ if (family === "key") {
4517
+ if (typeof candidate.command !== "string" || !/^[a-z][a-z0-9-]*$/.test(candidate.command)) return void 0;
4518
+ if (candidate.key !== void 0 && (typeof candidate.key !== "string" || !candidate.key.trim())) return void 0;
4519
+ return { command: candidate.command, ...candidate.key !== void 0 ? { key: candidate.key } : {} };
4520
+ }
4521
+ if (family === "button") return {};
4522
+ if (family === "cron") {
4523
+ if (typeof candidate.cron !== "string" || !candidate.cron.trim()) return void 0;
4524
+ if (cronparse(candidate.cron) === void 0) return void 0;
4525
+ if (candidate.timezone !== void 0 && (typeof candidate.timezone !== "string" || !timezonevalid(candidate.timezone))) return void 0;
4526
+ return { cron: candidate.cron.trim(), ...candidate.timezone !== void 0 ? { timezone: candidate.timezone } : {} };
4527
+ }
4528
+ if (family === "interval") {
4529
+ const period = positivewindow(candidate.period);
4530
+ if (period === void 0) return void 0;
4531
+ const jitter = jitterwindow(candidate.jitter);
4532
+ if (candidate.jitter !== void 0 && jitter === void 0) return void 0;
4533
+ return { period, ...jitter !== void 0 ? { jitter } : {} };
4534
+ }
4535
+ if (family === "urllist") {
4536
+ if (!Array.isArray(candidate.urls) || candidate.urls.length === 0) return void 0;
4537
+ const urls = candidate.urls.map((url) => httpsorigin(url) === void 0 ? void 0 : url.trim());
4538
+ if (urls.some((url) => url === void 0)) return void 0;
4539
+ return { urls };
4540
+ }
4541
+ if (family === "webhook") {
4542
+ if (typeof candidate.secret !== "string" || !webhooksecretok(candidate.secret)) return void 0;
4543
+ if (!Array.isArray(candidate.schema) || candidate.schema.length === 0) return void 0;
4544
+ const schema = candidate.schema.map((field) => webhookfieldof(field));
4545
+ if (schema.some((field) => field === void 0)) return void 0;
4546
+ const names = schema.map((field) => field.name);
4547
+ if (new Set(names).size !== names.length) return void 0;
4548
+ return { secret: candidate.secret, schema };
4549
+ }
4550
+ const events = candidate.events;
4551
+ if (!Array.isArray(events) || events.length === 0) return void 0;
4552
+ if (!events.every((name) => typeof name === "string" && triggereventcatalog.includes(name))) return void 0;
4553
+ return { events: [...new Set(events)] };
4554
+ }
4555
+ function webhooksecretok(secret) {
4556
+ if (secret.length < 24) return false;
4557
+ if (/^(.)\1+$/.test(secret)) return false;
4558
+ return /[a-z]/i.test(secret) && /\d/.test(secret);
4559
+ }
4560
+ function timezonevalid(timezone) {
4561
+ try {
4562
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
4563
+ return true;
4564
+ } catch {
4565
+ return false;
4566
+ }
4567
+ }
4568
+ function armrule(input) {
4569
+ if (typeof input.workflowid !== "string" || !input.workflowid.trim()) return void 0;
4570
+ const payload = triggerpayloadof(input.family, input.payload);
4571
+ if (!payload) return void 0;
4572
+ if (input.cooldown !== void 0 && (typeof input.cooldown !== "number" || !Number.isFinite(input.cooldown) || input.cooldown <= 0)) return void 0;
4573
+ const cooldown = input.cooldown ?? (input.family === "webhook" || input.family === "event" ? defaulttriggercooldown : 0);
4574
+ const label = input.label ?? `The ${input.family} rule of ${input.workflowid}`;
4575
+ 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 };
4576
+ }
4577
+ function updaterule(rule, patch) {
4578
+ return { ...rule, ...patch.state !== void 0 ? { state: { ...rule.state, ...patch.state } } : {}, ...patch.stats !== void 0 ? { stats: { ...rule.stats, ...patch.stats } } : {} };
4579
+ }
4580
+ function matchurl(pattern, url) {
4581
+ let parsedpattern;
4582
+ let parsedurl;
4583
+ try {
4584
+ parsedpattern = new URL(pattern);
4585
+ parsedurl = new URL(url);
4586
+ } catch {
4587
+ return false;
4588
+ }
4589
+ if (parsedpattern.protocol !== parsedurl.protocol) return false;
4590
+ if (parsedpattern.hostname !== parsedurl.hostname) return false;
4591
+ if (parsedpattern.port !== "" && parsedpattern.port !== parsedurl.port) return false;
4592
+ return globmatch(`${parsedpattern.pathname}${parsedpattern.search}`, `${parsedurl.pathname}${parsedurl.search}`);
4593
+ }
4594
+ function globmatch(pattern, text2) {
4595
+ const escaped = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4596
+ const expression = new RegExp(["^", pattern.split("**").map((segment) => segment.split("*").map(escaped).join("[^/]*")).join(".*"), "$"].join(""));
4597
+ return expression.test(text2);
4598
+ }
4599
+ function visitmatch(origins, url) {
4600
+ let origin = "";
4601
+ try {
4602
+ origin = new URL(url).origin;
4603
+ } catch {
4604
+ return false;
4605
+ }
4606
+ return origins.includes(origin);
4607
+ }
4608
+ function cronparse(expression) {
4609
+ const fields = expression.trim().split(/\s+/);
4610
+ if (fields.length !== 5) return void 0;
4611
+ const minutes = cronfield(fields[0] ?? "", 0, 59);
4612
+ const hours = cronfield(fields[1] ?? "", 0, 23);
4613
+ const daysofmonth = cronfield(fields[2] ?? "", 1, 31);
4614
+ const months = cronfield(fields[3] ?? "", 1, 12, monthnames);
4615
+ const daysofweek = cronfield(fields[4] ?? "", 0, 7, weekdaynames, true);
4616
+ if (!minutes || !hours || !daysofmonth || !months || !daysofweek) return void 0;
4617
+ return { minutes, hours, daysofmonth, months, daysofweek: [...new Set(daysofweek.map((day) => day % 7))].sort((left, right) => left - right) };
4618
+ }
4619
+ var weekdaynames = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
4620
+ 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 };
4621
+ function cronfield(field, min, max, names, sundayseven = false) {
4622
+ const values = /* @__PURE__ */ new Set();
4623
+ for (const part of field.split(",")) {
4624
+ if (!part) return void 0;
4625
+ const [range, stepstring] = part.split("/");
4626
+ const step = stepstring === void 0 ? 1 : Number(stepstring);
4627
+ if (!Number.isInteger(step) || step < 1) return void 0;
4628
+ let low = min;
4629
+ let high = max;
4630
+ if (range !== void 0 && range !== "*") {
4631
+ const bounds = range.split("-");
4632
+ if (bounds.length > 2) return void 0;
4633
+ const lowvalue = cronvalue(bounds[0] ?? "", min, max, names);
4634
+ if (lowvalue === void 0) return void 0;
4635
+ low = lowvalue;
4636
+ high = lowvalue;
4637
+ if (bounds.length === 2) {
4638
+ const highvalue = cronvalue(bounds[1] ?? "", min, max, names);
4639
+ if (highvalue === void 0 || highvalue < lowvalue) return void 0;
4640
+ high = highvalue;
4641
+ }
4642
+ }
4643
+ for (let value = low; value <= high; value += step) values.add(value);
4644
+ }
4645
+ const list = [...values];
4646
+ if (list.some((value) => value < min || value > max)) return void 0;
4647
+ if (sundayseven && values.has(7)) {
4648
+ values.delete(7);
4649
+ values.add(0);
4650
+ }
4651
+ return [...values].sort((left, right) => left - right);
4652
+ }
4653
+ function cronvalue(value, min, max, names) {
4654
+ const candidate = names?.[value.toLowerCase()];
4655
+ if (candidate !== void 0) return candidate;
4656
+ if (!/^\d+$/.test(value)) return void 0;
4657
+ const parsed = Number(value);
4658
+ if (parsed < min || parsed > max) return void 0;
4659
+ return parsed;
4660
+ }
4661
+ function calendarparts(at, timezone) {
4662
+ if (timezone === void 0) {
4663
+ const date = new Date(at);
4664
+ return { minute: date.getUTCMinutes(), hour: date.getUTCHours(), day: date.getUTCDate(), month: date.getUTCMonth() + 1, weekday: date.getUTCDay() };
4665
+ }
4666
+ 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));
4667
+ const pick = (type) => parts.find((part) => part.type === type)?.value ?? "";
4668
+ const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(pick("weekday"));
4669
+ const month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].indexOf(pick("month")) + 1;
4670
+ return { minute: Number(pick("minute")), hour: Number(pick("hour")), day: Number(pick("day")), month, weekday };
4671
+ }
4672
+ function crondaymatch(schedule, parts) {
4673
+ if (!schedule.months.includes(parts.month)) return false;
4674
+ const domfull = schedule.daysofmonth.length === 31;
4675
+ const dowfull = schedule.daysofweek.length === 7;
4676
+ const dommatch = schedule.daysofmonth.includes(parts.day);
4677
+ const dowmatch = schedule.daysofweek.includes(parts.weekday);
4678
+ if (!domfull && !dowfull) return dommatch || dowmatch;
4679
+ if (!domfull) return dommatch;
4680
+ if (!dowfull) return dowmatch;
4681
+ return true;
4682
+ }
4683
+ function cronnext(expression, from, timezone) {
4684
+ const schedule = cronparse(expression);
4685
+ if (!schedule) return void 0;
4686
+ const minute = 6e4;
4687
+ const hour = 60 * minute;
4688
+ const day = 24 * hour;
4689
+ let candidate = Math.floor(from / minute) * minute + minute;
4690
+ const horizon = from + 4 * 366 * day;
4691
+ while (candidate <= horizon) {
4692
+ const parts = calendarparts(candidate, timezone);
4693
+ if (!crondaymatch(schedule, parts)) {
4694
+ candidate += day - parts.hour * hour - parts.minute * minute;
4695
+ continue;
4696
+ }
4697
+ if (!schedule.hours.includes(parts.hour)) {
4698
+ const later = schedule.hours.find((value) => value > parts.hour);
4699
+ candidate += later === void 0 ? day - parts.hour * hour - parts.minute * minute : (later - parts.hour) * hour - parts.minute * minute;
4700
+ continue;
4701
+ }
4702
+ if (!schedule.minutes.includes(parts.minute)) {
4703
+ const later = schedule.minutes.find((value) => value > parts.minute);
4704
+ candidate += later === void 0 ? (60 - parts.minute) * minute : (later - parts.minute) * minute;
4705
+ continue;
4706
+ }
4707
+ return candidate;
4708
+ }
4709
+ return void 0;
4710
+ }
4711
+ function schedulecron(rule, from) {
4712
+ return cronnext(rule.cron, from, rule.timezone);
4713
+ }
4714
+ function scheduleinterval(rule, lastfire, armedat, seed) {
4715
+ const base = (lastfire ?? armedat) + rule.period;
4716
+ const jitter = rule.jitter ?? 0;
4717
+ if (jitter <= 0) return base;
4718
+ return Math.max(0, Math.round(base - jitter / 2 + seededrandom(seed) * jitter));
4719
+ }
4720
+ function listdue(rules, now) {
4721
+ return rules.flatMap((rule) => {
4722
+ if (rule.state.nextfireat === void 0 || rule.state.nextfireat > now) return [];
4723
+ if (!rule.state.enabled || rule.state.pausedat !== void 0) return [];
4724
+ return [{ rule, overdueby: now - rule.state.nextfireat }];
4725
+ });
4726
+ }
4727
+ function applycooldown(rule, now) {
4728
+ const lastfireat = rule.state.lastfireat;
4729
+ if (lastfireat === void 0 || rule.state.cooldown <= 0) return { suppressed: false, remaining: 0 };
4730
+ const remaining = lastfireat + rule.state.cooldown - now;
4731
+ return { suppressed: remaining > 0, remaining: Math.max(0, remaining) };
4732
+ }
4733
+ function evaluatetrigger(input) {
4734
+ const rule = input.rule;
4735
+ if (!rule.state.enabled) return { fired: false, suppressed: "disabled" };
4736
+ if (rule.state.pausedat !== void 0) return { fired: false, suppressed: "paused" };
4737
+ if (!input.workflowreviewed) return { fired: false, suppressed: "unreviewed" };
4738
+ if (input.runactive) return { fired: false, suppressed: "dedupe" };
4739
+ const cooldown = applycooldown(rule, input.now);
4740
+ if (cooldown.suppressed) return { fired: false, suppressed: "cooldown", remaining: cooldown.remaining };
4741
+ 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 } : {} };
4742
+ return { fired: true, fire };
4743
+ }
4744
+ function queuefire(queue, fire) {
4745
+ if (queue.some((pending) => pending.ruleid === fire.ruleid)) return { queue, queued: false, deduped: true };
4746
+ return { queue: [...queue, fire], queued: true, deduped: false };
4747
+ }
4748
+ async function drainqueue(queue, launch) {
4749
+ let remaining = [...queue];
4750
+ let launched = 0;
4751
+ while (remaining.length > 0) {
4752
+ const fire = remaining[0];
4753
+ try {
4754
+ await launch(fire);
4755
+ } catch {
4756
+ return { launched, remaining };
4757
+ }
4758
+ remaining = remaining.slice(1);
4759
+ launched += 1;
4760
+ }
4761
+ return { launched, remaining };
4762
+ }
4763
+ function verifywebhook(input) {
4764
+ if (typeof input.rule.secret !== "string" || !input.rule.secret) return { verified: false, reason: "The webhook rule carries no reviewed secret." };
4765
+ if (!secrectsmatch(input.secret, input.rule.secret)) return { verified: false, reason: "The webhook secret does not match the reviewed secret of the rule." };
4766
+ const payload = input.payload;
4767
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { verified: false, reason: "The webhook payload must be a JSON object." };
4768
+ const candidate = payload;
4769
+ for (const field of input.rule.schema ?? []) {
4770
+ const value = candidate[field.name];
4771
+ if (value === void 0) {
4772
+ if (field.required === true) return { verified: false, reason: `The required webhook field ${field.name} is missing.` };
4773
+ continue;
4774
+ }
4775
+ if (typeof value !== field.kind) return { verified: false, reason: `The webhook field ${field.name} is not a ${field.kind}.` };
4776
+ }
4777
+ return { verified: true };
4778
+ }
4779
+ function secrectsmatch(left, right) {
4780
+ if (left.length !== right.length) return false;
4781
+ let same = true;
4782
+ for (let index = 0; index < left.length; index += 1) if (left.charCodeAt(index) !== right.charCodeAt(index)) same = false;
4783
+ return same;
4784
+ }
4785
+ function eventrulematches(rule, event) {
4786
+ return (rule.events ?? []).includes(event);
4787
+ }
4788
+ function observeevents(rules, event) {
4789
+ if (!triggereventcatalog.includes(event)) return [];
4790
+ return rules.filter((rule) => rule.kind === "event" && rule.state.enabled && rule.state.pausedat === void 0 && eventrulematches(rule, event));
4791
+ }
4792
+ function pauseall(rules, now) {
4793
+ return rules.map((rule) => rule.state.enabled && rule.state.pausedat === void 0 ? updaterule(rule, { state: { pausedat: now } }) : rule);
4794
+ }
4795
+ function resumeall(rules) {
4796
+ return rules.map((rule) => {
4797
+ if (rule.state.pausedat === void 0) return rule;
4798
+ const state = { ...rule.state };
4799
+ delete state.pausedat;
4800
+ return { ...rule, state };
4801
+ });
4802
+ }
4803
+ function manualpreview(record2, now) {
4804
+ 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 };
4805
+ }
4806
+ function confirmmanualrun(preview, confirmed, now) {
4807
+ return { ...preview, confirmed, decidedat: now };
4808
+ }
4809
+ function triggersummary(rule) {
4810
+ return {
4811
+ kind: rule.kind,
4812
+ workflowid: rule.workflowid,
4813
+ label: rule.label,
4814
+ ...rule.origins !== void 0 ? { origins: rule.origins } : {},
4815
+ ...rule.pattern !== void 0 ? { pattern: rule.pattern } : {},
4816
+ ...rule.title !== void 0 ? { title: rule.title } : {},
4817
+ ...rule.command !== void 0 ? { command: rule.command } : {},
4818
+ ...rule.key !== void 0 ? { key: rule.key } : {},
4819
+ ...rule.cron !== void 0 ? { cron: rule.cron, ...rule.timezone !== void 0 ? { timezone: rule.timezone } : {} } : {},
4820
+ ...rule.period !== void 0 ? { period: rule.period, ...rule.jitter !== void 0 ? { jitter: rule.jitter } : {} } : {},
4821
+ ...rule.urls !== void 0 ? { urls: rule.urls } : {},
4822
+ ...rule.events !== void 0 ? { events: rule.events } : {},
4823
+ ...rule.schema !== void 0 ? { fields: rule.schema.length } : {},
4824
+ cooldown: rule.state.cooldown,
4825
+ enabled: rule.state.enabled,
4826
+ ...rule.state.nextfireat !== void 0 ? { nextfireat: rule.state.nextfireat } : {}
4827
+ };
4828
+ }
4829
+ function ruleorigins(rule) {
4830
+ const origins = /* @__PURE__ */ new Set();
4831
+ for (const origin of rule.origins ?? []) origins.add(origin);
4832
+ if (rule.pattern !== void 0) {
4833
+ const origin = httpsorigin(rule.pattern);
4834
+ if (origin !== void 0) origins.add(origin);
4835
+ }
4836
+ for (const url of rule.urls ?? []) {
4837
+ const origin = httpsorigin(url);
4838
+ if (origin !== void 0) origins.add(origin);
4839
+ }
4840
+ return [...origins];
4841
+ }
4842
+ function ruleoriginsgranted(rule, workfloworigins) {
4843
+ const granted = new Set(workfloworigins);
4844
+ return ruleorigins(rule).every((origin) => granted.has(origin));
4845
+ }
4846
+
3685
4847
  // netauth.ts
3686
4848
  function oauthflowof(value) {
3687
4849
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -3915,9 +5077,9 @@ function consolediff(input) {
3915
5077
  }
3916
5078
 
3917
5079
  // policy.ts
3918
- 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"]);
3919
- 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"]);
3920
- 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"]);
5080
+ 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"]);
5081
+ 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"]);
5082
+ 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"]);
3921
5083
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
3922
5084
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
3923
5085
  var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement", "captureframe", "shotcanvas"]);
@@ -3938,7 +5100,8 @@ var cdpactions = /* @__PURE__ */ new Set(["attachcdp", "detachcdp", "cdpcmd", "w
3938
5100
  var profileractions = /* @__PURE__ */ new Set(["measureflow", "heapshot", "trackmemory", "profilecpu", "watchshifts", "traceload", "annotatetrace", "replaytrace", "capturesourcemaps"]);
3939
5101
  var emulationactions = /* @__PURE__ */ new Set(["emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "blackboxscripts"]);
3940
5102
  var sessionactions = /* @__PURE__ */ new Set(["persiststate", "capturesession", "restoresession", "namedsessions", "diffsessions", "searchsessions", "exportsessions", "importsessions"]);
3941
- var workflowactions = /* @__PURE__ */ new Set(["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars"]);
5103
+ var workflowactions = /* @__PURE__ */ new Set(["composeworkflow", "savetemplate", "runworkflow", "dryrun", "delay", "waitelement", "compute", "extractvars", "condition", "branch", "loop", "repeatuntil", "whileloop", "foreach", "parallel", "trycatch"]);
5104
+ var triggeractions = /* @__PURE__ */ new Set(["visitrule", "urlrule", "menurule", "keyrule", "buttonrule", "cronrule", "intervalrule", "urllistrule", "webhookrule", "eventrule"]);
3942
5105
  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"]);
3943
5106
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
3944
5107
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
@@ -3960,6 +5123,9 @@ function issessionkind(kind) {
3960
5123
  function isworkflowkind(kind) {
3961
5124
  return workflowactions.has(kind);
3962
5125
  }
5126
+ function istriggeraction(kind) {
5127
+ return triggeractions.has(kind);
5128
+ }
3963
5129
  function isdebugkind(kind) {
3964
5130
  return debugactions.has(kind);
3965
5131
  }
@@ -5583,6 +6749,68 @@ function validateworkflowgrammar(step, options) {
5583
6749
  if (typeof options.text !== "string") return { allowed: false, reason: "The variable extraction needs the reviewed text the regex rule applies to." };
5584
6750
  return { allowed: true };
5585
6751
  }
6752
+ if (kind === "condition") {
6753
+ const condition = conditionof(options.condition);
6754
+ if (!condition) return { allowed: false, reason: "The condition step needs a reviewed boolean expression in its options." };
6755
+ const operatorcheck = validatexpressionoperators(condition.expression);
6756
+ if (!operatorcheck.allowed) return operatorcheck;
6757
+ return { allowed: true };
6758
+ }
6759
+ if (kind === "branch") {
6760
+ const branch = branchof(options.branch);
6761
+ if (!branch) return { allowed: false, reason: "The branch step needs reviewed unique paths with boolean match expressions and an else path in its options so every branch terminates." };
6762
+ for (const path of [...branch.paths, branch.else]) {
6763
+ if (path.when === void 0) continue;
6764
+ const operatorcheck = validatexpressionoperators(path.when);
6765
+ if (!operatorcheck.allowed) return operatorcheck;
6766
+ }
6767
+ return controlchildkinds(step);
6768
+ }
6769
+ if (kind === "loop") {
6770
+ const loop = loopof(options.loop);
6771
+ if (!loop) return { allowed: false, reason: "The loop step needs a reviewed list variable, distinct item and index variables, an optional positive safety bound and a non-empty body in its options; an absent bound keeps the documented default." };
6772
+ return controlchildkinds(step);
6773
+ }
6774
+ if (kind === "repeatuntil") {
6775
+ const repeat = repeatuntilof(options.repeatuntil);
6776
+ if (!repeat) return { allowed: false, reason: "The repeat until step needs a reviewed convergence expression, an optional positive safety bound and a non-empty body in its options." };
6777
+ const operatorcheck = validatexpressionoperators(repeat.until);
6778
+ if (!operatorcheck.allowed) return operatorcheck;
6779
+ return controlchildkinds(step);
6780
+ }
6781
+ if (kind === "whileloop") {
6782
+ const condition = whileof(options.while);
6783
+ if (!condition) return { allowed: false, reason: "The while step needs a reviewed condition, a mandatory positive safety bound and a non-empty body in its options; a while loop without a safety bound is refused." };
6784
+ const operatorcheck = validatexpressionoperators(condition.while);
6785
+ if (!operatorcheck.allowed) return operatorcheck;
6786
+ return controlchildkinds(step);
6787
+ }
6788
+ if (kind === "foreach") {
6789
+ const foreach = foreachof(options.foreach);
6790
+ if (!foreach) return { allowed: false, reason: "The foreach step needs a reviewed non-empty selector, distinct item and index variables and a non-empty body in its options." };
6791
+ return controlchildkinds(step);
6792
+ }
6793
+ if (kind === "parallel") {
6794
+ const parallel = parallelof(options.parallel);
6795
+ if (!parallel) return { allowed: false, reason: "The parallel step needs uniquely identified branches with bodies and a join policy of the first, last or fail strategy with cancel or continue on branch failure in its options." };
6796
+ return controlchildkinds(step);
6797
+ }
6798
+ if (kind === "trycatch") {
6799
+ const fragile = tryof(options.try);
6800
+ if (!fragile) return { allowed: false, reason: "The try step needs a fragile body, a catch handler and optional retry and timeout policies in its options: attempts stay user configured with no code ceiling, backoff is fixed or exponential and budgets are positive." };
6801
+ return controlchildkinds(step);
6802
+ }
6803
+ return { allowed: true };
6804
+ }
6805
+ function controlchildkinds(step) {
6806
+ const children = controlsteps({ id: step.id, kind: step.kind, label: step.summary, ...step.options !== void 0 ? { options: step.options } : {} });
6807
+ for (const child of children) {
6808
+ try {
6809
+ actionrisk(child.kind);
6810
+ } catch {
6811
+ return { allowed: false, reason: `The ${child.kind} step inside the control payload of the ${step.kind} step is not a reviewed action kind.` };
6812
+ }
6813
+ }
5586
6814
  return { allowed: true };
5587
6815
  }
5588
6816
  function validateregexrule(pattern) {
@@ -5643,7 +6871,94 @@ function workflowgate(input) {
5643
6871
  }
5644
6872
  return { allowed: true };
5645
6873
  }
6874
+ function validatetriggergrammar(step, options) {
6875
+ const family = triggerfamilyof(step.kind);
6876
+ if (family === void 0) return { allowed: false, reason: "The trigger step is not a reviewed trigger kind." };
6877
+ 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." };
6878
+ 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." };
6879
+ 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." };
6880
+ 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." };
6881
+ const payload = options.rule;
6882
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { allowed: false, reason: `The ${step.kind} step needs its reviewed rule payload in options.` };
6883
+ if (triggerpayloadof(family, payload) === void 0) {
6884
+ if (family === "visit") return { allowed: false, reason: "The visit rule needs a non-empty reviewed list of HTTPS origins it fires on." };
6885
+ 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." };
6886
+ if (family === "menu") return { allowed: false, reason: "The menu rule needs a reviewed non-empty context menu entry title." };
6887
+ if (family === "key") return { allowed: false, reason: "The keyboard shortcut rule needs a reviewed lowercase command name and an optional suggested key binding." };
6888
+ 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." };
6889
+ 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." };
6890
+ if (family === "urllist") return { allowed: false, reason: "The url list rule needs a reviewed non-empty list of HTTPS urls its workflow runs across." };
6891
+ 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.` };
6892
+ 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(", ")}.` };
6893
+ return { allowed: false, reason: "The trigger rule payload does not follow its family grammar." };
6894
+ }
6895
+ if (family === "cron") {
6896
+ const candidate = payload;
6897
+ 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." };
6898
+ }
6899
+ if (family === "webhook") {
6900
+ const candidate = payload;
6901
+ 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." };
6902
+ }
6903
+ 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 });
6904
+ if (armed === void 0) return { allowed: false, reason: "The trigger rule payload does not arm as a reviewed rule." };
6905
+ return { allowed: true };
6906
+ }
6907
+ function triggergate(input) {
6908
+ const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now: input.now, action: "arm the trigger rule" });
6909
+ if (!gate.allowed) return gate;
6910
+ if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Trigger rules need the approved plan review before they arm." };
6911
+ let triggeroptions = {};
6912
+ try {
6913
+ triggeroptions = parseoptions(input.step);
6914
+ } catch {
6915
+ triggeroptions = {};
6916
+ }
6917
+ 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." };
6918
+ return { allowed: true };
6919
+ }
6920
+ function triggerorigins(step) {
6921
+ let triggeroptions = {};
6922
+ try {
6923
+ triggeroptions = parseoptions(step);
6924
+ } catch {
6925
+ return [];
6926
+ }
6927
+ const family = triggerfamilyof(step.kind);
6928
+ if (family === void 0) return [];
6929
+ const armed = armrule({ family, workflowid: typeof triggeroptions.workflowid === "string" ? triggeroptions.workflowid : "", payload: triggeroptions.rule, ...typeof triggeroptions.cooldown === "number" ? { cooldown: triggeroptions.cooldown } : {}, now: 0 });
6930
+ if (armed === void 0) return [];
6931
+ const origins = [];
6932
+ for (const origin of armed.origins ?? []) origins.push(origin);
6933
+ if (armed.pattern !== void 0) {
6934
+ try {
6935
+ origins.push(new URL(armed.pattern).origin);
6936
+ } catch {
6937
+ }
6938
+ }
6939
+ for (const url of armed.urls ?? []) {
6940
+ try {
6941
+ origins.push(new URL(url).origin);
6942
+ } catch {
6943
+ }
6944
+ }
6945
+ return [...new Set(origins)];
6946
+ }
5646
6947
  function dryrunprojection(step) {
6948
+ if (iscontrolflowkind(step.kind)) {
6949
+ for (const child of controlsteps(step)) {
6950
+ const childrisk = resolvedrisk({ id: child.id, kind: child.kind, summary: child.label, risk: "read", ...child.target !== void 0 ? { target: child.target } : {}, ...child.value !== void 0 ? { value: child.value } : {}, ...child.options !== void 0 ? { options: child.options } : {} });
6951
+ if (childrisk !== "read") return void 0;
6952
+ }
6953
+ if (step.kind === "condition") return "The condition step would evaluate its reviewed expression over the extracted values with no page side effect.";
6954
+ if (step.kind === "branch") return "The branch step would choose one reviewed path by page state and only the chosen path would run.";
6955
+ if (step.kind === "loop") return "The loop step would iterate its reviewed list binding the item and index variables per iteration inside the safety bound.";
6956
+ if (step.kind === "repeatuntil") return "The repeat until step would rerun its body until the convergence expression holds inside the safety bound.";
6957
+ if (step.kind === "whileloop") return "The while step would loop while its condition holds inside the reviewed safety bound.";
6958
+ if (step.kind === "foreach") return "The foreach step would iterate the elements of its reviewed selector binding the item and index variables per iteration.";
6959
+ if (step.kind === "parallel") return "The parallel step would run its branches concurrently and join their outcomes under the reviewed strategy.";
6960
+ return "The try step would run its fragile body and only the catch handler on failure.";
6961
+ }
5647
6962
  const risk = resolvedrisk({ id: step.id, kind: step.kind, summary: step.label, risk: "read", ...step.target !== void 0 ? { target: step.target } : {}, ...step.value !== void 0 ? { value: step.value } : {}, ...step.options !== void 0 ? { options: step.options } : {} });
5648
6963
  if (risk !== "read") return void 0;
5649
6964
  if (step.kind === "delay") return `The delay step would sleep its reviewed base inside the jitter window.`;
@@ -6225,6 +7540,10 @@ function validatestep(step, origin) {
6225
7540
  const workflowcheck = validateworkflowgrammar(step, options);
6226
7541
  if (!workflowcheck.allowed) return workflowcheck;
6227
7542
  }
7543
+ if (istriggeraction(step.kind)) {
7544
+ const triggercheck = validatetriggergrammar(step, options);
7545
+ if (!triggercheck.allowed) return triggercheck;
7546
+ }
6228
7547
  if (step.kind === "tabcreate") {
6229
7548
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
6230
7549
  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." };
@@ -6419,33 +7738,37 @@ function canexecute(input) {
6419
7738
  const workflowgatecheck = workflowgate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
6420
7739
  if (!workflowgatecheck.allowed) return workflowgatecheck;
6421
7740
  }
7741
+ if (istriggeraction(input.step.kind)) {
7742
+ const triggergatecheck = triggergate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
7743
+ if (!triggergatecheck.allowed) return triggergatecheck;
7744
+ }
6422
7745
  if (iscontrolkind(input.step.kind)) {
6423
7746
  const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
6424
7747
  if (!controlgate.allowed) return controlgate;
6425
- let controloptions = {};
7748
+ let controloptions2 = {};
6426
7749
  try {
6427
- controloptions = parseoptions(input.step);
7750
+ controloptions2 = parseoptions(input.step);
6428
7751
  } catch {
6429
- controloptions = {};
7752
+ controloptions2 = {};
6430
7753
  }
6431
7754
  if (input.step.kind === "blockrequest") {
6432
7755
  const blockgatecheck = blockgate(input.session, input.step, now);
6433
7756
  if (!blockgatecheck.allowed) return blockgatecheck;
6434
- const rule = blockruleof(controloptions.block);
7757
+ const rule = blockruleof(controloptions2.block);
6435
7758
  if (rule) {
6436
7759
  const blockorigin = origincheck(input.session, rule.urlpattern);
6437
7760
  if (!blockorigin.allowed) return blockorigin;
6438
7761
  }
6439
7762
  }
6440
7763
  if (input.step.kind === "mockresponse" || input.step.kind === "rewriteheaders") {
6441
- const patterns = input.step.kind === "mockresponse" ? [mockspecof(controloptions.mock)?.urlpattern ?? ""] : Array.isArray(controloptions.rules) ? controloptions.rules.map((item) => item && typeof item === "object" && !Array.isArray(item) ? String(item.urlpattern ?? "") : "") : [];
7764
+ const patterns = input.step.kind === "mockresponse" ? [mockspecof(controloptions2.mock)?.urlpattern ?? ""] : Array.isArray(controloptions2.rules) ? controloptions2.rules.map((item) => item && typeof item === "object" && !Array.isArray(item) ? String(item.urlpattern ?? "") : "") : [];
6442
7765
  for (const pattern of patterns) {
6443
7766
  const patterngate = origincheck(input.session, pattern);
6444
7767
  if (!patterngate.allowed) return patterngate;
6445
7768
  }
6446
7769
  }
6447
7770
  if (input.step.kind === "setcookies" || input.step.kind === "readcookies" || input.step.kind === "clearcookies") {
6448
- const domain = typeof controloptions.domain === "string" && controloptions.domain.trim() ? controloptions.domain : Array.isArray(controloptions.cookies) ? String(controloptions.cookies[0]?.domain ?? "") : "";
7771
+ const domain = typeof controloptions2.domain === "string" && controloptions2.domain.trim() ? controloptions2.domain : Array.isArray(controloptions2.cookies) ? String(controloptions2.cookies[0]?.domain ?? "") : "";
6449
7772
  if (!domain) return { allowed: false, reason: "A reviewed cookie domain is required before cookie control runs." };
6450
7773
  const cookiegatecheck = cookiegate(input.session, domain, now);
6451
7774
  if (!cookiegatecheck.allowed) return cookiegatecheck;
@@ -6677,13 +8000,19 @@ function recordsession(progress, planid, stepid, entry, now) {
6677
8000
  }
6678
8001
  function recordworkflow(progress, planid, stepid, entry, now) {
6679
8002
  const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
6680
- const counts = `${entry.executed !== void 0 ? `${entry.executed} executed step${entry.executed === 1 ? "" : "s"}, ` : ""}${entry.refused !== void 0 ? `${entry.refused} refused step${entry.refused === 1 ? "" : "s"}, ` : ""}${entry.total !== void 0 ? `${entry.total} total step${entry.total === 1 ? "" : "s"}, ` : ""}`.replace(/, $/, "");
8003
+ const counts = `${entry.executed !== void 0 ? `${entry.executed} executed step${entry.executed === 1 ? "" : "s"}, ` : ""}${entry.refused !== void 0 ? `${entry.refused} refused step${entry.refused === 1 ? "" : "s"}, ` : ""}${entry.total !== void 0 ? `${entry.total} total step${entry.total === 1 ? "" : "s"}, ` : ""}${entry.iterations !== void 0 ? `${entry.iterations} iteration${entry.iterations === 1 ? "" : "s"}, ` : ""}${entry.denominator !== void 0 ? `denominator ${entry.denominator}, ` : ""}`.replace(/, $/, "");
6681
8004
  const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { workflow: entry }, at: now };
6682
8005
  return recordoutcome(base, planid, outcome, now);
6683
8006
  }
8007
+ function recordtrigger(progress, planid, stepid, entry, now) {
8008
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
8009
+ 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(/, $/, "");
8010
+ const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { trigger: entry }, at: now };
8011
+ return recordoutcome(base, planid, outcome, now);
8012
+ }
6684
8013
 
6685
8014
  // version.ts
6686
- var packageversion = "1.1.50";
8015
+ var packageversion = "1.1.52";
6687
8016
 
6688
8017
  // types.ts
6689
8018
  var protocolversion = packageversion;
@@ -6922,6 +8251,25 @@ function parseproposal(value, origin, grants) {
6922
8251
  }
6923
8252
  if (step.kind === "runworkflow" && workflowoptions.reviewed !== true) throw new Error("Workflow runs without the explicit run review of the expanded step list are refused.");
6924
8253
  }
8254
+ if (istriggeraction(step.kind)) {
8255
+ let triggeroptions = {};
8256
+ try {
8257
+ triggeroptions = parseoptions(step);
8258
+ } catch {
8259
+ triggeroptions = {};
8260
+ }
8261
+ if (triggeroptions.reviewed !== true) throw new Error("Trigger rules without the explicit arm review of their match fields and bound workflow are refused.");
8262
+ for (const ruleorigin of triggerorigins(step)) {
8263
+ const granted = covered.some((pattern) => {
8264
+ try {
8265
+ return new URL(ruleorigin).origin === new URL(pattern).origin;
8266
+ } catch {
8267
+ return false;
8268
+ }
8269
+ });
8270
+ if (!granted) throw new Error(`The trigger on ${ruleorigin} stays outside the grants.`);
8271
+ }
8272
+ }
6925
8273
  const evaluation = validatestep(step, origin);
6926
8274
  if (!evaluation.allowed) throw new Error(evaluation.reason);
6927
8275
  const target = outboundtarget(step);
@@ -6983,7 +8331,7 @@ function parseproposal(value, origin, grants) {
6983
8331
  }
6984
8332
  function workflowoutcome(input) {
6985
8333
  const selected = input.stepid !== void 0 ? input.entries.filter((entry) => entry.stepid === input.stepid) : input.entries;
6986
- const steps = selected.map((entry) => ({ stepid: entry.stepid, label: entry.label, state: entry.state, duration: entry.duration, summary: entry.summary, ...entry.block !== void 0 ? { block: entry.block } : {}, ...entry.produced !== void 0 ? { produced: entry.produced } : {}, ...entry.consumed !== void 0 ? { consumed: entry.consumed } : {}, ...entry.checkpoint === true ? { checkpoint: true } : {} }));
8334
+ const steps = selected.map((entry) => ({ stepid: entry.stepid, label: entry.label, state: entry.state, duration: entry.duration, summary: entry.summary, ...entry.block !== void 0 ? { block: entry.block } : {}, ...entry.produced !== void 0 ? { produced: entry.produced } : {}, ...entry.consumed !== void 0 ? { consumed: entry.consumed } : {}, ...entry.checkpoint === true ? { checkpoint: true } : {}, ...entry.details !== void 0 && entry.details.control !== void 0 ? { control: entry.details.control } : {} }));
6987
8335
  return { version: protocolversion, runid: input.run.id, workflowid: input.run.workflowid, state: input.run.state, ...input.run.dryrun === true ? { dryrun: true } : {}, steps };
6988
8336
  }
6989
8337
  function stepof(kind, candidate, index) {
@@ -7001,7 +8349,7 @@ function requestbody(input) {
7001
8349
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
7002
8350
  }
7003
8351
  function outcomeresponse(input) {
7004
- 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 } } : {} });
8352
+ 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 } : {} } } : {} });
7005
8353
  }
7006
8354
  function mapresponse(input) {
7007
8355
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -7136,7 +8484,46 @@ function sessionreport(input) {
7136
8484
  return { version: protocolversion, records: input.records, events: input.events, folders: input.folders, diffs: input.diffs, ...input.auto !== void 0 ? { auto: input.auto } : {}, ...input.crashed === true ? { crashed: true } : {} };
7137
8485
  }
7138
8486
  function workflowreport(input) {
7139
- return { version: protocolversion, workflows: input.workflows, runs: input.runs, templates: input.templates, log: input.log ?? [], scopes: input.scopes ?? [], provenance: input.provenance ?? [] };
8487
+ return { version: protocolversion, workflows: input.workflows, runs: input.runs, templates: input.templates, log: input.log ?? [], scopes: input.scopes ?? [], provenance: input.provenance ?? [], control: input.control ?? [] };
8488
+ }
8489
+ function triggerlist(input) {
8490
+ const names = new Map(input.workflows.map((record2) => [record2.id, record2.name]));
8491
+ const rules = input.rules.map((rule) => {
8492
+ const workflowname = names.get(rule.workflowid);
8493
+ return {
8494
+ id: rule.id,
8495
+ kind: rule.kind,
8496
+ workflowid: rule.workflowid,
8497
+ ...workflowname !== void 0 ? { workflowname } : {},
8498
+ label: rule.label,
8499
+ enabled: rule.state.enabled,
8500
+ ...rule.state.pausedat !== void 0 ? { paused: true } : {},
8501
+ cooldown: rule.state.cooldown,
8502
+ ...rule.state.lastfireat !== void 0 ? { lastfireat: rule.state.lastfireat } : {},
8503
+ ...rule.state.nextfireat !== void 0 ? { nextfireat: rule.state.nextfireat } : {},
8504
+ fires: rule.stats.fires,
8505
+ launches: rule.stats.launches,
8506
+ suppressions: rule.stats.suppressions,
8507
+ summary: triggersummaryof(rule)
8508
+ };
8509
+ });
8510
+ return { version: protocolversion, rules, queued: (input.queue ?? []).length };
8511
+ }
8512
+ function triggersummaryof(rule) {
8513
+ const summary = { kind: rule.kind, workflowid: rule.workflowid };
8514
+ if (rule.origins !== void 0) summary.origins = rule.origins;
8515
+ if (rule.pattern !== void 0) summary.pattern = rule.pattern;
8516
+ if (rule.title !== void 0) summary.title = rule.title;
8517
+ if (rule.command !== void 0) summary.command = rule.command;
8518
+ if (rule.key !== void 0) summary.key = rule.key;
8519
+ if (rule.cron !== void 0) summary.cron = rule.cron;
8520
+ if (rule.timezone !== void 0) summary.timezone = rule.timezone;
8521
+ if (rule.period !== void 0) summary.period = rule.period;
8522
+ if (rule.jitter !== void 0) summary.jitter = rule.jitter;
8523
+ if (rule.urls !== void 0) summary.urls = rule.urls;
8524
+ if (rule.events !== void 0) summary.events = rule.events;
8525
+ if (rule.schema !== void 0) summary.fields = rule.schema.length;
8526
+ return summary;
7140
8527
  }
7141
8528
 
7142
8529
  // capture.ts
@@ -8866,7 +10253,7 @@ function stepoptions2(step) {
8866
10253
  }
8867
10254
  async function refreshcapabilities() {
8868
10255
  const report = await readcapabilities();
8869
- const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds, ...cdpkinds], profile: [...profilerkinds], emulation: [...emulationkinds], sessions: [...sessionkinds], workflow: [...workflowkinds] };
10256
+ const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds, ...cdpkinds], profile: [...profilerkinds], emulation: [...emulationkinds], sessions: [...sessionkinds], workflow: [...workflowkinds, ...controlflowkinds] };
8870
10257
  await memory.setcapabilities(withmedia);
8871
10258
  return withmedia;
8872
10259
  }
@@ -9189,6 +10576,8 @@ async function executewatchstep(step, session, plan, tabid2, origin) {
9189
10576
  const record2 = entry;
9190
10577
  await memory.addmutationevent({ ...record2, sessionid: session.id });
9191
10578
  }
10579
+ void evaluatepagetriggers("mutate").catch(() => {
10580
+ });
9192
10581
  }
9193
10582
  if (step.kind === "watchfocus") {
9194
10583
  for (const entry of detailarray(output.details, "events")) {
@@ -9196,6 +10585,8 @@ async function executewatchstep(step, session, plan, tabid2, origin) {
9196
10585
  const record2 = entry;
9197
10586
  await memory.addfocusevent({ ...record2, sessionid: session.id });
9198
10587
  }
10588
+ void evaluatepagetriggers("focus").catch(() => {
10589
+ });
9199
10590
  }
9200
10591
  if (step.kind === "watchbanner") {
9201
10592
  for (const entry of detailarray(output.details, "banners")) {
@@ -9203,6 +10594,8 @@ async function executewatchstep(step, session, plan, tabid2, origin) {
9203
10594
  const record2 = entry;
9204
10595
  await memory.addbanner({ ...record2, sessionid: session.id });
9205
10596
  }
10597
+ void evaluatepagetriggers("banner").catch(() => {
10598
+ });
9206
10599
  }
9207
10600
  await audit("watch", `Watch ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
9208
10601
  return { output, watch };
@@ -9330,6 +10723,8 @@ async function tracktabupdate(tabid2, changeinfo) {
9330
10723
  }
9331
10724
  chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
9332
10725
  void tracktabupdate(tabid2, changeinfo);
10726
+ if (typeof changeinfo.url === "string" && changeinfo.url.startsWith("https://")) void evaluatenavigationtriggers(changeinfo.url).catch(() => {
10727
+ });
9333
10728
  });
9334
10729
  chrome.tabs.onActivated.addListener((activeinfo) => {
9335
10730
  void recordtabwatchevent("activated", activeinfo.tabId);
@@ -11566,7 +12961,7 @@ async function executehttpstep(step, session, plan, tabid2, origin) {
11566
12961
  var activesockets = /* @__PURE__ */ new Map();
11567
12962
  var channelbuses = /* @__PURE__ */ new Map();
11568
12963
  var netpoll = 100;
11569
- function waitsome(milliseconds) {
12964
+ function waitsome2(milliseconds) {
11570
12965
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
11571
12966
  }
11572
12967
  async function queueinboundmessage(channelid, payload) {
@@ -11654,7 +13049,7 @@ async function runsubscription(subscription, controller) {
11654
13049
  }
11655
13050
  if (controller.signal.aborted) break;
11656
13051
  if (subscription.lifetime !== void 0 && Date.now() - startedat >= subscription.lifetime) break;
11657
- await waitsome(netpoll * 10);
13052
+ await waitsome2(netpoll * 10);
11658
13053
  }
11659
13054
  const closed = { ...current, state: "closed", closedat: Date.now() };
11660
13055
  await memory.setsubscription(closed);
@@ -11748,7 +13143,7 @@ async function executesocketstep(step, session, plan, tabid2, origin) {
11748
13143
  const queued = await memory.getmessages(channelid);
11749
13144
  matched = queued.filter((envelope) => matchmessage(filter, envelope)).slice(0, filter.limit ?? (matched.length || void 0));
11750
13145
  if (matched.length >= (filter.limit ?? 1) || Date.now() >= deadline) break;
11751
- await waitsome(netpoll);
13146
+ await waitsome2(netpoll);
11752
13147
  }
11753
13148
  await memory.drainmessages(matched.map((envelope) => ({ channelid: envelope.channelid, sequence: envelope.sequence })));
11754
13149
  await audit("socket", `The message wait on channel ${channelid} matched ${matched.length} envelope${matched.length === 1 ? "" : "s"} of the reviewed filter within the ${budget} millisecond budget; payload values stay out of the audit trail.`, extra);
@@ -11809,7 +13204,7 @@ async function executesocketstep(step, session, plan, tabid2, origin) {
11809
13204
  break;
11810
13205
  }
11811
13206
  stopcursor = decision.cursor;
11812
- await waitsome(decision.next?.wait ?? cursor.interval);
13207
+ await waitsome2(decision.next?.wait ?? cursor.interval);
11813
13208
  next = { url: decision.next?.url ?? next.url, ...decision.next?.body !== void 0 ? { body: decision.next.body } : {} };
11814
13209
  }
11815
13210
  } finally {
@@ -11831,7 +13226,7 @@ async function executenetwatchstep(step, session, plan, tabid2, origin) {
11831
13226
  const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit >= 1 ? options.limit : void 0;
11832
13227
  const before = await bridgecall(tabid2, "resourcerecords");
11833
13228
  const known = new Set(resourcefacts(before ?? []).map((fact) => `${fact.url}@${fact.start}`));
11834
- if (window2 > 0) await waitsome(window2);
13229
+ if (window2 > 0) await waitsome2(window2);
11835
13230
  const after = await bridgecall(tabid2, "resourcerecords");
11836
13231
  const fresh = resourcefacts(after ?? []).filter((fact) => !known.has(`${fact.url}@${fact.start}`));
11837
13232
  const chosen = limit !== void 0 ? fresh.slice(0, limit) : fresh;
@@ -12012,6 +13407,10 @@ async function executetimelinestep(step, session, plan, tabid2, origin) {
12012
13407
  const watcherstate = activetimelinewatchers.get(watchid);
12013
13408
  activetimelinewatchers.delete(watchid);
12014
13409
  await memory.closewatch(watchid, Date.now());
13410
+ if (step.kind === "watchconsole" || step.kind === "watcherrors") {
13411
+ void evaluatepagetriggers(step.kind === "watchconsole" ? "console" : "error").catch(() => {
13412
+ });
13413
+ }
12015
13414
  await audit("timeline", `Watcher ${watchid} detached cleanly after its reviewed window of ${watchwindow} milliseconds.`, extra);
12016
13415
  if (watcherstate?.cancelled) {
12017
13416
  await audit("timeline", `Watcher ${watchid} cancelled on run cancel or the killswitch; the captured window is discarded.`, extra);
@@ -13342,7 +14741,7 @@ async function sleepreviewed(sampled, stepid) {
13342
14741
  } catch {
13343
14742
  }
13344
14743
  }
13345
- await waitsome(sampled);
14744
+ await waitsome2(sampled);
13346
14745
  return "timer";
13347
14746
  }
13348
14747
  async function executewaitelement(step, tabid2) {
@@ -13360,7 +14759,7 @@ async function executewaitelement(step, tabid2) {
13360
14759
  const probe = await bridgecall(tabid2, "elementrect", wait.selector).catch(() => void 0);
13361
14760
  if (probe?.ok) return { ok: true, summary: `Selector ${wait.selector} appeared after ${Date.now() - startedat} milliseconds of polling every ${wait.poll} milliseconds.`, details: { selector: wait.selector, waited: Date.now() - startedat, poll: wait.poll } };
13362
14761
  if (singlepass || Date.now() >= deadline) break;
13363
- await waitsome(pass + 1 < plan.probes ? wait.poll : plan.lastwait);
14762
+ await waitsome2(pass + 1 < plan.probes ? wait.poll : plan.lastwait);
13364
14763
  }
13365
14764
  return { ok: false, summary: `Selector ${wait.selector} did not appear within the reviewed timeout of ${wait.timeout} milliseconds.`, details: { selector: wait.selector, waited: Date.now() - startedat } };
13366
14765
  }
@@ -13475,13 +14874,26 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
13475
14874
  activeworkflowruns.set(run.id, guards);
13476
14875
  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 });
13477
14876
  const context = { session, plan, tabid: tabid2, origin };
13478
- const execute = async (workflowstep) => {
14877
+ const executesteprouted = async (workflowstep, stepcontext) => {
13479
14878
  if (guards.cancelled) return { ok: false, summary: `The run was cancelled before the ${workflowstep.label} step dispatched.` };
13480
- return await dispatchworkflowstep(workflowstep, context);
14879
+ if (!iscontrolflowkind(workflowstep.kind)) return await dispatchworkflowstep(workflowstep, context);
14880
+ const controlled = await runcontrolstep({
14881
+ step: workflowstep,
14882
+ scopes: stepcontext.scopes,
14883
+ outputs: stepcontext.outputs ?? {},
14884
+ execute: async (dispatched, inner) => await executesteprouted(dispatched, inner),
14885
+ now: Date.now(),
14886
+ runid: run.id,
14887
+ pagestate: await pagestateof(tabid2),
14888
+ resolveelements: async (selector) => await resolveelements(tabid2, selector)
14889
+ });
14890
+ return { ...controlled.output, scopes: controlled.scopes, log: controlled.log };
13481
14891
  };
14892
+ const execute = async (workflowstep, stepcontext) => await executesteprouted(workflowstep, stepcontext);
13482
14893
  let result;
14894
+ const runbudget = options.timeout && typeof options.timeout === "object" && !Array.isArray(options.timeout) && typeof options.timeout.runms === "number" && Number.isFinite(options.timeout.runms) && options.timeout.runms > 0 ? options.timeout.runms : void 0;
13483
14895
  try {
13484
- result = await runworkflow({
14896
+ const launch = () => runworkflow({
13485
14897
  record: record2,
13486
14898
  run,
13487
14899
  scopes: runscopes(options.variables),
@@ -13497,6 +14909,18 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
13497
14909
  await refreshbadge();
13498
14910
  }
13499
14911
  });
14912
+ if (runbudget === void 0) {
14913
+ result = await launch();
14914
+ } else {
14915
+ const raced = await applyruntimeout({ budgetms: runbudget, run: launch });
14916
+ if (raced.cancelled) {
14917
+ guards.cancelled = true;
14918
+ const aborted = await storetimeoutabort(run, step, raced.error.message, runbudget);
14919
+ result = { run: aborted.run, scopes: [], log: aborted.log, outputs: {} };
14920
+ } else {
14921
+ result = raced.value;
14922
+ }
14923
+ }
13500
14924
  } finally {
13501
14925
  activeworkflowruns.delete(run.id);
13502
14926
  }
@@ -13506,12 +14930,240 @@ async function executeworkflowrun(step, session, plan, tabid2, origin, dry) {
13506
14930
  if (!stored.some((candidate) => candidate.stepid === entry.stepid && candidate.startedat === entry.startedat)) await memory.addrunlogentry(run.id, entry);
13507
14931
  }
13508
14932
  await memory.setrunscopes(run.id, result.scopes);
14933
+ const decisions = [];
14934
+ for (const entry of result.log) {
14935
+ const control = entry.details?.control;
14936
+ if (control && typeof control === "object" && !Array.isArray(control)) {
14937
+ const decision = control;
14938
+ decisions.push(decision);
14939
+ await memory.addcontroldecision(run.id, decision);
14940
+ }
14941
+ }
14942
+ for (const decision of decisions) await auditcontroldecision(run.id, decision, session.id, plan.id);
14943
+ const loopbounds = record2.steps.flatMap((controlstep) => {
14944
+ const summary = controlsummary(controlstep);
14945
+ return summary !== void 0 && summary.bound !== void 0 && (controlstep.kind === "loop" || controlstep.kind === "repeatuntil" || controlstep.kind === "whileloop") ? [summary.bound] : [];
14946
+ });
14947
+ if (loopbounds.length > 0) {
14948
+ const iterations = decisions.filter((decision) => decision.kind === "loop").reduce((total, decision) => total + (decision.loops?.length ?? 0), 0);
14949
+ const denominator = loopbounds.reduce((total, bound) => total + bound, 0);
14950
+ await memory.setprogress(recordworkflow(await memory.getprogress(), plan.id, step.id, { family: "control", detail: `The control flow steps of the workflow ${record2.name} ran their loops toward the reviewed bounds`, runid: run.id, iterations, denominator }, Date.now()));
14951
+ }
13509
14952
  const executed = result.run.cursor;
13510
14953
  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()));
13511
14954
  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 });
13512
14955
  await refreshbadge();
13513
14956
  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 } : {} } };
13514
14957
  }
14958
+ async function pagestateof(tabid2) {
14959
+ const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
14960
+ if (!tab) return {};
14961
+ return { ...tab.url !== void 0 ? { url: tab.url } : {}, ...tab.title !== void 0 ? { title: tab.title } : {}, ...tab.status !== void 0 ? { ready: tab.status === "complete" } : {} };
14962
+ }
14963
+ async function resolveelements(tabid2, selector) {
14964
+ const resolved = await bridgecall(tabid2, "queryelements", selector).catch(() => void 0);
14965
+ if (!resolved?.ok) return [];
14966
+ return resolved.selectors ?? [];
14967
+ }
14968
+ async function storetimeoutabort(run, step, message, budget) {
14969
+ const at = Date.now();
14970
+ const aborted = { ...run, state: "failed", endedat: at, failreason: message };
14971
+ const entry = { stepid: step.id, label: `Run budget of ${step.id}`, state: "failed", startedat: at, duration: 0, summary: message, details: { errorclass: "timeout", budget, cancelled: true, control: { runid: run.id, stepid: step.id, kind: "timeout", at, timeouts: [{ stepid: step.id, budget, scope: "run", at }] } } };
14972
+ const decision = entry.details?.control;
14973
+ await memory.addcontroldecision(run.id, decision);
14974
+ await memory.addrunlogentry(run.id, entry);
14975
+ 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 });
14976
+ return { run: aborted, log: [entry] };
14977
+ }
14978
+ async function executetriggerstep(step, session, plan, tabid2, origin) {
14979
+ const options = stepoptions2(step);
14980
+ const family = triggerfamilyof(step.kind);
14981
+ if (!family) throw new Error(`The ${step.kind} step is not a reviewed trigger kind.`);
14982
+ const workflowid = typeof options.workflowid === "string" ? options.workflowid : "";
14983
+ const record2 = await memory.getworkflowrecord(workflowid);
14984
+ if (!record2) throw new Error(`No composed workflow matches ${workflowid || "the reviewed id"}; trigger rules bind to workflows already composed and reviewed.`);
14985
+ 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() });
14986
+ if (!armed) throw new Error(`The ${step.kind} payload does not arm as a reviewed rule.`);
14987
+ if (!ruleoriginsgranted(armed, record2.origins)) throw new Error("The trigger touches origins outside the workflow grant list; review the rule or the workflow origins.");
14988
+ for (const matchorigin of armed.origins ?? []) {
14989
+ if (!origingranted(session, matchorigin)) throw new Error(`The trigger origin ${matchorigin} falls outside the session grants.`);
14990
+ }
14991
+ if (armed.pattern !== void 0) {
14992
+ let patternorigin2 = "";
14993
+ try {
14994
+ patternorigin2 = new URL(armed.pattern).origin;
14995
+ } catch {
14996
+ patternorigin2 = "";
14997
+ }
14998
+ if (patternorigin2 && !origingranted(session, patternorigin2)) throw new Error(`The trigger origin ${patternorigin2} falls outside the session grants.`);
14999
+ }
15000
+ for (const url of armed.urls ?? []) {
15001
+ let listorigin = "";
15002
+ try {
15003
+ listorigin = new URL(url).origin;
15004
+ } catch {
15005
+ listorigin = "";
15006
+ }
15007
+ if (listorigin && !origingranted(session, listorigin)) throw new Error(`The trigger origin ${listorigin} falls outside the session grants.`);
15008
+ }
15009
+ 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;
15010
+ const rule = { ...armed, ...nextfireat !== void 0 ? { state: { ...armed.state, nextfireat } } : {} };
15011
+ await memory.addtriggerule(rule);
15012
+ await registermenurule(rule);
15013
+ await scheduletriggerwakes();
15014
+ 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()));
15015
+ 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 });
15016
+ void origin;
15017
+ void tabid2;
15018
+ 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 } : {} } } };
15019
+ }
15020
+ async function runofworkflowactive(workflowid) {
15021
+ if ([...activeworkflowruns.keys()].length === 0) return false;
15022
+ for (const run of await memory.listworkflowruns()) {
15023
+ if (run.workflowid === workflowid && run.state === "running") return true;
15024
+ }
15025
+ return false;
15026
+ }
15027
+ async function launchtriggerrun(fire, rule) {
15028
+ const session = await memory.getsession();
15029
+ const plan = await memory.getplan();
15030
+ const now = Date.now();
15031
+ if (!session || session.stoppedat || session.pausedat || session.expiresat <= now) return { launched: false, reason: "The trigger launch needs a live, unpaused browser session." };
15032
+ if (!plan || plan.state !== "approved") return { launched: false, reason: "The trigger launch needs the approved plan review." };
15033
+ const record2 = await memory.getworkflowrecord(rule.workflowid);
15034
+ if (!record2) return { launched: false, reason: "The workflow of the trigger rule is no longer composed; evaluation skips the rule." };
15035
+ for (const workfloworigin of record2.origins) {
15036
+ if (!origingranted(session, workfloworigin)) return { launched: false, reason: `The workflow origin ${workfloworigin} falls outside the session grants.` };
15037
+ }
15038
+ const variables = { triggercause: fire.cause, triggerrule: rule.id };
15039
+ if (fire.url !== void 0) variables.triggerurl = fire.url;
15040
+ if (fire.title !== void 0) variables.triggertitle = fire.title;
15041
+ if (fire.payload !== void 0) variables.triggerpayload = JSON.stringify(fire.payload);
15042
+ 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 }) };
15043
+ const output = await executeworkflowstep(runstep2, session, plan, session.tabid, session.origin).catch((error) => ({ ok: false, summary: error instanceof Error ? error.message : String(error), details: {} }));
15044
+ const launched = Boolean(output.ok);
15045
+ const runid = output.details?.runid;
15046
+ 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) } }));
15047
+ await memory.addtriggerfire(fire);
15048
+ 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 });
15049
+ if (launched) await notifytriggerfired(fire, rule, runid);
15050
+ await refreshbadge();
15051
+ return { launched, ...runid !== void 0 ? { runid } : {}, ...launched ? {} : { reason: output.ok === false ? output.summary : "the run ended" } };
15052
+ }
15053
+ function nextfireof(rule, after) {
15054
+ if (rule.kind === "cron" && rule.cron !== void 0) return schedulecron({ cron: rule.cron, ...rule.timezone !== void 0 ? { timezone: rule.timezone } : {} }, after);
15055
+ 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));
15056
+ return void 0;
15057
+ }
15058
+ async function firetrigger(rule, cause, url, title, payload) {
15059
+ const now = Date.now();
15060
+ const record2 = await memory.getworkflowrecord(rule.workflowid);
15061
+ const runactive = await runofworkflowactive(rule.workflowid);
15062
+ const decision = evaluatetrigger({ rule, now, cause, ...url !== void 0 ? { url } : {}, ...title !== void 0 ? { title } : {}, ...payload !== void 0 ? { payload } : {}, runactive: false, workflowreviewed: record2 !== void 0 });
15063
+ if (!decision.fired || decision.fire === void 0) {
15064
+ await memory.settriggerule(updaterule(rule, { stats: { suppressions: rule.stats.suppressions + 1 } }));
15065
+ 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` : ""}.`, {});
15066
+ return { fired: false, ...decision.suppressed !== void 0 ? { suppressed: decision.suppressed } : {} };
15067
+ }
15068
+ if (runactive) {
15069
+ const queued = queuefire(await memory.gettriggerqueue(), decision.fire);
15070
+ await memory.settriggerqueue(queued.queue);
15071
+ await memory.settriggerule(updaterule(rule, { stats: { suppressions: rule.stats.suppressions + 1 } }));
15072
+ 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"}.`, {});
15073
+ await refreshbadge();
15074
+ return { fired: true, queued: true };
15075
+ }
15076
+ await memory.settriggerule(updaterule(rule, { stats: { fires: rule.stats.fires + 1 } }));
15077
+ const launch = await launchtriggerrun(decision.fire, rule);
15078
+ return { fired: true, ...launch.launched ? {} : { suppressed: launch.reason } };
15079
+ }
15080
+ async function draintriggerqueue() {
15081
+ const queue = await memory.gettriggerqueue();
15082
+ if (queue.length === 0) return { launched: 0, remaining: 0 };
15083
+ const result = await drainqueue(queue, async (fire) => {
15084
+ const rule = await memory.gettriggerule(fire.ruleid);
15085
+ if (!rule) return;
15086
+ await memory.settriggerule(updaterule(rule, { stats: { fires: rule.stats.fires + 1 } }));
15087
+ await launchtriggerrun(fire, rule);
15088
+ });
15089
+ await memory.settriggerqueue(result.remaining);
15090
+ await audit("trigger", `The trigger queue drained ${result.launched} queued fire${result.launched === 1 ? "" : "s"} with ${result.remaining} still waiting.`, {});
15091
+ await refreshbadge();
15092
+ return { launched: result.launched, remaining: result.remaining.length };
15093
+ }
15094
+ async function evaluatenavigationtriggers(url, title) {
15095
+ if (!url.startsWith("https://")) return;
15096
+ const session = await memory.getsession();
15097
+ if (!session || session.stoppedat) return;
15098
+ for (const { rule } of await memory.listtriggers()) {
15099
+ if (!rule.state.enabled || rule.state.pausedat !== void 0) continue;
15100
+ if (rule.kind === "visit" && rule.origins !== void 0 && visitmatch(rule.origins, url)) await firetrigger(rule, "visit", url, title);
15101
+ if (rule.kind === "url" && rule.pattern !== void 0 && matchurl(rule.pattern, url)) await firetrigger(rule, "url", url, title);
15102
+ }
15103
+ await evaluatepagetriggers("navigate", url, title);
15104
+ }
15105
+ async function evaluatepagetriggers(event, url, title) {
15106
+ const session = await memory.getsession();
15107
+ if (!session || session.stoppedat || session.pausedat) return;
15108
+ for (const rule of observeevents(await memory.gettriggerules(), event)) {
15109
+ await firetrigger(rule, "event", url, title, { event });
15110
+ }
15111
+ }
15112
+ async function evaluatelistedtriggers() {
15113
+ const rules = await memory.gettriggerules();
15114
+ const due = listdue(rules, Date.now());
15115
+ for (const entry of due) {
15116
+ await firetrigger(entry.rule, "schedule");
15117
+ }
15118
+ await scheduletriggerwakes();
15119
+ return { due: due.length };
15120
+ }
15121
+ async function registermenurule(rule) {
15122
+ if (rule.kind !== "menu" || rule.title === void 0) return;
15123
+ const menus = chrome.contextMenus;
15124
+ if (typeof menus?.create !== "function") {
15125
+ 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.`, {});
15126
+ return;
15127
+ }
15128
+ try {
15129
+ menus.create({ id: `devthinktrigger${rule.id}`, title: rule.title, contexts: ["page", "selection", "link"] });
15130
+ } catch {
15131
+ }
15132
+ }
15133
+ async function registermenurules() {
15134
+ for (const { rule } of await memory.listtriggers()) await registermenurule(rule);
15135
+ }
15136
+ async function scheduletriggerwakes() {
15137
+ const alarms = chrome.alarms;
15138
+ if (typeof alarms?.create !== "function") return;
15139
+ try {
15140
+ const rules = await memory.gettriggerules();
15141
+ const scheduled = rules.filter((rule) => rule.state.enabled && rule.state.pausedat === void 0 && rule.state.nextfireat !== void 0);
15142
+ if (scheduled.length === 0) return;
15143
+ const next = Math.min(...scheduled.map((rule) => rule.state.nextfireat));
15144
+ const oneminute = 6e4;
15145
+ alarms.create("devthinktriggerwake", { when: Math.max(Date.now() + oneminute, next) });
15146
+ } catch {
15147
+ }
15148
+ }
15149
+ async function notifytriggerfired(fire, rule, runid) {
15150
+ const notifications = chrome.notifications;
15151
+ if (typeof notifications?.create === "function") {
15152
+ try {
15153
+ 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}` : ""}.` });
15154
+ } catch {
15155
+ }
15156
+ }
15157
+ void runid;
15158
+ }
15159
+ async function auditcontroldecision(runid, decision, sessionid, planid) {
15160
+ 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 });
15161
+ 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 });
15162
+ else if (decision.kind === "join" && decision.join !== void 0) await audit("workflow", `The join of the parallel step ${decision.stepid} of the run ${runid} merged ${decision.join.merged.length} variable${decision.join.merged.length === 1 ? "" : "s"} under the ${decision.join.strategy} strategy${decision.join.conflicts.length > 0 ? ` with the conflicts ${decision.join.conflicts.join(", ")}` : " with no conflict"}.`, { sessionid, planid });
15163
+ else if (decision.kind === "catch" && decision.catch !== void 0) await audit("workflow", `The catch handler of the try step ${decision.stepid} of the run ${runid} ran after the ${decision.catch.errorclass} failure${decision.catch.rerun ? " and reran the fragile body" : ""}.`, { sessionid, planid });
15164
+ else if (decision.kind === "retry" && decision.retries !== void 0) await audit("workflow", `The retry policy of the try step ${decision.stepid} of the run ${runid} ran ${decision.retries.length} retry attempt${decision.retries.length === 1 ? "" : "s"} with the backoff durations ${decision.retries.map((attempt) => `${attempt.delay} ms`).join(", ")}.`, { sessionid, planid });
15165
+ else if (decision.kind === "timeout" && decision.timeouts !== void 0) await audit("workflow", `The timeout policy of the try step ${decision.stepid} of the run ${runid} aborted ${decision.timeouts.length} step${decision.timeouts.length === 1 ? "" : "s"} that exceeded their reviewed budgets.`, { sessionid, planid });
15166
+ }
13515
15167
  async function executestep(stepid) {
13516
15168
  const session = await memory.getsession();
13517
15169
  const plan = await memory.getplan();
@@ -13597,6 +15249,9 @@ async function executeaction(step, session, plan, tabid2, origin, settings, verd
13597
15249
  } else if (isworkflowkind(step.kind)) {
13598
15250
  if (!session || !plan || plan.state !== "approved") throw new Error("Workflow kinds refuse to run outside an approved session plan.");
13599
15251
  output = await executeworkflowstep(step, session, plan, tabid2, origin);
15252
+ } else if (istriggeraction(step.kind)) {
15253
+ if (!session || !plan || plan.state !== "approved") throw new Error("Trigger kinds refuse to run outside an approved session plan.");
15254
+ output = await executetriggerstep(step, session, plan, tabid2, origin);
13600
15255
  } else {
13601
15256
  if (step.target && freshcheckkinds.has(step.kind)) {
13602
15257
  const fresh = await snapshot(tabid2);
@@ -13697,7 +15352,9 @@ async function pausesession() {
13697
15352
  if (session.pausedat) throw new Error("The browser session is already paused.");
13698
15353
  const paused = { ...session, pausedat: Date.now() };
13699
15354
  await memory.setsession(paused);
15355
+ await memory.settriggerules(pauseall(await memory.gettriggerules(), Date.now()));
13700
15356
  await audit("pause", "The user paused the browser session; no action or preview can run.", { sessionid: session.id });
15357
+ await audit("trigger", "The session pause suspended every armed trigger rule; fires that arrive while paused queue for the resume drain.", { sessionid: session.id });
13701
15358
  return paused;
13702
15359
  }
13703
15360
  async function resumesession() {
@@ -13707,7 +15364,11 @@ async function resumesession() {
13707
15364
  if (!session.pausedat) throw new Error("The browser session is not paused.");
13708
15365
  const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat, ...session.grants ? { grants: session.grants } : {} };
13709
15366
  await memory.setsession(resumed);
15367
+ await memory.settriggerules(resumeall(await memory.gettriggerules()));
13710
15368
  await audit("resume", "The user resumed the browser session; reviewed actions can run again.", { sessionid: session.id });
15369
+ await audit("trigger", "The session resume released the suspended trigger rules and drained the queued fires through the same gates.", { sessionid: session.id });
15370
+ void draintriggerqueue().catch(() => {
15371
+ });
13711
15372
  return resumed;
13712
15373
  }
13713
15374
  async function grantcapability(permission) {
@@ -13845,7 +15506,7 @@ async function handlerequest(message, sender) {
13845
15506
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
13846
15507
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
13847
15508
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
13848
- 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) } : {} }), runlogretention: runsettings?.runlogretention, ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
15509
+ 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, ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
13849
15510
  }
13850
15511
  case "capabilities":
13851
15512
  return refreshcapabilities();
@@ -13892,7 +15553,8 @@ async function handlerequest(message, sender) {
13892
15553
  const network = outcome.details?.network;
13893
15554
  const timeline = outcome.details?.timeline;
13894
15555
  const sessionblock = outcome.details?.session;
13895
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...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 } : {} } } : {} }));
15556
+ const triggerblock = outcome.details?.trigger;
15557
+ 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 } : {} } } : {} }));
13896
15558
  }
13897
15559
  case "map": {
13898
15560
  const plan = await memory.getplan();
@@ -15016,7 +16678,49 @@ async function handlerequest(message, sender) {
15016
16678
  const inputreview = message;
15017
16679
  const record2 = await memory.getworkflowrecord(inputreview.workflowid?.trim() ?? "");
15018
16680
  if (!record2) throw new Error(`No composed workflow matches ${inputreview.workflowid ?? ""}.`);
15019
- return { record: record2, steps: record2.steps.map((entry) => ({ id: entry.id, kind: entry.kind, label: entry.label, ...entry.target !== void 0 ? { target: entry.target } : {}, ...entry.value !== void 0 ? { value: entry.value } : {}, ...entry.block !== void 0 ? { block: entry.block } : {}, ...entry.bindings !== void 0 ? { bindings: entry.bindings } : {}, ...entry.expression !== void 0 ? { expression: entry.expression } : {}, ...entry.extract !== void 0 ? { extract: entry.extract } : {} })), blocks: record2.blocks, risk: record2.risk, origins: record2.origins };
16681
+ return { record: record2, steps: record2.steps.map((entry) => ({ id: entry.id, kind: entry.kind, label: entry.label, ...entry.target !== void 0 ? { target: entry.target } : {}, ...entry.value !== void 0 ? { value: entry.value } : {}, ...entry.block !== void 0 ? { block: entry.block } : {}, ...entry.bindings !== void 0 ? { bindings: entry.bindings } : {}, ...entry.expression !== void 0 ? { expression: entry.expression } : {}, ...entry.extract !== void 0 ? { extract: entry.extract } : {}, ...controlsummary(entry) !== void 0 ? { control: controlsummary(entry) } : {} })), blocks: record2.blocks, risk: record2.risk, origins: record2.origins };
16682
+ }
16683
+ case "setloopbound": {
16684
+ const inputbound = message;
16685
+ const session = await memory.getsession();
16686
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Editing a loop safety bound needs an active browser session behind the consent gates.");
16687
+ const record2 = await memory.getworkflowrecord(inputbound.workflowid?.trim() ?? "");
16688
+ if (!record2) throw new Error(`No composed workflow matches ${inputbound.workflowid ?? ""}.`);
16689
+ if (typeof inputbound.bound !== "number" || !Number.isInteger(inputbound.bound) || inputbound.bound < 1) throw new Error("The loop safety bound must be a positive integer with no code ceiling.");
16690
+ const target = record2.steps.find((entry) => entry.id === (inputbound.stepid ?? ""));
16691
+ if (!target || target.kind !== "loop" && target.kind !== "repeatuntil") throw new Error(`No loop or repeat until step of the workflow matches ${inputbound.stepid ?? ""}.`);
16692
+ let payload;
16693
+ try {
16694
+ payload = JSON.parse(target.options ?? "{}");
16695
+ } catch {
16696
+ payload = {};
16697
+ }
16698
+ const key = target.kind === "loop" ? "loop" : "repeatuntil";
16699
+ const body = payload[key] && typeof payload[key] === "object" && !Array.isArray(payload[key]) ? payload[key] : {};
16700
+ body.bound = inputbound.bound;
16701
+ payload[key] = body;
16702
+ const restep = { ...target, options: JSON.stringify(payload) };
16703
+ const recomposed = composeworkflow({
16704
+ id: record2.id,
16705
+ name: record2.name,
16706
+ version: record2.version + 1,
16707
+ origins: [...record2.origins],
16708
+ steps: record2.steps.map((entry) => entry.id === target.id ? restep : entry),
16709
+ blocks: [...record2.blocks],
16710
+ now: Date.now(),
16711
+ kindallowed: (kind) => {
16712
+ try {
16713
+ actionrisk(kind);
16714
+ return true;
16715
+ } catch {
16716
+ return false;
16717
+ }
16718
+ },
16719
+ riskof: (kind) => actionrisk(kind)
16720
+ });
16721
+ await memory.addworkflowrecord(recomposed);
16722
+ await audit("workflow", `The user edited the loop safety bound of the step ${target.id} of the workflow ${record2.name} to ${inputbound.bound} iterations; version ${recomposed.version} was recomposed and the older version survives for the audit trail.`, { sessionid: session.id });
16723
+ return { workflowid: recomposed.id, stepid: target.id, bound: inputbound.bound, version: recomposed.version };
15020
16724
  }
15021
16725
  case "approveworkflowrun": {
15022
16726
  const inputapprove = message;
@@ -15039,8 +16743,24 @@ async function handlerequest(message, sender) {
15039
16743
  const step = record2.steps.find((entry) => entry.id === (inputsingle.stepid ?? ""));
15040
16744
  if (!step) throw new Error(`No step of the workflow matches ${inputsingle.stepid ?? ""}.`);
15041
16745
  const { tab, origin } = await activecontext();
15042
- const executed = await runstep({ step, scopes: await memory.getrunscopes(stored.run.id), outputs: {}, execute: async (dispatched) => dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin }), now: Date.now(), ...step.block !== void 0 ? { block: step.block } : {} });
16746
+ const executestepsingle = async (dispatched, stepcontext) => {
16747
+ if (iscontrolflowkind(dispatched.kind)) {
16748
+ const controlled = await runcontrolstep({ step: dispatched, scopes: stepcontext.scopes, outputs: stepcontext.outputs ?? {}, execute: executestepsingle, now: Date.now(), runid: stored.run.id, pagestate: await pagestateof(tab.id), resolveelements: async (selector) => await resolveelements(tab.id, selector) });
16749
+ return { ...controlled.output, scopes: controlled.scopes, log: controlled.log };
16750
+ }
16751
+ return await dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin });
16752
+ };
16753
+ const executed = await runstep({ step, scopes: await memory.getrunscopes(stored.run.id), outputs: {}, execute: executestepsingle, now: Date.now(), ...step.block !== void 0 ? { block: step.block } : {} });
15043
16754
  await memory.addrunlogentry(stored.run.id, executed.log);
16755
+ if (executed.childlog !== void 0) for (const entry of executed.childlog) await memory.addrunlogentry(stored.run.id, entry);
16756
+ for (const entry of [executed.log, ...executed.childlog ?? []]) {
16757
+ const control = entry.details?.control;
16758
+ if (control && typeof control === "object" && !Array.isArray(control)) {
16759
+ const decision = control;
16760
+ await memory.addcontroldecision(stored.run.id, decision);
16761
+ await auditcontroldecision(stored.run.id, decision, session.id, plan.id);
16762
+ }
16763
+ }
15044
16764
  await memory.setrunscopes(stored.run.id, executed.scopes);
15045
16765
  const stepindex = record2.steps.findIndex((entry) => entry.id === step.id);
15046
16766
  const isthenext = stepindex === stored.run.cursor && executed.output.ok;
@@ -15073,15 +16793,32 @@ async function handlerequest(message, sender) {
15073
16793
  const scopes = await memory.getrunscopes(stored.run.id);
15074
16794
  const log = await memory.getrunlog(stored.run.id);
15075
16795
  const { tab, origin } = await activecontext();
15076
- const resumed = await runworkflow({ record: record2, run: stored.run, scopes, log, execute: async (dispatched) => dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin }), 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) => {
16796
+ let storedentries = log.length;
16797
+ const executeresume = async (dispatched, stepcontext) => {
16798
+ if (iscontrolflowkind(dispatched.kind)) {
16799
+ const controlled = await runcontrolstep({ step: dispatched, scopes: stepcontext.scopes, outputs: stepcontext.outputs ?? {}, execute: executeresume, now: Date.now(), runid: stored.run.id, pagestate: await pagestateof(tab.id), resolveelements: async (selector) => await resolveelements(tab.id, selector) });
16800
+ return { ...controlled.output, scopes: controlled.scopes, log: controlled.log };
16801
+ }
16802
+ return await dispatchworkflowstep(dispatched, { session, plan, tabid: tab.id, origin });
16803
+ };
16804
+ 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) => {
15077
16805
  await memory.setworkflowrun(state.run);
15078
- const last = state.log[state.log.length - 1];
15079
- if (last) await memory.addrunlogentry(state.run.id, last);
16806
+ for (const entry of state.log.slice(storedentries)) await memory.addrunlogentry(state.run.id, entry);
16807
+ storedentries = state.log.length;
15080
16808
  await memory.setrunscopes(state.run.id, state.scopes);
15081
16809
  } });
16810
+ for (const entry of resumed.log.slice(storedentries)) await memory.addrunlogentry(stored.run.id, entry);
16811
+ for (const entry of resumed.log.slice(log.length)) {
16812
+ const control = entry.details?.control;
16813
+ if (control && typeof control === "object" && !Array.isArray(control)) {
16814
+ const decision = control;
16815
+ await memory.addcontroldecision(stored.run.id, decision);
16816
+ await auditcontroldecision(stored.run.id, decision, session.id, plan.id);
16817
+ }
16818
+ }
15082
16819
  await memory.setworkflowrun(resumed.run);
15083
16820
  await memory.setrunscopes(stored.run.id, resumed.scopes);
15084
- await audit("workflow", `Resumed the workflow run ${stored.run.id} from the checkpoint at step cursor ${stored.run.cursor}; the run ended ${resumed.run.state} at cursor ${resumed.run.cursor}.`, { sessionid: session.id, planid: plan.id });
16821
+ await audit("workflow", `Resumed the workflow run ${stored.run.id} from the checkpoint at step cursor ${stored.run.cursor}; the run ended ${resumed.run.state} at cursor ${resumed.run.cursor} with ${resumed.log.length - log.length} new runlog entries.`, { sessionid: session.id, planid: plan.id });
15085
16822
  await refreshbadge();
15086
16823
  return { runid: resumed.run.id, state: resumed.run.state, cursor: resumed.run.cursor };
15087
16824
  }
@@ -15103,6 +16840,184 @@ async function handlerequest(message, sender) {
15103
16840
  if (!stored) throw new Error(`No workflow run matches ${inputoutcome.runid ?? ""}.`);
15104
16841
  return workflowoutcome({ run: stored.run, entries: stored.log, ...inputoutcome.stepid !== void 0 && inputoutcome.stepid !== "" ? { stepid: inputoutcome.stepid } : {} });
15105
16842
  }
16843
+ case "triggerreview": {
16844
+ const triggers = await memory.listtriggers();
16845
+ const runs = await memory.listworkflowruns();
16846
+ const queue = await memory.gettriggerqueue();
16847
+ const rules = await Promise.all(triggers.map(async (entry) => {
16848
+ const record2 = await memory.getworkflowrecord(entry.rule.workflowid);
16849
+ return {
16850
+ id: entry.rule.id,
16851
+ kind: entry.rule.kind,
16852
+ workflowid: entry.rule.workflowid,
16853
+ ...entry.workflowname !== void 0 ? { workflowname: entry.workflowname } : {},
16854
+ label: entry.rule.label,
16855
+ enabled: entry.rule.state.enabled,
16856
+ paused: entry.rule.state.pausedat !== void 0,
16857
+ cooldown: entry.rule.state.cooldown,
16858
+ ...entry.rule.state.lastfireat !== void 0 ? { lastfireat: entry.rule.state.lastfireat } : {},
16859
+ ...entry.rule.state.nextfireat !== void 0 ? { nextfireat: entry.rule.state.nextfireat } : {},
16860
+ fires: entry.rule.stats.fires,
16861
+ launches: entry.rule.stats.launches,
16862
+ suppressions: entry.rule.stats.suppressions,
16863
+ summary: triggersummary(entry.rule),
16864
+ steps: record2 ? record2.steps.map((inner) => ({ id: inner.id, kind: inner.kind, label: inner.label })) : [],
16865
+ ...entry.rule.kind === "webhook" ? { deliveries: (await memory.listwebhookpayloads(entry.rule.id)).length } : {}
16866
+ };
16867
+ }));
16868
+ const active = runs.filter((run) => run.state === "running").length;
16869
+ return { rules, queued: queue.length, active, fires: (await memory.listtriggerfires()).slice(0, 50) };
16870
+ }
16871
+ case "toggletrigger": {
16872
+ const inputtoggle = message;
16873
+ const rule = await memory.gettriggerule(inputtoggle.ruleid ?? "");
16874
+ if (!rule) throw new Error(`No armed trigger rule matches ${inputtoggle.ruleid ?? ""}.`);
16875
+ if (typeof inputtoggle.enabled !== "boolean") throw new Error("The trigger toggle needs the reviewed enabled flag.");
16876
+ const updated = updaterule(rule, { state: { enabled: inputtoggle.enabled } });
16877
+ await memory.settriggerule(updated);
16878
+ 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.`, {});
16879
+ await scheduletriggerwakes();
16880
+ return { ruleid: rule.id, enabled: updated.state.enabled };
16881
+ }
16882
+ case "createtrigger": {
16883
+ const inputcreate = message;
16884
+ const session = await memory.getsession();
16885
+ const plan = await memory.getplan();
16886
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Arming a trigger rule needs an active browser session behind the consent gates.");
16887
+ if (!plan || plan.state !== "approved") throw new Error("Arming a trigger rule needs the approved plan review.");
16888
+ const family = triggerfamilyof(`${inputcreate.family ?? ""}rule`);
16889
+ if (!family) throw new Error("The trigger family is not a reviewed family.");
16890
+ 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 ?? {} }) };
16891
+ const verdict = canexecute({ session, plan, step, tabid: session.tabid, origin: session.origin, now: Date.now() });
16892
+ if (!verdict.allowed) throw new Error(verdict.reason ?? "The trigger rule was refused by the consent gates.");
16893
+ return await executetriggerstep(step, session, plan, session.tabid, session.origin);
16894
+ }
16895
+ case "createvisitrule": {
16896
+ const inputvisit = message;
16897
+ const session = await memory.getsession();
16898
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Creating a visit rule needs an active browser session behind the consent gates.");
16899
+ const tab = await chrome.tabs.get(session.tabid).catch(() => void 0);
16900
+ const url = tab?.url ?? "";
16901
+ let origin = "";
16902
+ try {
16903
+ origin = new URL(url).origin;
16904
+ } catch {
16905
+ origin = "";
16906
+ }
16907
+ if (!origin.startsWith("https://")) throw new Error("The current page carries no HTTPS origin to bind a visit rule to.");
16908
+ return await handlerequest({ kind: "createtrigger", workflowid: inputvisit.workflowid ?? "", family: "visit", payload: { origins: [origin] }, label: `Visit ${origin}` }, {});
16909
+ }
16910
+ case "duplicatetrigger": {
16911
+ const inputduplicate = message;
16912
+ const rule = await memory.gettriggerule(inputduplicate.ruleid ?? "");
16913
+ if (!rule) throw new Error(`No armed trigger rule matches ${inputduplicate.ruleid ?? ""}.`);
16914
+ const record2 = await memory.getworkflowrecord(inputduplicate.workflowid ?? "");
16915
+ if (!record2) throw new Error(`No composed workflow matches ${inputduplicate.workflowid ?? ""}.`);
16916
+ const { state: rulestate, stats: rulestats, createdat: rulecreatedat, id: ruleid, workflowid: ruleworkflowid, label: rulelabel, cooldown: rulecooldown, ...familypayload } = rule;
16917
+ void rulestate;
16918
+ void rulestats;
16919
+ void rulecreatedat;
16920
+ void ruleid;
16921
+ void ruleworkflowid;
16922
+ void rulelabel;
16923
+ 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() });
16924
+ if (!armed) throw new Error("The duplicated rule payload failed its family grammar.");
16925
+ 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;
16926
+ const duplicate = { ...armed, ...nextfireat !== void 0 ? { state: { ...armed.state, nextfireat } } : {} };
16927
+ if (!ruleoriginsgranted(duplicate, record2.origins)) throw new Error("The duplicated rule touches origins outside the workflow grant list of its new workflow.");
16928
+ await memory.addtriggerule(duplicate);
16929
+ await registermenurule(duplicate);
16930
+ await scheduletriggerwakes();
16931
+ 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.`, {});
16932
+ return { ruleid: duplicate.id, workflowid: duplicate.workflowid };
16933
+ }
16934
+ case "manualrun": {
16935
+ const inputmanual = message;
16936
+ const record2 = await memory.getworkflowrecord(inputmanual.workflowid ?? "");
16937
+ if (!record2) throw new Error(`No composed workflow matches ${inputmanual.workflowid ?? ""}.`);
16938
+ const preview = manualpreview(record2, Date.now());
16939
+ await memory.addmanualrun(preview);
16940
+ 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.`, {});
16941
+ return { manualrun: preview, workflowname: record2.name };
16942
+ }
16943
+ case "confirmmanualrun": {
16944
+ const inputconfirm = message;
16945
+ const stored = (await memory.listmanualruns()).find((entry) => entry.id === (inputconfirm.previewid ?? ""));
16946
+ if (!stored) throw new Error(`No manual run preview matches ${inputconfirm.previewid ?? ""}.`);
16947
+ if (typeof inputconfirm.confirmed !== "boolean") throw new Error("The manual run confirmation needs the reviewed confirmed flag.");
16948
+ const session = await memory.getsession();
16949
+ const plan = await memory.getplan();
16950
+ 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.");
16951
+ if (!plan || plan.state !== "approved") throw new Error("Confirming a manual run needs the approved plan review.");
16952
+ const record2 = await memory.getworkflowrecord(stored.workflowid);
16953
+ if (!record2) throw new Error("The workflow of the manual run preview is no longer composed in the library.");
16954
+ const decided = confirmmanualrun(stored, inputconfirm.confirmed, Date.now());
16955
+ await memory.addmanualrun(decided);
16956
+ if (!inputconfirm.confirmed) {
16957
+ 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 });
16958
+ return { confirmed: false };
16959
+ }
16960
+ 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 }) };
16961
+ const output = await executeworkflowstep(runstep2, session, plan, session.tabid, session.origin).catch((error) => ({ ok: false, summary: error instanceof Error ? error.message : String(error), details: {} }));
16962
+ 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 });
16963
+ return { confirmed: true, runid: output.details?.runid, state: output.ok ? "done" : "failed" };
16964
+ }
16965
+ case "firetrigger": {
16966
+ const inputfire = message;
16967
+ const rule = await memory.gettriggerule(inputfire.ruleid ?? "");
16968
+ if (!rule) throw new Error(`No armed trigger rule matches ${inputfire.ruleid ?? ""}.`);
16969
+ const result = await firetrigger(rule, "manual");
16970
+ return { fired: result.fired, ...result.queued !== void 0 ? { queued: result.queued } : {}, ...result.suppressed !== void 0 ? { suppressed: result.suppressed } : {} };
16971
+ }
16972
+ case "receivewebhook": {
16973
+ const inputwebhook = message;
16974
+ const rule = await memory.gettriggerule(inputwebhook.ruleid ?? "");
16975
+ if (!rule || rule.kind !== "webhook") throw new Error(`No webhook rule matches ${inputwebhook.ruleid ?? ""}.`);
16976
+ const verification = verifywebhook({ rule, secret: inputwebhook.secret ?? "", payload: inputwebhook.payload });
16977
+ if (!verification.verified) {
16978
+ await memory.settriggerule(updaterule(rule, { stats: { suppressions: rule.stats.suppressions + 1 } }));
16979
+ await audit("trigger", `A webhook delivery for the rule ${rule.id} was refused: ${verification.reason}; only secret verified payloads ever persist.`, {});
16980
+ return { verified: false, reason: verification.reason };
16981
+ }
16982
+ await memory.addwebhookpayload(rule.id, inputwebhook.payload, Date.now());
16983
+ const result = await firetrigger(rule, "webhook", void 0, void 0, inputwebhook.payload);
16984
+ return { verified: true, fired: result.fired, ...result.queued !== void 0 ? { queued: result.queued } : {}, ...result.suppressed !== void 0 ? { suppressed: result.suppressed } : {} };
16985
+ }
16986
+ case "rotatetriggersecret": {
16987
+ const inputrotate = message;
16988
+ const rule = await memory.gettriggerule(inputrotate.ruleid ?? "");
16989
+ if (!rule || rule.kind !== "webhook") throw new Error(`No webhook rule matches ${inputrotate.ruleid ?? ""}.`);
16990
+ const bytes = crypto.getRandomValues(new Uint8Array(24));
16991
+ const secret = [...bytes].map((byte) => (byte % 36).toString(36)).join("").padEnd(24, "7x9k2m").slice(0, 24);
16992
+ const rotated = { ...rule, secret, state: { ...rule.state }, stats: { ...rule.stats } };
16993
+ await memory.settriggerule(rotated);
16994
+ 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.`, {});
16995
+ return { ruleid: rule.id, secret };
16996
+ }
16997
+ case "settriggerretention": {
16998
+ const inputretention = message;
16999
+ const settings = await memory.getsettings();
17000
+ const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
17001
+ await memory.setsettings({ ...settings, ...retention !== void 0 ? { triggerretention: retention } : {} });
17002
+ 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.`);
17003
+ return { triggerretention: retention };
17004
+ }
17005
+ case "triggerhistory": {
17006
+ const inputhistory = message;
17007
+ const fires = await memory.listtriggerfires(inputhistory.ruleid);
17008
+ return { fires };
17009
+ }
17010
+ case "buttontrigger": {
17011
+ const rules = (await memory.gettriggerules()).filter((rule) => rule.kind === "button" && rule.state.enabled && rule.state.pausedat === void 0);
17012
+ if (rules.length === 0) return { fired: false, reason: "No enabled button rule is armed." };
17013
+ const results = [];
17014
+ for (const rule of rules) results.push({ ruleid: rule.id, ...await firetrigger(rule, "button") });
17015
+ return { fired: results.some((entry) => entry.fired), rules: results };
17016
+ }
17017
+ case "triggerfiredreport": {
17018
+ const fires = await memory.listtriggerfires();
17019
+ return triggerlist({ rules: await memory.gettriggerules(), workflows: await memory.listworkflows(), queue: await memory.gettriggerqueue() });
17020
+ }
15106
17021
  default:
15107
17022
  throw new Error("Unknown Devthink request.");
15108
17023
  }
@@ -15202,4 +17117,78 @@ chrome.runtime.onConnect.addListener((port) => {
15202
17117
  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) }));
15203
17118
  });
15204
17119
  });
17120
+ {
17121
+ const webnavigation = chrome.webNavigation;
17122
+ try {
17123
+ webnavigation?.onCommitted?.addListener((details) => {
17124
+ if (details.frameId !== 0 || !details.url.startsWith("https://")) return;
17125
+ void evaluatenavigationtriggers(details.url).catch(() => {
17126
+ });
17127
+ });
17128
+ } catch {
17129
+ }
17130
+ }
17131
+ {
17132
+ const menus = chrome.contextMenus;
17133
+ try {
17134
+ menus?.onClicked?.addListener((info) => {
17135
+ const menuid = typeof info.menuItemId === "string" ? info.menuItemId : "";
17136
+ if (!menuid.startsWith("devthinktrigger")) return;
17137
+ void (async () => {
17138
+ const rule = await memory.gettriggerule(menuid.slice("devthinktrigger".length));
17139
+ if (!rule) return;
17140
+ await firetrigger(rule, "menu");
17141
+ })().catch(() => {
17142
+ });
17143
+ });
17144
+ } catch {
17145
+ }
17146
+ }
17147
+ {
17148
+ const commands = chrome.commands;
17149
+ try {
17150
+ commands?.onCommand?.addListener((command) => {
17151
+ void (async () => {
17152
+ for (const { rule } of await memory.listtriggers()) {
17153
+ if (rule.kind !== "key" || rule.command !== command) continue;
17154
+ await firetrigger(rule, "key");
17155
+ }
17156
+ })().catch(() => {
17157
+ });
17158
+ });
17159
+ } catch {
17160
+ }
17161
+ }
17162
+ {
17163
+ const action = chrome.action;
17164
+ try {
17165
+ action?.onClicked?.addListener(() => {
17166
+ void handlerequest({ kind: "buttontrigger" }, {}).catch(() => {
17167
+ });
17168
+ });
17169
+ } catch {
17170
+ }
17171
+ }
17172
+ {
17173
+ const alarms = chrome.alarms;
17174
+ try {
17175
+ alarms?.onAlarm?.addListener((alarm) => {
17176
+ if (alarm.name !== "devthinktriggerwake") return;
17177
+ void evaluatelistedtriggers().then(() => draintriggerqueue()).catch(() => {
17178
+ });
17179
+ });
17180
+ } catch {
17181
+ }
17182
+ }
17183
+ setInterval(() => {
17184
+ void evaluatelistedtriggers().then(() => draintriggerqueue()).catch(() => {
17185
+ });
17186
+ }, 3e4);
17187
+ async function restoretriggers() {
17188
+ await registermenurules();
17189
+ await evaluatelistedtriggers();
17190
+ await draintriggerqueue();
17191
+ }
17192
+ restoretriggers().catch(() => {
17193
+ });
15205
17194
  //# sourceMappingURL=background.js.map