wield-mcp 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1907 @@
1
+ // <define:__WIELD_DEV_KEYS__>
2
+ var define_WIELD_DEV_KEYS_default = {};
3
+
4
+ // packages/recipe/src/html.ts
5
+ var MAX_JSON_VALUE = 256 * 1024;
6
+
7
+ // packages/recipe/src/interpreter.ts
8
+ var LIMITS = {
9
+ steps: 12,
10
+ foreach: 20,
11
+ requests: 40,
12
+ responseBytes: 5 * 1024 * 1024,
13
+ runMs: 6e4,
14
+ redirects: 3,
15
+ confirmMs: 13e4
16
+ };
17
+
18
+ // packages/recipe/src/lint.ts
19
+ var OPERAND = String.raw`(?<![\w$.@])(§S|params\s*\.\s*[A-Za-z_]\w*|\$[A-Za-z_]\w*)`;
20
+ var COMPARISON = new RegExp(String.raw`${OPERAND}\s*(<=|>=|<|>)|(<=|>=|<|>)\s*${OPERAND}`, "g");
21
+
22
+ // packages/recipe/src/canonical.ts
23
+ function canonicalJson(v) {
24
+ return JSON.stringify(sortKeys(v));
25
+ }
26
+ function sortKeys(v) {
27
+ if (Array.isArray(v)) return v.map(sortKeys);
28
+ if (v && typeof v === "object") {
29
+ const out = {};
30
+ for (const k of Object.keys(v).sort()) {
31
+ const x = v[k];
32
+ if (x !== void 0) out[k] = sortKeys(x);
33
+ }
34
+ return out;
35
+ }
36
+ return v;
37
+ }
38
+
39
+ // packages/recipe/src/signature.ts
40
+ var enc = new TextEncoder();
41
+ function wieldSigMessage(ctx, scope, sha256hex) {
42
+ return enc.encode(`wield-sig/v1
43
+ ${ctx}
44
+ ${scope}
45
+ ${sha256hex}`);
46
+ }
47
+ function b64decode(s) {
48
+ const t = s.replace(/-/g, "+").replace(/_/g, "/");
49
+ const bin = atob(t + "===".slice((t.length + 3) % 4));
50
+ const out = new Uint8Array(bin.length);
51
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
52
+ return out;
53
+ }
54
+ function hex(b) {
55
+ let s = "";
56
+ for (const x of b) s += x.toString(16).padStart(2, "0");
57
+ return s;
58
+ }
59
+ async function sha256HexWeb(data) {
60
+ const bytes = typeof data === "string" ? enc.encode(data) : data;
61
+ return hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes)));
62
+ }
63
+ var webCryptoVerify = async (pub, message, sig) => {
64
+ const subtle = globalThis.crypto?.subtle;
65
+ if (!subtle) throw new Error("WebCrypto is not available");
66
+ const key = await subtle.importKey("raw", pub, { name: "Ed25519" }, false, ["verify"]);
67
+ return subtle.verify({ name: "Ed25519" }, key, sig, message);
68
+ };
69
+ async function verifySignature(keyring, sig, expect, verify = webCryptoVerify) {
70
+ if (!sig) return { ok: false, reason: "missing" };
71
+ if (typeof sig.kid !== "string" || typeof sig.ctx !== "string" || typeof sig.sig !== "string") return { ok: false, reason: "malformed" };
72
+ if (sig.ctx !== expect.ctx) return { ok: false, reason: "wrong_context", kid: sig.kid };
73
+ const pub = keyring[sig.kid];
74
+ if (!pub) return { ok: false, reason: "unknown_key", kid: sig.kid };
75
+ let pk, raw;
76
+ try {
77
+ pk = b64decode(pub);
78
+ raw = b64decode(sig.sig);
79
+ } catch {
80
+ return { ok: false, reason: "malformed", kid: sig.kid };
81
+ }
82
+ if (pk.length !== 32 || raw.length !== 64) return { ok: false, reason: "malformed", kid: sig.kid };
83
+ let good = false;
84
+ try {
85
+ good = await verify(pk, wieldSigMessage(expect.ctx, expect.scope, expect.sha256), raw);
86
+ } catch (e) {
87
+ if (/not available|not supported|Unrecognized|NotSupported/i.test(String(e?.message ?? e))) throw e;
88
+ good = false;
89
+ }
90
+ return good ? { ok: true, kid: sig.kid } : { ok: false, reason: "bad_signature", kid: sig.kid };
91
+ }
92
+ function describeSignatureFailure(what, reason) {
93
+ switch (reason) {
94
+ case "missing":
95
+ return `${what} is not signed`;
96
+ case "malformed":
97
+ return `${what} has a malformed signature`;
98
+ case "unknown_key":
99
+ return `${what} is signed with a key this version of Wield does not trust`;
100
+ case "wrong_context":
101
+ return `${what} carries a signature made for something else`;
102
+ case "bad_signature":
103
+ return `${what} does not match its signature (it was changed after signing)`;
104
+ }
105
+ }
106
+
107
+ // packages/recipe/src/keys.ts
108
+ var REGISTRY_KEYS = {
109
+ // wield.run production registry key (CHANGES 18:40 · C3; also GET /api/v1/keys). Verified against the live index,
110
+ // all 31 recipes and a pinned version on 2026-11-07 before pinning.
111
+ "wield-2026-11": "qYB97SDzBjuHUZOIzEUKFHo3zlHD6YmI1iAQpXsyTaM"
112
+ };
113
+
114
+ // packages/recipe/src/features.ts
115
+ var INTERPRETER_FEATURES = [
116
+ /** extract.html / html_items fields with parse: "json" (and path): per-element JSON attributes (CHANGES 2026-11-07 C1). */
117
+ "html_json",
118
+ /** page.host / page.origin in templates and context expressions: the language host the run landed on (CHANGES 22:39 C4). */
119
+ "page_context"
120
+ ];
121
+
122
+ // apps/extension/src/shared/types.ts
123
+ var PROTOCOL_VERSION = 1;
124
+ var BUILD_ID = "0.1.15+20261107220223.8c74";
125
+ var RELOAD_HINT = "Reload the Wield extension once: open chrome://extensions (vivaldi://extensions in Vivaldi) and click the reload arrow on the Wield card, then try again.";
126
+ var LIMITS2 = {
127
+ steps: 12,
128
+ foreach: 20,
129
+ requests: 40,
130
+ responseBytes: 5 * 1024 * 1024,
131
+ runMs: 6e4,
132
+ confirmMs: 12e4,
133
+ /** How long the "quick human check" note waits for a click in the site tab. */
134
+ humanCheckMs: 6e4,
135
+ tabLoadMs: 3e4,
136
+ /** Settle time after a tab we opened reaches `complete`, so anti-bot sensor scripts can post (tests/e2e/SPIKE.md). */
137
+ tabSettleMs: 1500
138
+ };
139
+
140
+ // apps/extension/src/sw/ask.ts
141
+ var pending = /* @__PURE__ */ new Map();
142
+ function answerAsk(id, allow) {
143
+ const done = pending.get(id);
144
+ if (!done) return false;
145
+ done(allow ? "allow" : "deny");
146
+ return true;
147
+ }
148
+ async function askUser(ask, signal) {
149
+ const id = crypto.randomUUID();
150
+ const full = { ...ask, id, expires_at: Date.now() + LIMITS2.confirmMs };
151
+ await chrome.storage.session.set({ [`ask:${id}`]: full });
152
+ const win = await chrome.windows.create({
153
+ url: chrome.runtime.getURL(`confirm.html#${id}`),
154
+ type: "popup",
155
+ width: 460,
156
+ height: ask.kind === "write" || ask.kind === "draft" ? 620 : 420,
157
+ focused: true
158
+ });
159
+ return new Promise((resolve) => {
160
+ const finish2 = (outcome) => {
161
+ if (!pending.has(id)) return;
162
+ pending.delete(id);
163
+ clearTimeout(timer);
164
+ chrome.windows.onRemoved.removeListener(onRemoved);
165
+ signal.removeEventListener("abort", onAbort);
166
+ chrome.storage.session.remove(`ask:${id}`).catch(() => {
167
+ });
168
+ if (win?.id !== void 0) chrome.windows.remove(win.id).catch(() => {
169
+ });
170
+ resolve(outcome);
171
+ };
172
+ const onRemoved = (windowId) => windowId === win?.id && finish2("closed");
173
+ const onAbort = () => finish2("cancelled");
174
+ const timer = setTimeout(() => finish2("timeout"), LIMITS2.confirmMs);
175
+ pending.set(id, finish2);
176
+ chrome.windows.onRemoved.addListener(onRemoved);
177
+ signal.addEventListener("abort", onAbort);
178
+ });
179
+ }
180
+
181
+ // apps/extension/src/sw/native.ts
182
+ var NATIVE_HOST = "run.wield.bridge";
183
+ var ALIVE_AFTER_MS = 800;
184
+ var NativeLink = class {
185
+ constructor(onMessage, onState) {
186
+ this.onMessage = onMessage;
187
+ this.onState = onState;
188
+ }
189
+ port = null;
190
+ retry = 0;
191
+ retryTimer;
192
+ state = { state: "disconnected", last_error: null, since: null };
193
+ ensure() {
194
+ if (this.port || this.retryTimer) return;
195
+ this.connect();
196
+ }
197
+ connect() {
198
+ clearTimeout(this.retryTimer);
199
+ this.retryTimer = void 0;
200
+ this.set({ state: "connecting", last_error: this.state.last_error, since: (/* @__PURE__ */ new Date()).toISOString() });
201
+ let port;
202
+ try {
203
+ port = chrome.runtime.connectNative(NATIVE_HOST);
204
+ } catch (e) {
205
+ this.fail(String(e));
206
+ return;
207
+ }
208
+ this.port = port;
209
+ const alive = setTimeout(() => this.port === port && this.up(), ALIVE_AFTER_MS);
210
+ port.onMessage.addListener((m) => {
211
+ if (this.port !== port) return;
212
+ this.up();
213
+ this.onMessage(m);
214
+ });
215
+ port.onDisconnect.addListener(() => {
216
+ clearTimeout(alive);
217
+ if (this.port !== port) return;
218
+ this.port = null;
219
+ this.fail(chrome.runtime.lastError?.message ?? "The bridge exited");
220
+ });
221
+ }
222
+ send(msg) {
223
+ if (!this.port) return false;
224
+ try {
225
+ this.port.postMessage(msg);
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+ get connected() {
232
+ return this.state.state === "connected";
233
+ }
234
+ up() {
235
+ if (this.state.state === "connected") return;
236
+ this.retry = 0;
237
+ this.set({ state: "connected", last_error: null, since: (/* @__PURE__ */ new Date()).toISOString() });
238
+ }
239
+ fail(err) {
240
+ const delay = Math.min(3e4, 1e3 * 2 ** this.retry++);
241
+ this.set({ state: "disconnected", last_error: err, since: (/* @__PURE__ */ new Date()).toISOString() });
242
+ this.retryTimer = setTimeout(() => {
243
+ this.retryTimer = void 0;
244
+ this.connect();
245
+ }, delay);
246
+ }
247
+ set(s) {
248
+ this.state = s;
249
+ this.onState(s);
250
+ }
251
+ };
252
+
253
+ // apps/extension/src/sw/errors.ts
254
+ var ExecError = class extends Error {
255
+ constructor(code, message, step) {
256
+ super(message);
257
+ this.code = code;
258
+ this.step = step;
259
+ this.name = "ExecError";
260
+ }
261
+ };
262
+ var CODES = /* @__PURE__ */ new Set([
263
+ "invalid_params",
264
+ "login_required",
265
+ "confirmation_denied",
266
+ "expect_failed",
267
+ "origin_blocked",
268
+ "timeout",
269
+ "tab_error",
270
+ "cancelled",
271
+ "limit_exceeded",
272
+ "executor_offline",
273
+ "recipe_invalid",
274
+ "site_challenge",
275
+ "recipe_unsigned"
276
+ ]);
277
+ function toRunError(e, fallback = "tab_error") {
278
+ const any = e;
279
+ const code = any?.code && CODES.has(any.code) ? any.code : fallback;
280
+ const out = { code, message: String(any?.message ?? e).slice(0, 500) };
281
+ if (any?.step) out.step = any.step;
282
+ return out;
283
+ }
284
+ var UnresponsiveTab = class extends ExecError {
285
+ unresponsive = true;
286
+ constructor(message) {
287
+ super("tab_error", message);
288
+ }
289
+ };
290
+
291
+ // apps/extension/src/sw/store.ts
292
+ var MAX_RUNS = 30;
293
+ async function recordRun(run) {
294
+ const { runs = [] } = await chrome.storage.local.get("runs");
295
+ runs.unshift(run);
296
+ await chrome.storage.local.set({ runs: runs.slice(0, MAX_RUNS) });
297
+ }
298
+ async function recordSites(sites) {
299
+ const { sites: prev = {} } = await chrome.storage.local.get("sites");
300
+ for (const s of sites) prev[s.site_id] = s;
301
+ await chrome.storage.local.set({ sites: prev });
302
+ }
303
+ async function recordBridge(state) {
304
+ await chrome.storage.session.set({ bridge: state });
305
+ }
306
+
307
+ // apps/extension/src/sw/pacing.ts
308
+ var STRICT = { burst: 3, everyMs: 2e3, perHour: 30 };
309
+ var WATCHED = { burst: 4, everyMs: 1e3, perHour: 60 };
310
+ var PROTECTED = {
311
+ "bahn.de": STRICT,
312
+ // Akamai, gates fahrplan (403 OPS_BLOCKED on 7 Nov)
313
+ "idealo.de": STRICT,
314
+ // Akamai
315
+ "zalando.de": STRICT,
316
+ // Akamai
317
+ "mobile.de": STRICT,
318
+ // Akamai on suchen.mobile.de
319
+ "eventim.de": STRICT,
320
+ // Akamai
321
+ "eventim.com": STRICT,
322
+ // Akamai on public-api.eventim.com
323
+ "immobilienscout24.de": STRICT,
324
+ // AWS WAF Bot Control
325
+ "kleinanzeigen.de": WATCHED,
326
+ // Akamai sensor present; logged-in messaging
327
+ "doctolib.de": WATCHED,
328
+ // Cloudflare / DataDome; logged-in booking
329
+ "jameda.de": WATCHED,
330
+ // PerimeterX / DataDome
331
+ ...false ? { "pace.localhost": { burst: 2, everyMs: 1e3, perHour: 6 } } : {}
332
+ };
333
+ var DEFAULT_RULE = { burst: 6, everyMs: 500, perHour: 300 };
334
+ function siteKey(origin) {
335
+ return new URL(origin).hostname.split(".").slice(-2).join(".");
336
+ }
337
+ function ruleFor(key) {
338
+ const r = PROTECTED[key] ?? DEFAULT_RULE;
339
+ return false ? { ...r, perHour: r.perHour * 20 } : r;
340
+ }
341
+ var chains = /* @__PURE__ */ new Map();
342
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
343
+ function acquire(origin) {
344
+ const key = siteKey(origin);
345
+ const rule = ruleFor(key);
346
+ const run = async () => {
347
+ const storeKey = `pace:${key}`;
348
+ const now = Date.now();
349
+ const st = (await chrome.storage.session.get(storeKey))[storeKey] ?? { tokens: rule.burst, at: now, hour: [] };
350
+ st.hour = st.hour.filter((t) => t > now - 36e5);
351
+ if (st.hour.length >= rule.perHour) {
352
+ const freeIn = Math.ceil((st.hour[0] + 36e5 - now) / 6e4);
353
+ return {
354
+ ok: false,
355
+ message: `Wield paused ${key}: ${rule.perHour} requests in the last hour is its limit, to keep your session with the site in good standing. Try again in about ${freeIn} min.`
356
+ };
357
+ }
358
+ let tokens = Math.min(rule.burst, st.tokens + (now - st.at) / rule.everyMs);
359
+ let waited = 0;
360
+ if (tokens < 1) {
361
+ waited = Math.ceil((1 - tokens) * rule.everyMs);
362
+ await sleep(waited);
363
+ tokens = 1;
364
+ }
365
+ const at = now + waited;
366
+ st.tokens = tokens - 1;
367
+ st.at = at;
368
+ st.hour.push(at);
369
+ await chrome.storage.session.set({ [storeKey]: st });
370
+ return { ok: true, waited_ms: waited };
371
+ };
372
+ const next = (chains.get(key) ?? Promise.resolve()).then(run, run);
373
+ chains.set(key, next.catch(() => void 0));
374
+ return next;
375
+ }
376
+ async function paceSnapshot() {
377
+ const all = await chrome.storage.session.get(null);
378
+ const out = {};
379
+ const now = Date.now();
380
+ for (const [k, v] of Object.entries(all)) {
381
+ if (!k.startsWith("pace:")) continue;
382
+ const key = k.slice(5);
383
+ out[key] = { last_hour: v.hour.filter((t) => t > now - 36e5).length, per_hour: ruleFor(key).perHour };
384
+ }
385
+ return out;
386
+ }
387
+
388
+ // apps/extension/src/shared/keys.ts
389
+ function registryKeyring() {
390
+ return { ...REGISTRY_KEYS, ...typeof define_WIELD_DEV_KEYS_default === "undefined" ? {} : define_WIELD_DEV_KEYS_default };
391
+ }
392
+ function signaturesRequired() {
393
+ return Object.keys(REGISTRY_KEYS).length > 0;
394
+ }
395
+
396
+ // apps/extension/src/sw/policy.ts
397
+ var KEY = "policies2";
398
+ var DOMAIN = /^(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/i;
399
+ async function storedPolicies() {
400
+ return (await chrome.storage.local.get(KEY))[KEY] ?? {};
401
+ }
402
+ function badShape(d) {
403
+ if (!d || typeof d !== "object") return "policy missing";
404
+ if (typeof d.org_id !== "string" || !d.org_id) return "policy needs org_id";
405
+ if (typeof d.issued_at !== "string" || Number.isNaN(Date.parse(d.issued_at))) return "policy needs issued_at";
406
+ if (d.allowed_sites != null && (!Array.isArray(d.allowed_sites) || !d.allowed_sites.every((s) => typeof s === "string" && DOMAIN.test(s)))) return "allowed_sites must be site ids like bahn.de";
407
+ if (d.writes !== void 0 && d.writes !== "allow" && d.writes !== "deny") return "writes must be allow or deny";
408
+ if (d.telemetry !== void 0 && !["off", "user", "allowed"].includes(d.telemetry)) return "telemetry must be off or user";
409
+ if (d.org_key !== void 0 && (typeof d.org_key?.kid !== "string" || typeof d.org_key?.public_key !== "string")) return "org_key must be {kid, public_key}";
410
+ return null;
411
+ }
412
+ function isOlder(a, b) {
413
+ if (typeof a.version === "number" && typeof b.version === "number" && a.version !== b.version) return a.version < b.version;
414
+ return Date.parse(a.issued_at) < Date.parse(b.issued_at);
415
+ }
416
+ async function acceptPolicy(doc) {
417
+ const raw = doc;
418
+ if (!raw || typeof raw !== "object") return { ok: false, error: "policy document missing" };
419
+ let body;
420
+ let signedText;
421
+ if (typeof raw.body === "string") {
422
+ try {
423
+ body = JSON.parse(raw.body);
424
+ } catch {
425
+ return { ok: false, error: "the signed policy body is not JSON" };
426
+ }
427
+ if (raw.policy && canonicalJson(raw.policy) !== canonicalJson(body)) return { ok: false, error: "the policy differs from its signed body" };
428
+ signedText = raw.body;
429
+ } else {
430
+ if (raw.policy && typeof raw.policy === "object") body = raw.policy;
431
+ else {
432
+ const { signature: _s, ...rest } = raw;
433
+ body = rest;
434
+ }
435
+ signedText = canonicalJson(body);
436
+ }
437
+ const bad = badShape(body);
438
+ if (bad) return { ok: false, error: bad };
439
+ const check = await verifySignature(registryKeyring(), raw.signature, { ctx: "policy", scope: body.org_id, sha256: await sha256HexWeb(signedText) });
440
+ if (!check.ok) return { ok: false, error: describeSignatureFailure("The organization policy", check.reason) };
441
+ const all = await storedPolicies();
442
+ const prev = all[body.org_id];
443
+ if (prev && isOlder(body, prev.policy)) return { ok: false, error: "an older policy cannot replace a newer one" };
444
+ if (!prev || canonicalJson(prev.policy) !== canonicalJson(body)) {
445
+ all[body.org_id] = { policy: body, received_at: (/* @__PURE__ */ new Date()).toISOString() };
446
+ await chrome.storage.local.set({ [KEY]: all });
447
+ }
448
+ return { ok: true, org_id: body.org_id };
449
+ }
450
+ async function managedPolicy() {
451
+ try {
452
+ const m = await chrome.storage.managed.get(null);
453
+ return m && Object.keys(m).length ? m : null;
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+ async function effectivePolicy() {
459
+ const eff = { allowLists: [], writesDenied: null, telemetryOff: false, requireSigned: false, orgKeys: /* @__PURE__ */ new Map(), sources: [] };
460
+ const now = Date.now();
461
+ const apply = (name, kind, p, expired) => {
462
+ eff.sources.push({ name, kind, expired, ...p.issued_at ? { issued_at: p.issued_at } : {} });
463
+ if (Array.isArray(p.allowed_sites) && p.allowed_sites.length) eff.allowLists.push({ source: name, sites: p.allowed_sites });
464
+ if (p.writes === "deny" && !eff.writesDenied) eff.writesDenied = name;
465
+ if (p.telemetry === "off") eff.telemetryOff = true;
466
+ if (p.require_signed === true) eff.requireSigned = true;
467
+ };
468
+ for (const s of Object.values(await storedPolicies())) {
469
+ const p = s.policy;
470
+ apply(p.org_name || p.org_slug || p.org_id, "signed", p, !!p.expires_at && Date.parse(p.expires_at) < now);
471
+ if (p.org_key) eff.orgKeys.set(p.org_id, p.org_key);
472
+ }
473
+ const m = await managedPolicy();
474
+ if (m) apply(typeof m.org_name === "string" && m.org_name ? m.org_name : "your administrator", "managed", m, false);
475
+ return eff;
476
+ }
477
+ function siteMatches(pattern, host) {
478
+ const p = pattern.toLowerCase();
479
+ const h = host.toLowerCase();
480
+ if (p.startsWith("*.")) return h.endsWith(p.slice(1));
481
+ return h === p || h.endsWith(`.${p}`);
482
+ }
483
+ function blockedBy(eff, host) {
484
+ for (const l of eff.allowLists) if (!l.sites.some((s) => siteMatches(s, host))) return l.source;
485
+ return null;
486
+ }
487
+ function policySummary(eff) {
488
+ if (!eff.sources.length) return null;
489
+ return {
490
+ orgs: eff.sources.map((s) => ({ name: s.name, kind: s.kind, expired: s.expired })),
491
+ allowed_sites: eff.allowLists.length ? eff.allowLists.map((l) => l.sites) : null,
492
+ writes: eff.writesDenied ? "deny" : "allow",
493
+ telemetry: eff.telemetryOff ? "off" : "user",
494
+ require_signed: eff.requireSigned
495
+ };
496
+ }
497
+
498
+ // apps/extension/src/sw/signing.ts
499
+ async function developerMode() {
500
+ return (await chrome.storage.local.get("dev_mode")).dev_mode === true;
501
+ }
502
+ async function checkRecipeSignature(recipe, signature, policy, source, draft, bundled = false) {
503
+ if (bundled) return { status: "unsigned", why: "bundled" };
504
+ if (signature === void 0 || signature === null) {
505
+ if (policy.requireSigned) throw new ExecError("recipe_unsigned", "This recipe is not signed, and your organization requires signed recipes. Nothing was sent.");
506
+ if (source === "local") {
507
+ if (!await developerMode()) throw new ExecError("recipe_unsigned", "This is an unsigned local recipe. Wield runs those only with Developer mode on (Wield popup \u2192 Developer). Nothing was sent.");
508
+ return { status: "unsigned", why: "local" };
509
+ }
510
+ if (draft === true) return { status: "unsigned", why: "draft" };
511
+ if (signaturesRequired()) throw new ExecError("recipe_unsigned", "This recipe is not signed. Wield only runs signed registry recipes; nothing was sent.");
512
+ return { status: "unsigned", why: "not required" };
513
+ }
514
+ const s = signature;
515
+ const isPrivate = s.ctx === "private-recipe";
516
+ const scope = isPrivate ? String(s.scope ?? "") : "public";
517
+ const orgKey = isPrivate ? policy.orgKeys.get(scope) : void 0;
518
+ const keyring = isPrivate ? orgKey ? { [orgKey.kid]: orgKey.public_key } : {} : registryKeyring();
519
+ const ctx = isPrivate ? "private-recipe" : "recipe";
520
+ const check = await verifySignature(keyring, s, { ctx, scope, sha256: await sha256HexWeb(canonicalJson(recipe)) });
521
+ if (!check.ok) throw new ExecError("recipe_unsigned", `${describeSignatureFailure("This recipe", check.reason)}: refusing to run it. Nothing was sent.`);
522
+ return { status: "verified", kid: check.kid, scope };
523
+ }
524
+
525
+ // apps/extension/src/sw/withdrawn.ts
526
+ var KEY2 = "withdrawn_index";
527
+ async function acceptIndex(doc) {
528
+ const d = doc;
529
+ if (!d || typeof d.body !== "string") return { ok: false, error: "the index body (exact signed text) is missing" };
530
+ const check = await verifySignature(registryKeyring(), d.signature, { ctx: "index", scope: "public", sha256: await sha256HexWeb(d.body) });
531
+ if (!check.ok) return { ok: false, error: describeSignatureFailure("The registry index", check.reason) };
532
+ let idx;
533
+ try {
534
+ idx = JSON.parse(d.body);
535
+ } catch {
536
+ return { ok: false, error: "the signed index is not JSON" };
537
+ }
538
+ const generated_at = String(idx.generated_at ?? "");
539
+ if (!Number.isFinite(Date.parse(generated_at))) return { ok: false, error: "the index has no generated_at" };
540
+ if (Date.parse(generated_at) > Date.now() + 24 * 60 * 6e4) return { ok: false, error: "the index is dated more than a day in the future" };
541
+ const prev = await stored();
542
+ if (prev && Date.parse(prev.generated_at) > Date.parse(generated_at)) return { ok: false, error: "an older registry index cannot replace a newer one" };
543
+ const map = {};
544
+ for (const r of Array.isArray(idx.recipes) ? idx.recipes : []) {
545
+ if (!r || typeof r.id !== "string") continue;
546
+ const min = Number.isInteger(r.min_version) ? r.min_version : 1;
547
+ const withdrawn = Array.isArray(r.withdrawn_versions) ? r.withdrawn_versions.filter((v) => Number.isInteger(v)) : [];
548
+ if (min > 1 || withdrawn.length) map[r.id] = { min, withdrawn };
549
+ }
550
+ await chrome.storage.local.set({ [KEY2]: { generated_at, map } });
551
+ return { ok: true, generated_at, withdrawn: Object.keys(map).length };
552
+ }
553
+ async function stored() {
554
+ return (await chrome.storage.local.get(KEY2))[KEY2] ?? null;
555
+ }
556
+ async function withdrawnReason(id, version) {
557
+ const w = (await stored())?.map[id];
558
+ if (!w) return null;
559
+ if (version < w.min) return `${id} v${version} is below the registry's minimum version ${w.min}`;
560
+ if (w.withdrawn.includes(version)) return `${id} v${version} was withdrawn by the registry`;
561
+ return null;
562
+ }
563
+
564
+ // packages/builder/src/classify.ts
565
+ var JWT = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
566
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
567
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
568
+ var ISO_DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
569
+ var DE_DATE = /^\d{1,2}\.\d{1,2}\.(\d{2}|\d{4})$/;
570
+ var TIME = /^\d{1,2}:\d{2}(:\d{2})?$/;
571
+ var URN = /^(?:[a-z][a-z0-9-]{1,15}:(?:[a-z][a-z0-9_-]{0,30})?:+|gid:\/\/[a-z]+\/[A-Za-z]+\/)(?=\S)/;
572
+ var SECRET_KEY = /(^|[_-])(token|csrf|xsrf|auth|authorization|session|sess|sid|ssid|esid|secret|password|passwd|pwd|otp|nonce|signature|sig|apikey|api_key|access_key|private|cookie|bearer|jwt|hash_?key|device_?id|fingerprint|captcha|recaptcha|cf_clearance)([_-]|$)|token|csrf|xsrf|secret|password|session|signature|apikey|captcha/i;
573
+ function looksLikeToken(v) {
574
+ if (typeof v !== "string") return false;
575
+ const s = v.trim();
576
+ if (s.length < 16) return false;
577
+ if (JWT.test(s)) return true;
578
+ if (UUID.test(s)) return true;
579
+ if (/\s/.test(s)) return false;
580
+ if (/^[0-9]+$/.test(s)) return false;
581
+ if (ISO_DATETIME.test(s) || ISO_DATE.test(s) || DE_DATE.test(s) || TIME.test(s)) return false;
582
+ if (/^https?:\/\//.test(s)) return false;
583
+ if (/^[\w.+-]+\/[\w.+*-]+(\s*;.*)?$/.test(s)) return false;
584
+ if (/^[a-z]+(?:[-_][a-z]+){1,}$/i.test(s)) return false;
585
+ if (/[,;|]/.test(s) && s.split(/\s*[,;|]\s*/).every((p) => /^[A-Za-z][A-Za-z0-9_.-]{0,40}$/.test(p) && !/\d{4,}/.test(p))) return false;
586
+ if (URN.test(s)) return false;
587
+ if (/^[a-z]+(?:[A-Z][a-z]*)+\d{0,2}$|^(?:[A-Z][a-z]+){2,}[A-Z]?\d{0,2}$/.test(s)) return false;
588
+ const parts = s.split(/[-_]+/).filter(Boolean);
589
+ if (parts.length >= 3 && parts.every((p) => /^[a-z0-9]+$/.test(p)) && parts.filter((p) => /^[a-z]{2,}$/.test(p)).length >= parts.length / 2) return false;
590
+ if (/^\/?[A-Za-z_$][\w$-]*(?:[./:][A-Za-z_$][\w$-]*)+\/?$/.test(s)) return false;
591
+ if (s.includes("/") && s.split("/").every((seg) => seg === "" || seg.length < 16 || /^[A-Za-z][a-z]+(?:[A-Z][a-z]+)*$/.test(seg))) return false;
592
+ const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((r) => r.test(s)).length;
593
+ return s.length >= 24 || classes >= 3;
594
+ }
595
+
596
+ // packages/builder/src/trace.ts
597
+ var TRACE_FORMAT = "wield-trace/1";
598
+ var MULTI_PART_TLDS = /* @__PURE__ */ new Set([
599
+ "co.uk",
600
+ "org.uk",
601
+ "ac.uk",
602
+ "gov.uk",
603
+ "com.au",
604
+ "net.au",
605
+ "org.au",
606
+ "co.nz",
607
+ "co.jp",
608
+ "ne.jp",
609
+ "or.jp",
610
+ "com.br",
611
+ "com.mx",
612
+ "com.ar",
613
+ "com.tr",
614
+ "co.za",
615
+ "com.sg",
616
+ "com.hk",
617
+ "com.tw",
618
+ "co.kr",
619
+ "co.in",
620
+ "co.il",
621
+ "com.cn",
622
+ "com.pl",
623
+ "co.at",
624
+ "or.at",
625
+ "gv.at",
626
+ "com.ua",
627
+ "com.es",
628
+ "com.pt",
629
+ "com.ph",
630
+ "com.my",
631
+ "co.id",
632
+ "co.th",
633
+ "com.vn",
634
+ "com.sa",
635
+ "com.eg",
636
+ "com.co",
637
+ "com.pe",
638
+ "com.cl"
639
+ ]);
640
+ function registrableDomain(host) {
641
+ const h = host.toLowerCase().replace(/\.$/, "");
642
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(h) || !h.includes(".")) return h;
643
+ const parts = h.split(".");
644
+ const last2 = parts.slice(-2).join(".");
645
+ if (parts.length >= 3 && MULTI_PART_TLDS.has(last2)) return parts.slice(-3).join(".");
646
+ return last2;
647
+ }
648
+ function validateTrace(t) {
649
+ const p = [];
650
+ const err = (path, message) => p.push({ path, message });
651
+ if (!t || typeof t !== "object") return [{ path: "/", message: "trace must be an object" }];
652
+ const x = t;
653
+ if (x.format !== TRACE_FORMAT) err("/format", `must be "${TRACE_FORMAT}"`);
654
+ if (typeof x.task !== "string" || !x.task.trim() || x.task.length > 300) err("/task", "a task of 1\u2013300 characters is required");
655
+ try {
656
+ if (!x.site || new URL(x.site.origin).origin !== x.site.origin || !x.site.origin.startsWith("https://")) err("/site/origin", "must be an https origin");
657
+ } catch {
658
+ err("/site/origin", "must be an https origin");
659
+ }
660
+ if (!Array.isArray(x.inputs)) err("/inputs", "must be an array");
661
+ if (!Array.isArray(x.calls) || x.calls.length === 0) err("/calls", "at least one call is required");
662
+ else if (x.calls.length > 200) err("/calls", "at most 200 calls");
663
+ const size = JSON.stringify(t).length;
664
+ if (size > 15e5) err("/", `trace is ${Math.round(size / 1024)} KB; the limit is 1.5 MB`);
665
+ const walk = (v, path, key) => {
666
+ if (p.length > 20) return;
667
+ if (typeof v === "string") {
668
+ if (v.startsWith("\xAB") || /\{\{in\d+\}\}/.test(v)) return;
669
+ if (/^(Bearer|Basic)\s+[A-Za-z0-9._~+/-]{12,}/.test(v)) err(path, "an Authorization value was not redacted");
670
+ else if (key !== "html" && /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\./.test(v)) err(path, "a JWT was not redacted");
671
+ else if (SECRET_KEY.test(key) && looksLikeToken(v) && !/^sha256Hash$/i.test(key)) err(path, `value under "${key}" looks like an unredacted secret`);
672
+ return;
673
+ }
674
+ if (Array.isArray(v)) {
675
+ if (v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string" && /^(cookie|cookie2|authorization|proxy-authorization)$/i.test(v[0]) && !/«secret[^»]*»/.test(v[1])) err(path, `${v[0]} header value must be redacted`);
676
+ v.forEach((e, i) => walk(e, `${path}/${i}`, Array.isArray(e) ? key : typeof v[0] === "string" && i === 1 ? String(v[0]) : key));
677
+ return;
678
+ }
679
+ if (v && typeof v === "object") for (const [k, val] of Object.entries(v)) walk(val, `${path}/${k}`, k);
680
+ };
681
+ walk(t, "", "");
682
+ return p;
683
+ }
684
+
685
+ // apps/extension/src/sw/sites.ts
686
+ var PAUSED = "paused_sites";
687
+ async function pausedHosts() {
688
+ return (await chrome.storage.local.get(PAUSED))[PAUSED] ?? [];
689
+ }
690
+ async function setPaused(host, paused) {
691
+ const list = new Set(await pausedHosts());
692
+ if (paused) list.add(host);
693
+ else list.delete(host);
694
+ await chrome.storage.local.set({ [PAUSED]: [...list] });
695
+ }
696
+ async function isPaused(host) {
697
+ return (await pausedHosts()).includes(host);
698
+ }
699
+ var originOf = (pattern) => pattern.replace(/\/\*$/, "");
700
+ function patternCovers(pattern, origin) {
701
+ const m = /^(https?|\*):\/\/([^/]+)\/\*$/.exec(pattern);
702
+ if (!m) return pattern === "<all_urls>";
703
+ const u = new URL(origin);
704
+ if (m[1] !== "*" && `${m[1]}:` !== u.protocol) return false;
705
+ const host = m[2];
706
+ if (host === "*") return true;
707
+ if (host.startsWith("*.")) return u.hostname === host.slice(2) || u.hostname.endsWith(host.slice(1));
708
+ return u.hostname === host;
709
+ }
710
+ async function sitesOverview(runs, states) {
711
+ const manifestPatterns = chrome.runtime.getManifest().host_permissions ?? [];
712
+ const builtIn = new Set(manifestPatterns.filter((p) => /^https:\/\/[a-z0-9.-]+\/\*$/.test(p)).map(originOf));
713
+ const grantedPatterns = ((await chrome.permissions.getAll()).origins ?? []).filter((p) => !manifestPatterns.includes(p));
714
+ const granted = new Set(grantedPatterns.map(originOf).filter((o) => /^https:\/\/[a-z0-9.-]+$/.test(o)));
715
+ const accessOf = (origin) => manifestPatterns.some((p) => patternCovers(p, origin)) ? "built-in" : grantedPatterns.some((p) => patternCovers(p, origin)) ? "granted" : "none";
716
+ const paused = new Set(await pausedHosts());
717
+ const pace = await paceSnapshot();
718
+ const policy = await effectivePolicy();
719
+ const rows = /* @__PURE__ */ new Map();
720
+ const row = (origin) => {
721
+ let r = rows.get(origin);
722
+ if (!r) {
723
+ const host = new URL(origin).hostname;
724
+ r = {
725
+ origin,
726
+ host,
727
+ access: accessOf(origin),
728
+ paused: paused.has(host),
729
+ policy_blocked_by: blockedBy(policy, host),
730
+ logged_in: null,
731
+ tab_open: false,
732
+ last_run: null,
733
+ pace: pace[siteKey(origin)] ?? null,
734
+ active: false
735
+ };
736
+ rows.set(origin, r);
737
+ }
738
+ return r;
739
+ };
740
+ for (const o of builtIn) row(o);
741
+ for (const o of granted) row(o).active = true;
742
+ for (const s of states) {
743
+ if (!s.origin) continue;
744
+ const r = row(s.origin);
745
+ r.logged_in = s.logged_in;
746
+ r.tab_open = s.tab_open;
747
+ r.active = true;
748
+ }
749
+ for (const run of runs) {
750
+ const origin = run.origin ?? run.requests[0]?.origin;
751
+ if (run.kind !== "run" || !origin) continue;
752
+ const r = row(origin);
753
+ r.active = true;
754
+ if (!r.last_run) r.last_run = { ok: run.ok, ...run.error_code ? { error_code: run.error_code } : {}, at: run.started_at, recipe_id: run.recipe_id };
755
+ }
756
+ for (const h of paused) {
757
+ const existing = [...rows.values()].find((x) => x.host === h);
758
+ if (existing) existing.active = true;
759
+ else row(`https://${h}`).active = true;
760
+ }
761
+ return [...rows.values()].sort((a, b) => Number(b.active) - Number(a.active) || a.host.replace(/^www\./, "").localeCompare(b.host.replace(/^www\./, "")));
762
+ }
763
+
764
+ // apps/extension/src/sw/tabs.ts
765
+ var OWNED_KEY = "ownedTabs";
766
+ async function ownedTabs() {
767
+ const r = await chrome.storage.session.get(OWNED_KEY);
768
+ return new Set(r[OWNED_KEY] ?? []);
769
+ }
770
+ async function setOwned(ids) {
771
+ await chrome.storage.session.set({ [OWNED_KEY]: [...ids] });
772
+ }
773
+ chrome.tabs.onRemoved.addListener(async (tabId) => {
774
+ const owned = await ownedTabs();
775
+ if (owned.delete(tabId)) await setOwned(owned);
776
+ unmarkStale(tabId);
777
+ });
778
+ var STALE_KEY = "staleTabs";
779
+ async function staleTabs() {
780
+ const r = await chrome.storage.session.get(STALE_KEY);
781
+ return new Set(r[STALE_KEY] ?? []);
782
+ }
783
+ async function markStale(tabId) {
784
+ const s = await staleTabs();
785
+ s.add(tabId);
786
+ await chrome.storage.session.set({ [STALE_KEY]: [...s] });
787
+ }
788
+ function unmarkStale(tabId) {
789
+ void staleTabs().then(async (s) => {
790
+ if (s.delete(tabId)) await chrome.storage.session.set({ [STALE_KEY]: [...s] });
791
+ });
792
+ }
793
+ chrome.tabs.onUpdated.addListener((tabId, info) => {
794
+ if (info.status === "loading") unmarkStale(tabId);
795
+ });
796
+ function originPattern(origin) {
797
+ return `${origin}/*`;
798
+ }
799
+ function siteOrigins(origin, localeHosts = []) {
800
+ const port = new URL(origin).port;
801
+ return [origin, ...localeHosts.map((h) => `https://${h}${port ? `:${port}` : ""}`)];
802
+ }
803
+ async function tabsForOrigin(origin, localeHosts = []) {
804
+ const origins = siteOrigins(origin, localeHosts);
805
+ const tabs = await chrome.tabs.query({ url: origins.map(originPattern) });
806
+ return tabs.filter((t) => t.id !== void 0 && t.url && origins.includes(new URL(t.url).origin));
807
+ }
808
+ function waitComplete(tabId, timeoutMs) {
809
+ return new Promise((resolve, reject) => {
810
+ let settled = false;
811
+ const finish2 = (fn) => {
812
+ if (settled) return;
813
+ settled = true;
814
+ clearTimeout(timer);
815
+ chrome.tabs.onUpdated.removeListener(onUpdated);
816
+ chrome.tabs.onRemoved.removeListener(onRemoved);
817
+ fn();
818
+ };
819
+ const onUpdated = (id, info, tab) => {
820
+ if (id === tabId && info.status === "complete") finish2(() => resolve(tab));
821
+ };
822
+ const onRemoved = (id) => {
823
+ if (id === tabId) finish2(() => reject(new ExecError("tab_error", "The site tab was closed while loading")));
824
+ };
825
+ const timer = setTimeout(
826
+ () => finish2(() => reject(new ExecError("tab_error", `The site tab did not finish loading within ${timeoutMs / 1e3} s`))),
827
+ timeoutMs
828
+ );
829
+ chrome.tabs.onUpdated.addListener(onUpdated);
830
+ chrome.tabs.onRemoved.addListener(onRemoved);
831
+ chrome.tabs.get(tabId).then(
832
+ (t) => t.status === "complete" && finish2(() => resolve(t)),
833
+ () => finish2(() => reject(new ExecError("tab_error", "The site tab is gone")))
834
+ );
835
+ });
836
+ }
837
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
838
+ function startUrl(origin, startPath) {
839
+ if (!startPath) return `${origin}/`;
840
+ if (!startPath.startsWith("/") || startPath.startsWith("//")) throw new ExecError("recipe_invalid", 'site.start_path must be a path starting with "/"');
841
+ const u = new URL(startPath, origin);
842
+ if (u.origin !== origin) throw new ExecError("recipe_invalid", "site.start_path must stay on site.origin");
843
+ return u.toString();
844
+ }
845
+ async function acquireTab(origin, signal, startPath, opts = {}) {
846
+ const url = startUrl(origin, startPath);
847
+ const owned = await ownedTabs();
848
+ const stale = await staleTabs();
849
+ const accepted = siteOrigins(origin, opts.localeHosts);
850
+ const candidates = (await tabsForOrigin(origin, opts.localeHosts)).filter((t) => !t.discarded && !stale.has(t.id)).sort((a, b) => score(b, owned) - score(a, owned));
851
+ const pick = opts.forceNew ? void 0 : candidates[0];
852
+ if (pick?.id !== void 0) {
853
+ if (pick.status !== "complete") await waitComplete(pick.id, LIMITS2.tabLoadMs);
854
+ return { tabId: pick.id, opened: false, owned: owned.has(pick.id), ...pick.url ? { host: new URL(pick.url).host } : {} };
855
+ }
856
+ let tab;
857
+ try {
858
+ tab = await chrome.tabs.create({ url, active: false });
859
+ } catch {
860
+ const win = await chrome.windows.create({ url, focused: false });
861
+ tab = win.tabs[0];
862
+ }
863
+ const tabId = tab.id;
864
+ owned.add(tabId);
865
+ await setOwned(owned);
866
+ chrome.tabs.update(tabId, { autoDiscardable: false }).catch(() => {
867
+ });
868
+ const done = await waitComplete(tabId, LIMITS2.tabLoadMs);
869
+ const finalOrigin = done.url ? new URL(done.url).origin : "";
870
+ if (!accepted.includes(finalOrigin)) {
871
+ throw new ExecError("tab_error", `${origin} redirected the tab to ${finalOrigin || "a site Wield has no access to"}; the recipe's site.origin needs updating`);
872
+ }
873
+ if (!signal.aborted) await sleep2(LIMITS2.tabSettleMs);
874
+ return { tabId, opened: true, owned: true, ...done.url ? { host: new URL(done.url).host } : {} };
875
+ }
876
+ function score(t, owned) {
877
+ let s = 0;
878
+ if (t.status === "complete") s += 4;
879
+ if (!owned.has(t.id)) s += 2;
880
+ s += (t.lastAccessed ?? 0) / 1e13;
881
+ return s;
882
+ }
883
+ var MAX_WARM_TABS = 4;
884
+ var WARM_KEY = "warmTabs";
885
+ async function warmTab(origin, startPath) {
886
+ const open = (await tabsForOrigin(origin)).filter((t) => !t.discarded);
887
+ if (open.length) return "open";
888
+ const warmed = new Set((await chrome.storage.session.get(WARM_KEY))[WARM_KEY] ?? []);
889
+ for (const id of [...warmed]) await chrome.tabs.get(id).catch(() => warmed.delete(id));
890
+ if (warmed.size >= MAX_WARM_TABS) return "skipped";
891
+ const owned = await ownedTabs();
892
+ let tab;
893
+ try {
894
+ tab = await chrome.tabs.create({ url: startUrl(origin, startPath), active: false });
895
+ } catch {
896
+ return "skipped";
897
+ }
898
+ owned.add(tab.id);
899
+ await setOwned(owned);
900
+ warmed.add(tab.id);
901
+ await chrome.storage.session.set({ [WARM_KEY]: [...warmed] });
902
+ chrome.tabs.update(tab.id, { autoDiscardable: false }).catch(() => {
903
+ });
904
+ return "opened";
905
+ }
906
+
907
+ // apps/extension/src/sw/runs.ts
908
+ var ORIGIN_RE = /^https:\/\/[^/]+$/;
909
+ var FORBIDDEN_HEADERS = /* @__PURE__ */ new Set(["cookie", "cookie2", "host", "set-cookie"]);
910
+ function isAllowedOrigin(o) {
911
+ return typeof o === "string" && ORIGIN_RE.test(o);
912
+ }
913
+ function checkHeaders(s) {
914
+ for (const h of Object.keys(s.request?.headers ?? {})) {
915
+ if (FORBIDDEN_HEADERS.has(h.toLowerCase())) throw new ExecError("recipe_invalid", `step ${s.id} sets the ${h} header; the browser sends cookies itself`, s.id);
916
+ }
917
+ }
918
+ function post(port, m) {
919
+ try {
920
+ port?.postMessage(m);
921
+ } catch {
922
+ }
923
+ }
924
+ function checkRecipe(r) {
925
+ const bad = (m) => {
926
+ throw new ExecError("recipe_invalid", m);
927
+ };
928
+ const recipe = r;
929
+ if (!recipe || typeof recipe !== "object") bad("recipe missing");
930
+ if (typeof recipe.id !== "string" || typeof recipe.version !== "number") bad("recipe.id/version missing");
931
+ const site = recipe.site;
932
+ if (!site || !isAllowedOrigin(site.origin)) bad("site.origin must be https://host");
933
+ if (!Array.isArray(site.origins) || site.origins.length === 0 || !site.origins.every(isAllowedOrigin)) bad("site.origins must list https origins");
934
+ if (site.locale_hosts !== void 0) {
935
+ if (!Array.isArray(site.locale_hosts) || site.locale_hosts.length > 20) bad("site.locale_hosts must be a list of up to 20 host names");
936
+ for (const h of site.locale_hosts) {
937
+ if (typeof h !== "string" || !/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(h) || !(h === site.id || h.endsWith(`.${site.id}`))) bad(`site.locale_hosts: "${String(h)}" must be an exact host name under ${site.id}`);
938
+ }
939
+ }
940
+ if (!Array.isArray(recipe.steps) || recipe.steps.length < 1) bad("recipe has no steps");
941
+ if (recipe.steps.length > LIMITS2.steps) throw new ExecError("limit_exceeded", `recipe has ${recipe.steps.length} steps; the limit is ${LIMITS2.steps}`);
942
+ for (const s of [...recipe.steps, ...recipe.login_check ? [recipe.login_check] : []]) {
943
+ if (!s?.request?.url || !s.request.method) bad(`step ${s?.id ?? "?"} has no request`);
944
+ if ((s.foreach?.max ?? 0) > LIMITS2.foreach) throw new ExecError("limit_exceeded", `step ${s.id}: foreach.max is above ${LIMITS2.foreach}`, s.id);
945
+ checkHeaders(s);
946
+ }
947
+ return recipe;
948
+ }
949
+ var Budget = class {
950
+ constructor(ms, onExpire) {
951
+ this.onExpire = onExpire;
952
+ this.remaining = ms;
953
+ this.resume();
954
+ }
955
+ remaining;
956
+ startedAt = 0;
957
+ timer;
958
+ pause() {
959
+ if (!this.timer) return;
960
+ clearTimeout(this.timer);
961
+ this.timer = void 0;
962
+ this.remaining -= Date.now() - this.startedAt;
963
+ }
964
+ resume() {
965
+ if (this.timer) return;
966
+ this.startedAt = Date.now();
967
+ this.timer = setTimeout(this.onExpire, Math.max(0, this.remaining));
968
+ }
969
+ stop() {
970
+ clearTimeout(this.timer);
971
+ this.timer = void 0;
972
+ }
973
+ left() {
974
+ return Math.max(1, this.remaining - (this.timer ? Date.now() - this.startedAt : 0));
975
+ }
976
+ };
977
+ var active = /* @__PURE__ */ new Map();
978
+ var runAborts = /* @__PURE__ */ new Map();
979
+ function cancelRun(run_id) {
980
+ runAborts.get(run_id)?.abort();
981
+ const run = active.get(run_id);
982
+ if (!run) return runAborts.has(run_id);
983
+ post(run.port, { type: "cancel" });
984
+ run.finish({ ok: false, error: { code: "cancelled", message: "Cancelled by the client" } });
985
+ return true;
986
+ }
987
+ function activeRuns() {
988
+ return active.size;
989
+ }
990
+ var newerBuild = null;
991
+ async function buildOnDisk() {
992
+ try {
993
+ const r = await fetch(chrome.runtime.getURL("build-id.json"), { cache: "no-store" });
994
+ return (await r.json()).build ?? null;
995
+ } catch {
996
+ return null;
997
+ }
998
+ }
999
+ async function checkForNewBuild() {
1000
+ const disk = await buildOnDisk();
1001
+ const changed = !!disk && disk !== BUILD_ID;
1002
+ if (changed !== (newerBuild !== null)) {
1003
+ chrome.action.setBadgeText({ text: changed ? "\u21BB" : "" }).catch(() => {
1004
+ });
1005
+ if (changed) chrome.action.setBadgeBackgroundColor({ color: "#121311" }).catch(() => {
1006
+ });
1007
+ chrome.action.setTitle({ title: changed ? "Wield was rebuilt: reload it (open this popup)" : "Wield" }).catch(() => {
1008
+ });
1009
+ }
1010
+ newerBuild = changed ? disk : null;
1011
+ return changed;
1012
+ }
1013
+ function pendingBuild() {
1014
+ return newerBuild;
1015
+ }
1016
+ var OUTCOME_MSG = {
1017
+ allow: "allowed by the user",
1018
+ deny: "denied by the user",
1019
+ closed: "denied: the window was closed",
1020
+ timeout: `denied: no answer in ${Math.round(LIMITS2.confirmMs / 1e3)} s`,
1021
+ cancelled: "cancelled before an answer"
1022
+ };
1023
+ var DENIED_ERROR = {
1024
+ deny: "The user clicked Deny in the browser. Nothing was sent.",
1025
+ closed: "The user closed the Allow window. Nothing was sent.",
1026
+ timeout: `No answer in the Allow window within ${Math.round(LIMITS2.confirmMs / 1e3)} s. Nothing was sent; ask the user to try again while at the computer.`,
1027
+ cancelled: "The run was cancelled before the user answered. Nothing was sent."
1028
+ };
1029
+ async function ensureHostAccess(recipe, run_id, budget, signal, emit) {
1030
+ const pattern = originPattern(recipe.site.origin);
1031
+ if (await chrome.permissions.contains({ origins: [pattern] })) return;
1032
+ emit({ level: "info", kind: "confirm", msg: `asking for access to ${new URL(recipe.site.origin).host}` });
1033
+ budget?.pause();
1034
+ const outcome = await askUser({ kind: "permission", run_id, site: { id: recipe.site.id, name: recipe.site.name, origin: recipe.site.origin }, task: recipe.task }, signal);
1035
+ budget?.resume();
1036
+ emit({ level: outcome === "allow" ? "info" : "warn", kind: "confirm", msg: outcome === "allow" ? `access to ${new URL(recipe.site.origin).host} granted by the user` : `site access ${OUTCOME_MSG[outcome]}` });
1037
+ if (outcome !== "allow" || !await chrome.permissions.contains({ origins: [pattern] })) {
1038
+ throw new ExecError("origin_blocked", `Access to ${recipe.site.origin} was not granted in the browser (${OUTCOME_MSG[outcome]})`);
1039
+ }
1040
+ }
1041
+ var INJECT_TIMEOUT_MS = 5e3;
1042
+ var ACK_TIMEOUT_MS = 2e3;
1043
+ async function inject(tabId) {
1044
+ for (let attempt = 0; ; attempt++) {
1045
+ try {
1046
+ await Promise.race([
1047
+ chrome.scripting.executeScript({ target: { tabId }, files: ["content.js"] }),
1048
+ new Promise((_, reject) => setTimeout(() => reject(new UnresponsiveTab(`The site tab did not take Wield's script within ${INJECT_TIMEOUT_MS / 1e3} s`)), INJECT_TIMEOUT_MS))
1049
+ ]);
1050
+ return;
1051
+ } catch (e) {
1052
+ if (e instanceof UnresponsiveTab) throw e;
1053
+ const msg = e.message ?? String(e);
1054
+ if (attempt === 0 && /Could not load file/i.test(msg)) {
1055
+ await new Promise((r) => setTimeout(r, 300));
1056
+ continue;
1057
+ }
1058
+ throw new ExecError("tab_error", `Could not run in the site tab: ${msg}`);
1059
+ }
1060
+ }
1061
+ }
1062
+ function execute(opts) {
1063
+ const abort = new AbortController();
1064
+ let resolveDone;
1065
+ const done = new Promise((r) => resolveDone = r);
1066
+ let finished = false;
1067
+ const budget = new Budget(opts.budgetMs, () => {
1068
+ post(run.port, { type: "cancel" });
1069
+ run.finish({ ok: false, error: { code: "timeout", message: `Run exceeded ${Math.round(opts.budgetMs / 1e3)} s` } });
1070
+ });
1071
+ const run = {
1072
+ run_id: opts.run_id,
1073
+ abort,
1074
+ finish: (r) => {
1075
+ if (finished) return;
1076
+ finished = true;
1077
+ budget.stop();
1078
+ abort.abort();
1079
+ active.delete(opts.run_id);
1080
+ try {
1081
+ run.port?.disconnect();
1082
+ } catch {
1083
+ }
1084
+ resolveDone(r);
1085
+ }
1086
+ };
1087
+ active.set(opts.run_id, run);
1088
+ let deniedBy;
1089
+ (async () => {
1090
+ const { recipe } = opts;
1091
+ let tabId = opts.tabId;
1092
+ if (tabId === void 0) {
1093
+ await ensureHostAccess(recipe, opts.run_id, budget, abort.signal, opts.emit);
1094
+ const t0 = Date.now();
1095
+ const tab = await acquireTab(recipe.site.origin, abort.signal, recipe.site.start_path, { forceNew: opts.forceNewTab, localeHosts: recipe.site.locale_hosts ?? [] });
1096
+ tabId = tab.tabId;
1097
+ if (opts.meta) {
1098
+ opts.meta.owned = tab.owned;
1099
+ if (tab.opened) opts.meta.openedTabId = tab.tabId;
1100
+ }
1101
+ const host = tab.host ?? new URL(recipe.site.origin).host;
1102
+ opts.emit({ level: "info", kind: "tab", msg: tab.opened ? `opened background tab ${host} (${Date.now() - t0} ms)` : `using open tab ${host}` });
1103
+ }
1104
+ if (opts.meta) opts.meta.tabId = tabId;
1105
+ if (finished) return;
1106
+ await inject(tabId);
1107
+ if (finished) return;
1108
+ const port = chrome.tabs.connect(tabId, { name: `wield-run:${opts.run_id}` });
1109
+ run.port = port;
1110
+ port.onDisconnect.addListener(() => {
1111
+ run.finish({ ok: false, error: { code: "tab_error", message: "The site tab closed or navigated away during the run" } });
1112
+ });
1113
+ let acked = false;
1114
+ port.onMessage.addListener(async (m) => {
1115
+ acked = true;
1116
+ if (finished) return;
1117
+ if (m.type === "ack") return;
1118
+ if (m.type === "event") {
1119
+ if (m.kind === "done" || m.kind === "confirm") return;
1120
+ opts.emit({ level: m.level, kind: m.kind, msg: m.msg, data: m.data });
1121
+ } else if (m.type === "confirm") {
1122
+ const n = m.info.writes.length;
1123
+ opts.emit({ level: "info", kind: "confirm", msg: `waiting for Allow in the browser: ${n} write request${n === 1 ? "" : "s"}` });
1124
+ budget.pause();
1125
+ const outcome = await askUser(
1126
+ { kind: "write", run_id: opts.run_id, site: { id: recipe.site.id, name: recipe.site.name, origin: recipe.site.origin }, task: recipe.task, info: m.info },
1127
+ abort.signal
1128
+ );
1129
+ budget.resume();
1130
+ if (outcome !== "allow") deniedBy = outcome;
1131
+ opts.emit({ level: outcome === "allow" ? "info" : "warn", kind: "confirm", msg: OUTCOME_MSG[outcome] });
1132
+ post(port, { type: "confirm_result", id: m.id, allow: outcome === "allow" });
1133
+ } else if (m.type === "pace") {
1134
+ const r = await acquire(m.origin);
1135
+ if (r.ok && r.waited_ms >= 250) opts.emit({ level: "info", kind: "step", msg: `pacing: waited ${(r.waited_ms / 1e3).toFixed(1)} s before the next ${new URL(m.origin).host} request` });
1136
+ if (!r.ok) opts.emit({ level: "warn", kind: "step", msg: r.message });
1137
+ post(port, { type: "pace_result", id: m.id, ok: r.ok, ...r.ok ? { waited_ms: r.waited_ms } : { message: r.message } });
1138
+ } else if (m.type === "result") {
1139
+ if (!m.ok && m.error.code === "confirmation_denied" && deniedBy) m.error.message = DENIED_ERROR[deniedBy];
1140
+ run.finish(m.ok ? { ok: true, output: m.output } : { ok: false, error: m.error, ...m.challenge ? { challenge: m.challenge } : {} });
1141
+ }
1142
+ });
1143
+ post(port, { type: "run", run_id: opts.run_id, recipe, params: opts.params, mode: opts.mode, timeout_ms: budget.left(), build: BUILD_ID, ...opts.trust === "registry" ? { trust: "registry" } : {} });
1144
+ setTimeout(() => {
1145
+ if (acked || finished) return;
1146
+ try {
1147
+ port.disconnect();
1148
+ } catch {
1149
+ }
1150
+ run.finish({ ok: false, unresponsive: true, error: { code: "tab_error", message: `Wield's script in the ${new URL(recipe.site.origin).host} tab did not answer within ${ACK_TIMEOUT_MS / 1e3} s` } });
1151
+ }, ACK_TIMEOUT_MS);
1152
+ })().catch((e) => run.finish({ ok: false, error: toRunError(e), ...e instanceof UnresponsiveTab ? { unresponsive: true } : {} }));
1153
+ return { done, active: run };
1154
+ }
1155
+ var EVENT_FIELDS = ["method", "origin", "path", "status", "ms", "bytes"];
1156
+ function sanitize(e) {
1157
+ const out = { level: e.level, kind: e.kind, msg: String(e.msg).slice(0, 300) };
1158
+ if (e.data) {
1159
+ const d = {};
1160
+ for (const k of EVENT_FIELDS) if (e.data[k] !== void 0) d[k] = e.data[k];
1161
+ out.data = d;
1162
+ }
1163
+ return out;
1164
+ }
1165
+ async function recoverUnresponsive(recipe, meta, attempt, emit) {
1166
+ const tabId = meta.tabId;
1167
+ const host = new URL(recipe.site.origin).host;
1168
+ if (meta.owned) {
1169
+ emit({ level: "warn", kind: "tab", msg: `Wield's script in its own ${host} tab did not answer (the extension was probably reloaded since); reloading that tab and retrying` });
1170
+ try {
1171
+ await chrome.tabs.reload(tabId);
1172
+ await new Promise((r) => setTimeout(r, 300));
1173
+ await waitComplete(tabId, LIMITS2.tabLoadMs);
1174
+ await new Promise((r) => setTimeout(r, LIMITS2.tabSettleMs));
1175
+ } catch {
1176
+ return attempt(void 0, true);
1177
+ }
1178
+ return attempt(tabId);
1179
+ }
1180
+ await markStale(tabId);
1181
+ emit({ level: "warn", kind: "tab", msg: `Wield's script in your ${host} tab did not answer (the extension was probably reloaded since that tab opened); running in a fresh background tab instead. Reload your tab to use it again.` });
1182
+ return attempt(void 0, true);
1183
+ }
1184
+ var CANCELLED = { ok: false, error: { code: "cancelled", message: "Cancelled by the client" } };
1185
+ function isWrite(recipe) {
1186
+ return recipe.effect === "write" || recipe.steps.some((s) => s.write);
1187
+ }
1188
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1189
+ var nonGet = (s) => !SAFE_METHODS.has(String(s.request.method).toUpperCase());
1190
+ function hardenUnsigned(r, emit) {
1191
+ if (r.login_check && nonGet(r.login_check)) throw new ExecError("recipe_invalid", "login_check must be a GET in a recipe the registry did not sign. Nothing was sent.", r.login_check.id);
1192
+ const marked = r.steps.filter((s) => nonGet(s) && !s.write);
1193
+ if (!marked.length) return r;
1194
+ emit?.({ level: "warn", kind: "step", msg: `not signed by the registry: ${marked.map((s) => `${String(s.request.method).toUpperCase()} step ${s.id}`).join(", ")} treated as a change (Allow first)` });
1195
+ return { ...r, effect: "write", steps: r.steps.map((s) => nonGet(s) && !s.write ? { ...s, write: true } : s) };
1196
+ }
1197
+ async function askDraft(r, run_id, signal) {
1198
+ const domain = registrableDomain(new URL(r.site.origin).hostname);
1199
+ const origins = [.../* @__PURE__ */ new Set([r.site.origin, ...r.site.origins ?? []])].map((o) => ({ origin: o, foreign: registrableDomain(new URL(o).hostname) !== domain }));
1200
+ const steps = [...r.login_check ? [r.login_check] : [], ...r.steps].map((s) => ({ method: String(s.request.method).toUpperCase(), url: String(s.request.url).slice(0, 300) }));
1201
+ const host = new URL(r.site.origin).hostname;
1202
+ const outcome = await askUser({ kind: "draft", run_id, site: { id: r.site.id ?? host, name: r.site.name ?? host, origin: r.site.origin }, task: r.task ?? r.id, draft: { domain, origins, steps } }, signal);
1203
+ if (outcome !== "allow") throw new ExecError("confirmation_denied", `The new recipe was not tested: ${outcome === "deny" ? "you denied it" : outcome === "timeout" ? "no answer in time" : "the window was closed"}. Nothing was sent.`);
1204
+ }
1205
+ function sleep3(ms, signal) {
1206
+ return new Promise((resolve) => {
1207
+ const t = setTimeout(resolve, ms);
1208
+ signal?.addEventListener("abort", () => (clearTimeout(t), resolve()), { once: true });
1209
+ });
1210
+ }
1211
+ var RELOAD_SETTLE_MS = 3e3;
1212
+ var HUMAN_SETTLE_MS = 2e3;
1213
+ var CHECK_EVENT = {
1214
+ interacted: "the user touched the page; retrying once",
1215
+ dismissed: 'the user chose "Not now" on the check',
1216
+ timeout: `no one touched the page within ${Math.round(LIMITS2.humanCheckMs / 1e3)} s`,
1217
+ closed: "the site tab was closed during the check",
1218
+ cancelled: "check cancelled"
1219
+ };
1220
+ async function humanCheck(tabId, siteName, host, signal) {
1221
+ try {
1222
+ const tab = await chrome.tabs.update(tabId, { active: true });
1223
+ if (tab?.windowId !== void 0) await chrome.windows.update(tab.windowId, { focused: true, drawAttention: true }).catch(() => {
1224
+ });
1225
+ await inject(tabId);
1226
+ } catch {
1227
+ return "closed";
1228
+ }
1229
+ const port = chrome.tabs.connect(tabId, { name: `wield-check:${crypto.randomUUID()}` });
1230
+ return new Promise((resolve) => {
1231
+ let settled = false;
1232
+ const finish2 = (o) => {
1233
+ if (settled) return;
1234
+ settled = true;
1235
+ clearTimeout(timer);
1236
+ try {
1237
+ port.postMessage({ type: "hide" });
1238
+ port.disconnect();
1239
+ } catch {
1240
+ }
1241
+ resolve(o);
1242
+ };
1243
+ const timer = setTimeout(() => finish2("timeout"), LIMITS2.humanCheckMs);
1244
+ signal.addEventListener("abort", () => finish2("cancelled"), { once: true });
1245
+ port.onMessage.addListener((m) => finish2(m.type === "interacted" ? "interacted" : "dismissed"));
1246
+ port.onDisconnect.addListener(() => {
1247
+ chrome.tabs.get(tabId).then(() => finish2("interacted"), () => finish2("closed"));
1248
+ });
1249
+ port.postMessage({ type: "show", host, site_name: siteName, timeout_ms: LIMITS2.humanCheckMs });
1250
+ });
1251
+ }
1252
+ async function recoverFromChallenge(c) {
1253
+ const first = c.first;
1254
+ const ch = first.challenge;
1255
+ const host = ch?.host ?? new URL(c.recipe.site.origin).host;
1256
+ const tabId = c.meta.tabId;
1257
+ const fail = (why) => ({
1258
+ ok: false,
1259
+ error: {
1260
+ code: "site_challenge",
1261
+ message: `${host} answered with a bot check${ch ? ` (${ch.vendor}, HTTP ${ch.status})` : ""} instead of data. ${why}`,
1262
+ ...first.error.step ? { step: first.error.step } : {}
1263
+ }
1264
+ });
1265
+ const askUserHint = `Ask the user to open ${host} in their browser, click once on the page, then try again.`;
1266
+ if (isWrite(c.recipe)) return fail(`Nothing was changed. ${askUserHint}`);
1267
+ if (ch?.status === 429 || /HTTP 429/.test(first.error.message)) {
1268
+ return { ok: false, error: { code: "site_challenge", message: `${host} is rate-limiting requests (HTTP 429). Wait a few minutes before trying again.`, ...first.error.step ? { step: first.error.step } : {} } };
1269
+ }
1270
+ if (c.meta.owned && c.left() > RELOAD_SETTLE_MS + 1e4) {
1271
+ if (c.meta.openedTabId === tabId) {
1272
+ c.emit({ level: "warn", kind: "tab", msg: `the ${host} tab was just opened; waiting ${COLD_RETRY_WAIT_MS / 1e3} s for it to settle, then retrying once` });
1273
+ await sleep3(COLD_RETRY_WAIT_MS, c.signal);
1274
+ } else {
1275
+ c.emit({ level: "warn", kind: "tab", msg: `reloading the ${host} tab Wield opened, waiting ${RELOAD_SETTLE_MS / 1e3} s, then retrying once` });
1276
+ try {
1277
+ await chrome.tabs.reload(tabId);
1278
+ await new Promise((r3) => setTimeout(r3, 300));
1279
+ await waitComplete(tabId, LIMITS2.tabLoadMs);
1280
+ } catch {
1281
+ return fail(askUserHint);
1282
+ }
1283
+ await sleep3(RELOAD_SETTLE_MS, c.signal);
1284
+ }
1285
+ if (c.signal.aborted) return CANCELLED;
1286
+ const r2 = await c.attempt(tabId);
1287
+ if (r2.ok || r2.error.code !== "site_challenge") return r2;
1288
+ }
1289
+ if (c.left() < 1e4) return fail(askUserHint);
1290
+ c.emit({ level: "warn", kind: "confirm", msg: `asking the user for a quick check in the ${host} tab (up to ${Math.round(LIMITS2.humanCheckMs / 1e3)} s)` });
1291
+ const t = Date.now();
1292
+ const outcome = await humanCheck(tabId, c.recipe.site.name, host, c.signal);
1293
+ c.paused(Date.now() - t);
1294
+ c.emit({ level: outcome === "interacted" ? "info" : "warn", kind: "confirm", msg: CHECK_EVENT[outcome] });
1295
+ if (outcome === "cancelled") return CANCELLED;
1296
+ if (outcome === "dismissed") return fail(`The user chose "Not now". ${askUserHint}`);
1297
+ if (outcome === "timeout") return fail(`Wield showed a note in the ${host} tab for ${Math.round(LIMITS2.humanCheckMs / 1e3)} s, but no one touched the page. ${askUserHint}`);
1298
+ if (outcome === "closed") return fail(`The ${host} tab was closed during the check.`);
1299
+ await sleep3(HUMAN_SETTLE_MS, c.signal);
1300
+ if (c.signal.aborted) return CANCELLED;
1301
+ try {
1302
+ await waitComplete(tabId, LIMITS2.tabLoadMs);
1303
+ } catch {
1304
+ return fail(`The ${host} tab was closed.`);
1305
+ }
1306
+ const r = await c.attempt(tabId);
1307
+ if (!r.ok && r.error.code === "site_challenge") return fail("It was still blocked after the page was touched. Wait several minutes before trying again: repeated tries make it worse.");
1308
+ return r;
1309
+ }
1310
+ var COLD_RETRY_WAIT_MS = 4e3;
1311
+ function shouldRetryCold(recipe, result, meta, budgetLeftMs) {
1312
+ const writes = isWrite(recipe);
1313
+ return !result.ok && result.error.code === "expect_failed" && !writes && meta.openedTabId !== void 0 && budgetLeftMs > COLD_RETRY_WAIT_MS + 8e3;
1314
+ }
1315
+ async function handleRun(msg, out, internal = {}) {
1316
+ const run_id = msg.run_id;
1317
+ if (typeof run_id !== "string" || !run_id) {
1318
+ out.send({ type: "error", code: "bad_message", message: "run needs a run_id" });
1319
+ return;
1320
+ }
1321
+ if (active.has(run_id)) return;
1322
+ const t0 = Date.now();
1323
+ const requests = [];
1324
+ let recipe;
1325
+ const emit = (e) => {
1326
+ const ev = sanitize(e);
1327
+ if (ev.kind === "request" && ev.data?.method && ev.data.origin) {
1328
+ requests.push({ method: ev.data.method, origin: ev.data.origin, path: ev.data.path ?? "/", status: ev.data.status ?? 0, ms: ev.data.ms ?? 0 });
1329
+ }
1330
+ out.send({ type: "event", run_id, ts: (/* @__PURE__ */ new Date()).toISOString(), ...ev });
1331
+ };
1332
+ let result;
1333
+ const runAbort = new AbortController();
1334
+ if (await checkForNewBuild()) {
1335
+ result = { ok: false, error: { code: "tab_error", message: `Wield was rebuilt on disk (${pendingBuild()}) but is still running ${BUILD_ID}. ${RELOAD_HINT}` } };
1336
+ emit({ level: "error", kind: "done", msg: `tab_error: ${result.error.message}` });
1337
+ out.send({ type: "result", run_id, ok: false, error: result.error, duration_ms: Date.now() - t0, requests: 0 });
1338
+ return;
1339
+ }
1340
+ runAborts.set(run_id, runAbort);
1341
+ try {
1342
+ let rec = recipe = checkRecipe(msg.recipe);
1343
+ const params = msg.params && typeof msg.params === "object" ? msg.params : {};
1344
+ const budgetMs = Math.min(typeof msg.timeout_ms === "number" && msg.timeout_ms > 0 ? msg.timeout_ms : LIMITS2.runMs, LIMITS2.runMs);
1345
+ emit({ level: "info", kind: "step", msg: `run ${rec.id} v${rec.version}` });
1346
+ if (msg.policy !== void 0 && msg.policy !== null) {
1347
+ const acc = await acceptPolicy(msg.policy);
1348
+ if (!acc.ok) throw new ExecError("origin_blocked", `${acc.error}. Nothing was sent.`);
1349
+ }
1350
+ const policy = await effectivePolicy();
1351
+ const host = new URL(rec.site.origin).host;
1352
+ const by = blockedBy(policy, new URL(rec.site.origin).hostname);
1353
+ if (by) throw new ExecError("origin_blocked", `Your organization's policy (${by}) doesn't allow ${host}. Nothing was sent.`);
1354
+ if (await isPaused(new URL(rec.site.origin).hostname)) {
1355
+ throw new ExecError("origin_blocked", `You paused ${host} in Wield. Resume it in the Wield popup to let your agent use it. Nothing was sent.`);
1356
+ }
1357
+ const sig = await checkRecipeSignature(msg.recipe, msg.signature, policy, msg.source, msg.draft, internal.bundled === true);
1358
+ emit(
1359
+ sig.status === "verified" ? { level: "info", kind: "step", msg: `recipe signature ok (${sig.kid}, ${sig.scope})` } : { level: "warn", kind: "step", msg: sig.why === "draft" ? "unsigned builder draft, run once to verify it" : sig.why === "local" ? "unsigned local recipe (developer folder)" : sig.why === "bundled" ? "the sample recipe packaged with Wield" : "recipe is unsigned" }
1360
+ );
1361
+ const trust = sig.status === "verified" && sig.scope === "public" ? "registry" : void 0;
1362
+ if (trust) {
1363
+ const why = await withdrawnReason(rec.id, rec.version);
1364
+ if (why) throw new ExecError("recipe_invalid", `${why}. Nothing was sent.`);
1365
+ }
1366
+ if (!trust) rec = recipe = hardenUnsigned(rec, emit);
1367
+ if (policy.writesDenied && isWrite(rec)) {
1368
+ throw new ExecError("confirmation_denied", `Your organization's policy (${policy.writesDenied}) doesn't allow actions that change something on a site. Nothing was sent.`);
1369
+ }
1370
+ if (sig.status === "unsigned" && sig.why === "draft") await askDraft(rec, run_id, runAbort.signal);
1371
+ let pausedMs = 0;
1372
+ const left = () => budgetMs - (Date.now() - t0 - pausedMs);
1373
+ const meta = {};
1374
+ const attempt = (tabId, forceNewTab = false) => execute({ run_id, recipe: rec, params, mode: "run", budgetMs: Math.max(1e3, left()), emit, meta, tabId, forceNewTab, ...trust ? { trust } : {} }).done;
1375
+ result = await attempt();
1376
+ if (!result.ok && result.unresponsive && meta.tabId !== void 0 && !runAbort.signal.aborted) {
1377
+ result = await recoverUnresponsive(rec, meta, attempt, emit);
1378
+ }
1379
+ if (shouldRetryCold(rec, result, meta, left())) {
1380
+ emit({ level: "warn", kind: "step", msg: `the site tab was just opened; waiting ${COLD_RETRY_WAIT_MS / 1e3} s for it to settle, then retrying once` });
1381
+ await sleep3(COLD_RETRY_WAIT_MS, runAbort.signal);
1382
+ result = runAbort.signal.aborted ? CANCELLED : await attempt(meta.openedTabId);
1383
+ }
1384
+ if (!result.ok && result.error.code === "site_challenge" && meta.tabId !== void 0 && !runAbort.signal.aborted) {
1385
+ result = await recoverFromChallenge({ recipe: rec, first: result, meta, attempt, emit, signal: runAbort.signal, left, paused: (ms) => pausedMs += ms });
1386
+ }
1387
+ } catch (e) {
1388
+ result = { ok: false, error: toRunError(e, "recipe_invalid") };
1389
+ } finally {
1390
+ runAborts.delete(run_id);
1391
+ }
1392
+ const duration_ms = Date.now() - t0;
1393
+ emit({
1394
+ level: result.ok ? "info" : "error",
1395
+ kind: "done",
1396
+ msg: result.ok ? `ok in ${duration_ms} ms, ${requests.length} requests` : `${result.error.code}: ${result.error.message}`
1397
+ });
1398
+ out.send(
1399
+ result.ok ? { type: "result", run_id, ok: true, output: result.output, duration_ms, requests: requests.length } : { type: "result", run_id, ok: false, error: result.error, duration_ms, requests: requests.length }
1400
+ // challenge details stay local
1401
+ );
1402
+ await recordRun({
1403
+ run_id,
1404
+ recipe_id: recipe?.id ?? (typeof msg.recipe?.id === "string" ? String(msg.recipe.id).slice(0, 80) : "(invalid recipe)"),
1405
+ version: recipe?.version ?? 0,
1406
+ site_id: recipe?.site?.id ?? "",
1407
+ ...recipe?.site?.origin ? { origin: recipe.site.origin } : {},
1408
+ kind: "run",
1409
+ started_at: new Date(t0).toISOString(),
1410
+ duration_ms,
1411
+ ok: result.ok,
1412
+ ...result.ok ? {} : { error_code: result.error.code },
1413
+ requests
1414
+ }).catch(() => {
1415
+ });
1416
+ }
1417
+ async function handleStatus(msg, out, version) {
1418
+ const req_id = typeof msg.req_id === "string" ? msg.req_id : "";
1419
+ const sites = Array.isArray(msg.sites) ? msg.sites.slice(0, 40) : [];
1420
+ const results = await Promise.all(sites.map((s) => checkSite(s, req_id, out)));
1421
+ out.send({ type: "status_result", req_id, extension_version: version, sites: results.map(({ site_id, logged_in, tab_open }) => ({ site_id, logged_in, tab_open })) });
1422
+ await recordSites(results).catch(() => {
1423
+ });
1424
+ }
1425
+ async function checkSite(s, req_id, out) {
1426
+ const site_id = String(s.site_id ?? "");
1427
+ const origin = isAllowedOrigin(s.origin) ? s.origin : "";
1428
+ const base = { site_id, origin, logged_in: null, tab_open: false, checked_at: (/* @__PURE__ */ new Date()).toISOString() };
1429
+ if (!origin) return base;
1430
+ const tabs = (await tabsForOrigin(origin)).filter((t) => t.status === "complete" && !t.discarded);
1431
+ base.tab_open = tabs.length > 0;
1432
+ if (!tabs.length || !s.login_check || !await chrome.permissions.contains({ origins: [originPattern(origin)] })) return base;
1433
+ if (blockedBy(await effectivePolicy(), new URL(origin).hostname) || await isPaused(new URL(origin).hostname)) return base;
1434
+ const lc = s.login_check;
1435
+ const recipe = {
1436
+ id: `${site_id}/login-check`,
1437
+ version: 1,
1438
+ task: "Check login state",
1439
+ effect: "read",
1440
+ requires_login: true,
1441
+ site: { id: site_id, name: site_id, origin, origins: [origin] },
1442
+ steps: [],
1443
+ login_check: { ...lc, id: lc?.id ?? "login_check" },
1444
+ // status.sites[].auth (CHANGES 11:58, C1): a login check may need a derived token.
1445
+ ...s.auth && typeof s.auth === "object" ? { auth: s.auth } : {}
1446
+ };
1447
+ const run_id = `${req_id}:${site_id}`;
1448
+ const t0 = Date.now();
1449
+ const requests = [];
1450
+ try {
1451
+ if (!lc?.request?.url) throw new ExecError("recipe_invalid", "login_check has no request");
1452
+ checkHeaders(lc);
1453
+ const r = await execute({
1454
+ run_id,
1455
+ recipe,
1456
+ params: {},
1457
+ mode: "login_check",
1458
+ budgetMs: 1e4,
1459
+ tabId: tabs[0].id,
1460
+ emit: (e) => {
1461
+ const ev = sanitize(e);
1462
+ if (ev.kind !== "request") return;
1463
+ if (ev.data?.method) requests.push({ method: ev.data.method, origin: ev.data.origin ?? origin, path: ev.data.path ?? "/", status: ev.data.status ?? 0, ms: ev.data.ms ?? 0 });
1464
+ out.send({ type: "event", run_id: req_id, ts: (/* @__PURE__ */ new Date()).toISOString(), ...ev });
1465
+ }
1466
+ }).done;
1467
+ base.logged_in = r.ok && typeof r.output === "boolean" ? r.output : null;
1468
+ } catch {
1469
+ base.logged_in = null;
1470
+ }
1471
+ await recordRun({ run_id, recipe_id: recipe.id, version: 1, site_id, kind: "status", started_at: new Date(t0).toISOString(), duration_ms: Date.now() - t0, ok: base.logged_in !== null, requests }).catch(() => {
1472
+ });
1473
+ return base;
1474
+ }
1475
+
1476
+ // apps/extension/src/sw/recorder.ts
1477
+ var MAX_ENTRIES = 400;
1478
+ var MAX_BODY = 1e6;
1479
+ var MAX_TOTAL_BODY = 8e6;
1480
+ var MAX_RECORDING_MS = 10 * 6e4;
1481
+ var REVIEW_TIMEOUT_MS = 15 * 6e4;
1482
+ var REC_SCRIPT_ID = "wield-rec-main";
1483
+ var NONCE_KEY = "__wield_rec__";
1484
+ var NAV_KEY = "__wield_rec_nav__";
1485
+ var COOKIE_STANDIN = "sent by the browser (not visible to the recorder)";
1486
+ var current = null;
1487
+ var recId = () => `rec_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
1488
+ function progress(r, msg, level = "info") {
1489
+ if (r.req_id) r.out?.send({ type: "event", run_id: r.req_id, ts: (/* @__PURE__ */ new Date()).toISOString(), level, kind: "step", msg });
1490
+ }
1491
+ function recordingState() {
1492
+ if (!current) return null;
1493
+ return { rec_id: current.rec_id, origin: current.origin, task: current.task, state: current.state, requests: current.entries.size, started_at: current.started_at };
1494
+ }
1495
+ async function startRecording(opts, out) {
1496
+ if (current) return { ok: false, error: `A recording is already running on ${new URL(current.origin).host}. Finish or cancel it first.` };
1497
+ let origin;
1498
+ try {
1499
+ origin = new URL(opts.origin).origin;
1500
+ } catch {
1501
+ return { ok: false, error: "origin must be a URL like https://www.example.de" };
1502
+ }
1503
+ if (!origin.startsWith("https://")) return { ok: false, error: "Wield records only https sites." };
1504
+ const host = new URL(origin).hostname;
1505
+ const by = blockedBy(await effectivePolicy(), host);
1506
+ if (by) return { ok: false, error: `Your organization's policy (${by}) doesn't allow ${host}.` };
1507
+ if (!opts.fromPopup) {
1508
+ const outcome = await askUser({ kind: "record", run_id: opts.req_id ?? "record", site: { id: host, name: host, origin }, task: opts.task }, new AbortController().signal);
1509
+ if (outcome !== "allow") return { ok: false, error: "The user did not allow recording." };
1510
+ }
1511
+ if (!await chrome.permissions.contains({ origins: [originPattern(origin)] })) {
1512
+ const outcome = await askUser({ kind: "permission", run_id: opts.req_id ?? "record", site: { id: host, name: host, origin }, task: opts.task || "Record a task" }, new AbortController().signal);
1513
+ if (outcome !== "allow" || !await chrome.permissions.contains({ origins: [originPattern(origin)] })) return { ok: false, error: `Access to ${host} was not granted.` };
1514
+ }
1515
+ let tabId = opts.tabId;
1516
+ if (tabId === void 0) {
1517
+ try {
1518
+ tabId = (await acquireTab(origin, new AbortController().signal)).tabId;
1519
+ } catch (e) {
1520
+ return { ok: false, error: e.message };
1521
+ }
1522
+ }
1523
+ const tab = await chrome.tabs.update(tabId, { active: true });
1524
+ if (tab?.windowId !== void 0) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {
1525
+ });
1526
+ const r = {
1527
+ rec_id: recId(),
1528
+ ...opts.req_id ? { req_id: opts.req_id } : {},
1529
+ origin,
1530
+ task: (opts.task ?? "").slice(0, 300),
1531
+ tabId,
1532
+ nonce: crypto.randomUUID(),
1533
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1534
+ t0: Date.now(),
1535
+ entries: /* @__PURE__ */ new Map(),
1536
+ bodyBytes: 0,
1537
+ typedPages: /* @__PURE__ */ new Map(),
1538
+ state: "recording",
1539
+ ...out ? { out } : {}
1540
+ };
1541
+ try {
1542
+ await startCapture(r);
1543
+ } catch (e) {
1544
+ await stopCapture(r);
1545
+ return { ok: false, error: `Could not start recording in that tab: ${e.message}` };
1546
+ }
1547
+ current = r;
1548
+ r.timer = setTimeout(() => void finish(r, "done"), MAX_RECORDING_MS);
1549
+ await attachBar(r);
1550
+ progress(r, `recording on ${new URL(origin).host}: waiting for the user to do the task and click Done`);
1551
+ return { ok: true, rec_id: r.rec_id };
1552
+ }
1553
+ async function startCapture(r) {
1554
+ await chrome.storage.session.set({ rec_active: { tabId: r.tabId } });
1555
+ await chrome.scripting.executeScript({ target: { tabId: r.tabId }, func: (k, n) => sessionStorage.setItem(k, n), args: [NONCE_KEY, r.nonce] });
1556
+ await chrome.scripting.unregisterContentScripts({ ids: [REC_SCRIPT_ID] }).catch(() => {
1557
+ });
1558
+ await chrome.scripting.registerContentScripts([
1559
+ { id: REC_SCRIPT_ID, js: ["recmain.js"], matches: [originPattern(r.origin)], runAt: "document_start", world: "MAIN", allFrames: false, persistAcrossSessions: false }
1560
+ ]);
1561
+ await chrome.scripting.executeScript({ target: { tabId: r.tabId }, files: ["recmain.js"], world: "MAIN" });
1562
+ }
1563
+ async function stopCapture(r) {
1564
+ await chrome.scripting.unregisterContentScripts({ ids: [REC_SCRIPT_ID] }).catch(() => {
1565
+ });
1566
+ await chrome.scripting.executeScript({
1567
+ target: { tabId: r.tabId },
1568
+ func: (keys) => {
1569
+ for (const k of keys) {
1570
+ try {
1571
+ sessionStorage.removeItem(k);
1572
+ } catch {
1573
+ }
1574
+ }
1575
+ },
1576
+ args: [[NONCE_KEY, NAV_KEY]]
1577
+ }).catch(() => {
1578
+ });
1579
+ await chrome.storage.session.remove("rec_active").catch(() => {
1580
+ });
1581
+ }
1582
+ void (async () => {
1583
+ const { rec_active } = await chrome.storage.session.get("rec_active").catch(() => ({}));
1584
+ if (rec_active && !current) await stopCapture(rec_active);
1585
+ })();
1586
+ chrome.tabs.onRemoved.addListener((tabId) => {
1587
+ if (current && current.tabId === tabId && current.state === "recording") void finish(current, "done", true);
1588
+ });
1589
+ async function attachBar(r) {
1590
+ if (current !== r || r.state !== "recording") return;
1591
+ try {
1592
+ await chrome.scripting.executeScript({ target: { tabId: r.tabId }, files: ["content.js"] });
1593
+ const port = chrome.tabs.connect(r.tabId, { name: `wield-rec:${r.rec_id}` });
1594
+ r.port = port;
1595
+ port.onMessage.addListener((m) => {
1596
+ if (current !== r) return;
1597
+ if (m.type === "entry" && m.entry && typeof m.entry === "object") addEntry(r, m.entry);
1598
+ else if (m.type === "typed" && Array.isArray(m.typed) && r.typedPages.size < 50) r.typedPages.set(String(m.page ?? "page").slice(0, 20), cleanTyped(m.typed));
1599
+ else if (m.type === "done") void finish(r, "done");
1600
+ else if (m.type === "cancel") void finish(r, "cancel");
1601
+ });
1602
+ port.onDisconnect.addListener(() => current === r && r.state === "recording" && setTimeout(() => void attachBar(r), 300));
1603
+ port.postMessage({ type: "show", host: new URL(r.origin).host, task: r.task, started_at: r.t0, max_ms: MAX_RECORDING_MS, requests: r.entries.size, nonce: r.nonce });
1604
+ } catch {
1605
+ }
1606
+ }
1607
+ function cleanTyped(list) {
1608
+ return list.filter((t) => !!t && typeof t.value === "string" && typeof t.label === "string").slice(0, 50).map((t) => ({ label: t.label.slice(0, 80), ...typeof t.name === "string" ? { name: t.name.slice(0, 80) } : {}, input_type: String(t.input_type ?? "text").slice(0, 30), value: t.value.slice(0, 200) }));
1609
+ }
1610
+ function typedValues(r) {
1611
+ const all = [];
1612
+ for (const list of r.typedPages.values()) for (const t of list) if (!all.some((p) => p.value === t.value && p.label === t.label)) all.push(t);
1613
+ return all.slice(0, 50);
1614
+ }
1615
+ function headerMap(h) {
1616
+ const out = {};
1617
+ if (!h || typeof h !== "object") return out;
1618
+ for (const [k, v] of Object.entries(h).slice(0, 60)) if (typeof v === "string") out[k.slice(0, 100)] = v.slice(0, 4096);
1619
+ return out;
1620
+ }
1621
+ function addEntry(r, e) {
1622
+ if (r.state !== "recording" || r.entries.size >= MAX_ENTRIES) return;
1623
+ if (typeof e.url !== "string" || !/^https?:\/\//.test(e.url)) return;
1624
+ let body = typeof e.body === "string" ? e.body : void 0;
1625
+ let truncated = e.body_truncated === true;
1626
+ if (body !== void 0) {
1627
+ if (r.bodyBytes >= MAX_TOTAL_BODY) {
1628
+ body = void 0;
1629
+ truncated = true;
1630
+ } else {
1631
+ if (body.length > MAX_BODY) {
1632
+ body = body.slice(0, MAX_BODY);
1633
+ truncated = true;
1634
+ }
1635
+ r.bodyBytes += body.length;
1636
+ }
1637
+ }
1638
+ const request_headers = headerMap(e.request_headers);
1639
+ if (e.sends_cookies === true && !Object.keys(request_headers).some((k) => k.toLowerCase() === "cookie")) request_headers.cookie = COOKIE_STANDIN;
1640
+ const seq = r.entries.size + 1;
1641
+ r.entries.set(String(seq), {
1642
+ seq,
1643
+ t_ms: Math.max(0, Math.round(Number(e.started) - r.t0) || 0),
1644
+ method: String(e.method ?? "GET").toUpperCase().slice(0, 10),
1645
+ url: e.url.slice(0, 4096),
1646
+ resource_type: e.kind === "document" ? "Document" : e.kind === "xhr" ? "XHR" : "Fetch",
1647
+ request_headers,
1648
+ ...typeof e.post_data === "string" ? { post_data: e.post_data.slice(0, 65536) } : {},
1649
+ ...typeof e.status === "number" ? { status: e.status } : {},
1650
+ ...typeof e.mime === "string" && e.mime ? { mime: e.mime.split(";")[0].trim().slice(0, 100) } : {},
1651
+ response_headers: headerMap(e.response_headers),
1652
+ ...body !== void 0 ? { body, body_bytes: body.length } : {},
1653
+ ...truncated ? { body_truncated: true } : {},
1654
+ ...typeof e.failed === "string" ? { failed: e.failed.slice(0, 200) } : {}
1655
+ });
1656
+ }
1657
+ async function finish(r, how, tabGone = false) {
1658
+ if (current !== r || r.state !== "recording") return;
1659
+ clearTimeout(r.timer);
1660
+ await new Promise((res) => setTimeout(res, how === "done" && !tabGone ? 600 : 0));
1661
+ await stopCapture(r);
1662
+ try {
1663
+ r.port?.postMessage({ type: "hide" });
1664
+ r.port?.disconnect();
1665
+ } catch {
1666
+ }
1667
+ if (how === "cancel") {
1668
+ progress(r, "the user cancelled the recording", "warn");
1669
+ if (r.req_id) r.out?.send({ type: "recording", req_id: r.req_id, rec_id: r.rec_id, ok: false, discarded: true, error: { code: "cancelled", message: "The user cancelled the recording. Nothing was sent." } });
1670
+ current = null;
1671
+ return;
1672
+ }
1673
+ r.state = "review";
1674
+ const raw = {
1675
+ rec_id: r.rec_id,
1676
+ origin: r.origin,
1677
+ task: r.task,
1678
+ started_at: r.started_at,
1679
+ duration_ms: Date.now() - r.t0,
1680
+ extension_version: chrome.runtime.getManifest().version,
1681
+ entries: [...r.entries.values()].sort((a, b) => a.t_ms - b.t_ms || a.seq - b.seq).map((e, i) => ({ ...e, seq: i + 1 })),
1682
+ typed: typedValues(r)
1683
+ };
1684
+ await chrome.storage.session.set({ [`rec:${r.rec_id}`]: raw });
1685
+ r.entries.clear();
1686
+ progress(r, `the user clicked Done (${raw.entries.length} requests captured); waiting for them to review what would be sent`);
1687
+ await chrome.tabs.create({ url: chrome.runtime.getURL(`record.html#${r.rec_id}`), active: true });
1688
+ r.timer = setTimeout(() => void discard(r.rec_id, "No answer on the review page within 15 minutes. Nothing was sent."), REVIEW_TIMEOUT_MS);
1689
+ }
1690
+ async function stopRecording(how) {
1691
+ if (!current || current.state !== "recording") return false;
1692
+ await finish(current, how);
1693
+ return true;
1694
+ }
1695
+ async function sendRecording(rec_id, trace) {
1696
+ const problems = validateTrace(trace);
1697
+ if (problems.length) return { ok: false, delivered: "refused", message: `Not sent: ${problems[0].message}. Nothing left your browser.` };
1698
+ const r = current;
1699
+ await chrome.storage.session.remove(`rec:${rec_id}`);
1700
+ await saveTrace(trace);
1701
+ if (r && r.rec_id === rec_id) {
1702
+ clearTimeout(r.timer);
1703
+ current = null;
1704
+ if (r.req_id && r.out?.send({ type: "recording", req_id: r.req_id, rec_id, ok: true, trace })) {
1705
+ return { ok: true, delivered: "agent", message: "Sent to your agent. It builds the recipe next." };
1706
+ }
1707
+ if (!r.req_id && r.out?.send({ type: "recording", rec_id, ok: true, trace })) {
1708
+ return { ok: true, delivered: "saved", message: `Saved on this computer for your agent (recording ${rec_id}). Ask your agent to build a recipe from it.` };
1709
+ }
1710
+ }
1711
+ return { ok: true, delivered: "saved", message: "Kept in this browser only (Wield\u2019s helper isn\u2019t connected). Download it from here, or connect Wield and record again." };
1712
+ }
1713
+ async function discard(rec_id, why = "The user discarded the recording. Nothing was sent.") {
1714
+ await chrome.storage.session.remove(`rec:${rec_id}`);
1715
+ const r = current;
1716
+ if (r && r.rec_id === rec_id) {
1717
+ clearTimeout(r.timer);
1718
+ current = null;
1719
+ if (r.req_id) r.out?.send({ type: "recording", req_id: r.req_id, rec_id, ok: false, discarded: true, error: { code: "cancelled", message: why } });
1720
+ }
1721
+ }
1722
+ async function saveTrace(trace) {
1723
+ const { saved_traces = [] } = await chrome.storage.local.get("saved_traces");
1724
+ saved_traces.unshift(trace);
1725
+ await chrome.storage.local.set({ saved_traces: saved_traces.slice(0, 5) });
1726
+ }
1727
+
1728
+ // apps/extension/src/sw/index.ts
1729
+ var VERSION = chrome.runtime.getManifest().version;
1730
+ var CAPABILITIES = ["policy", "signed_recipes", "warm", "record", "index", ...INTERPRETER_FEATURES];
1731
+ function browserName() {
1732
+ const brands = navigator.userAgentData?.brands ?? [];
1733
+ const b = brands.find((x) => /Chrome|Chromium|Edge|Brave|Opera/.test(x.brand) && !/Not/.test(x.brand));
1734
+ if (b) return `${b.brand.replace("Google ", "")} ${b.version}`;
1735
+ const m = navigator.userAgent.match(/Chrome\/(\d+)/);
1736
+ return m ? `Chrome ${m[1]}` : "Chromium";
1737
+ }
1738
+ var link = new NativeLink(
1739
+ (msg) => void onBridgeMessage(msg),
1740
+ (s) => void recordBridge(s)
1741
+ );
1742
+ async function onBridgeMessage(raw) {
1743
+ const msg = raw;
1744
+ switch (msg?.type) {
1745
+ case "hello":
1746
+ link.send({
1747
+ type: "hello_ack",
1748
+ extension_version: VERSION,
1749
+ browser: browserName(),
1750
+ protocol: PROTOCOL_VERSION,
1751
+ capabilities: CAPABILITIES,
1752
+ policy: policySummary(await effectivePolicy())
1753
+ });
1754
+ return;
1755
+ case "record_start":
1756
+ case "record": {
1757
+ const m = msg;
1758
+ if (!m.req_id) return void link.send({ type: "error", code: "bad_message", message: "record needs req_id" });
1759
+ const r = await startRecording({ origin: String(m.origin ?? ""), task: String(m.task ?? ""), req_id: m.req_id }, link);
1760
+ if (!r.ok) {
1761
+ const code = /policy/.test(r.error) ? "origin_blocked" : /not allow|not granted/.test(r.error) ? "confirmation_denied" : "tab_error";
1762
+ link.send({ type: "recording", req_id: m.req_id, ok: false, error: { code, message: r.error } });
1763
+ }
1764
+ return;
1765
+ }
1766
+ case "warm": {
1767
+ const m = msg;
1768
+ if (!isAllowedOrigin(m.origin)) return;
1769
+ if (blockedBy(await effectivePolicy(), new URL(m.origin).hostname) || await isPaused(new URL(m.origin).hostname)) return;
1770
+ if (!await chrome.permissions.contains({ origins: [originPattern(m.origin)] })) return;
1771
+ await warmTab(m.origin, typeof m.start_path === "string" ? m.start_path : void 0).catch(() => "skipped");
1772
+ return;
1773
+ }
1774
+ case "index": {
1775
+ const r = await acceptIndex(msg.document);
1776
+ link.send({ type: "index_ack", ...msg.req_id ? { req_id: msg.req_id } : {}, ...r });
1777
+ return;
1778
+ }
1779
+ case "policy": {
1780
+ const r = await acceptPolicy(msg.document);
1781
+ link.send({ type: "policy_ack", ...msg.req_id ? { req_id: msg.req_id } : {}, ...r });
1782
+ return;
1783
+ }
1784
+ case "run":
1785
+ await handleRun(msg, link);
1786
+ return;
1787
+ case "cancel":
1788
+ if (typeof msg.run_id === "string") cancelRun(msg.run_id);
1789
+ return;
1790
+ case "status":
1791
+ await handleStatus(msg, link, VERSION);
1792
+ return;
1793
+ default:
1794
+ link.send({
1795
+ type: "error",
1796
+ code: "bad_message",
1797
+ message: `Unknown message type: ${String(msg?.type).slice(0, 40)}`,
1798
+ ...msg?.run_id ? { run_id: msg.run_id } : {},
1799
+ ...msg?.req_id ? { req_id: msg.req_id } : {}
1800
+ });
1801
+ }
1802
+ }
1803
+ chrome.runtime.onMessageExternal.addListener((m, sender, reply) => {
1804
+ let host = "";
1805
+ try {
1806
+ host = new URL(sender.origin ?? sender.url ?? "").hostname;
1807
+ } catch {
1808
+ return false;
1809
+ }
1810
+ const allowed = host === "wield.run" || false;
1811
+ if (!allowed || sender.origin && !sender.origin.startsWith("https://")) return false;
1812
+ if (m?.type !== "ping") reply({ type: "error", message: 'Only {type: "ping"} is supported.' });
1813
+ else reply({ type: "pong", product: "wield", version: VERSION, bridge: link.connected ? "connected" : "offline" });
1814
+ return false;
1815
+ });
1816
+ var OWN_PAGES = chrome.runtime.getURL("");
1817
+ chrome.runtime.onMessage.addListener((m, sender, reply) => {
1818
+ if (sender.id !== chrome.runtime.id || !sender.url?.startsWith(OWN_PAGES)) return false;
1819
+ if (m?.type === "ask_answer" && typeof m.id === "string") {
1820
+ reply({ ok: answerAsk(m.id, m.allow === true) });
1821
+ return false;
1822
+ }
1823
+ if (m?.type === "popup_state") {
1824
+ link.ensure();
1825
+ void checkForNewBuild();
1826
+ Promise.all([chrome.storage.local.get(["runs", "sites"]), paceSnapshot()]).then(async ([local, pace]) => {
1827
+ reply({
1828
+ version: VERSION,
1829
+ browser: browserName(),
1830
+ bridge: link.state,
1831
+ active_runs: activeRuns(),
1832
+ runs: local.runs ?? [],
1833
+ sites: Object.values(local.sites ?? {}),
1834
+ sites_overview: await sitesOverview(local.runs ?? [], Object.values(local.sites ?? {})),
1835
+ pace,
1836
+ policy: policySummary(await effectivePolicy()),
1837
+ build: BUILD_ID,
1838
+ recording: recordingState(),
1839
+ pending_build: pendingBuild()
1840
+ });
1841
+ });
1842
+ return true;
1843
+ }
1844
+ if (m?.type === "record_begin" && typeof m.tabId === "number") {
1845
+ chrome.tabs.get(m.tabId).then(async (tab) => {
1846
+ const origin = tab.url ? new URL(tab.url).origin : "";
1847
+ reply(await startRecording({ origin, task: String(m.task ?? ""), tabId: m.tabId, fromPopup: true }, link));
1848
+ }, () => reply({ ok: false, error: "That tab is gone." }));
1849
+ return true;
1850
+ }
1851
+ if (m?.type === "record_stop") {
1852
+ stopRecording(m.how === "cancel" ? "cancel" : "done").then((ok) => reply({ ok }));
1853
+ return true;
1854
+ }
1855
+ if (m?.type === "record_send" && typeof m.rec_id === "string" && m.trace) {
1856
+ sendRecording(m.rec_id, m.trace).then(reply);
1857
+ return true;
1858
+ }
1859
+ if (m?.type === "record_discard" && typeof m.rec_id === "string") {
1860
+ discard(m.rec_id).then(() => reply({ ok: true }));
1861
+ return true;
1862
+ }
1863
+ if (m?.type === "sample_run") {
1864
+ (async () => {
1865
+ const recipe = await (await fetch(chrome.runtime.getURL("sample-recipe.json"))).json();
1866
+ const run_id = `sample_${crypto.randomUUID().slice(0, 8)}`;
1867
+ let resolve;
1868
+ const result = new Promise((r) => resolve = r);
1869
+ const capture = { send: (msg) => (msg.type === "result" && resolve(msg), true) };
1870
+ await handleRun({ run_id, recipe, params: recipe.verify?.example_params ?? {}, timeout_ms: 3e4 }, capture, { bundled: true });
1871
+ reply(await result);
1872
+ })().catch((e) => reply({ ok: false, error: { code: "tab_error", message: String(e?.message ?? e) } }));
1873
+ return true;
1874
+ }
1875
+ if (m?.type === "site_pause" && typeof m.host === "string") {
1876
+ setPaused(m.host, m.paused === true).then(() => reply({ ok: true }));
1877
+ return true;
1878
+ }
1879
+ if (m?.type === "site_revoke" && typeof m.origin === "string" && /^https:\/\/[a-z0-9.-]+$/.test(m.origin)) {
1880
+ chrome.permissions.remove({ origins: [`${m.origin}/*`] }).then((ok) => reply({ ok }), () => reply({ ok: false }));
1881
+ return true;
1882
+ }
1883
+ if (m?.type === "reload_extension") {
1884
+ reply({ ok: true });
1885
+ setTimeout(() => chrome.runtime.reload(), 100);
1886
+ return false;
1887
+ }
1888
+ if (m?.type === "reconnect") {
1889
+ link.connect();
1890
+ void checkForNewBuild();
1891
+ setInterval(() => void checkForNewBuild(), 15e3);
1892
+ reply({ ok: true });
1893
+ return false;
1894
+ }
1895
+ return false;
1896
+ });
1897
+ chrome.runtime.onStartup.addListener(() => link.ensure());
1898
+ chrome.runtime.onInstalled.addListener((d) => {
1899
+ link.ensure();
1900
+ if (d.reason === "install") void chrome.tabs.create({ url: chrome.runtime.getURL("welcome.html") });
1901
+ });
1902
+ chrome.tabs.onUpdated.addListener(() => link.ensure());
1903
+ chrome.tabs.onActivated.addListener(() => link.ensure());
1904
+ link.connect();
1905
+ void checkForNewBuild();
1906
+ setInterval(() => void checkForNewBuild(), 15e3);
1907
+ globalThis.wield = { link, version: VERSION, build: BUILD_ID, buildOnDisk };