@syrin/iris 0.4.0 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,31 @@
2
2
  var IRIS_DEFAULT_PORT = 4400;
3
3
  var IRIS_WS_PATH = "/iris";
4
4
  var IRIS_PROTOCOL_VERSION = 1;
5
+ var TRANSPORT_LIMITS = {
6
+ MAX_MESSAGE_BYTES: 1024 * 1024,
7
+ MAX_MESSAGES_PER_SECOND: 1e3,
8
+ MAX_SESSIONS: 32,
9
+ MAX_PENDING_CONNECTIONS: 16,
10
+ HELLO_TIMEOUT_MS: 5e3,
11
+ MAX_BUFFER_BYTES: 8 * 1024 * 1024,
12
+ MAX_SESSION_ID_LENGTH: 128,
13
+ MAX_URL_LENGTH: 4096,
14
+ MAX_TITLE_LENGTH: 512,
15
+ MAX_ADAPTERS: 32,
16
+ MAX_ADAPTER_NAME_LENGTH: 128,
17
+ MAX_TOKEN_LENGTH: 512,
18
+ MAX_COMMAND_ID_LENGTH: 128,
19
+ MAX_COMMAND_NAME_LENGTH: 128,
20
+ MAX_REF_LENGTH: 128,
21
+ MAX_ERROR_LENGTH: 4096,
22
+ MAX_SERIALIZE_DEPTH: 8,
23
+ MAX_COLLECTION_ITEMS: 200,
24
+ MAX_OBJECT_KEYS: 200,
25
+ MAX_STRING_LENGTH: 64 * 1024
26
+ };
27
+ var REDACTED_VALUE = "[REDACTED]";
28
+ var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
29
+ var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
5
30
  var RunKind = {
6
31
  FLOW_REPLAY: "flow_replay",
7
32
  // auto-recorded by iris_flow_replay
@@ -42,6 +67,11 @@ var RecorderPhase = {
42
67
  ANNOTATING: "annotating"
43
68
  // recording paused, awaiting an annotation target/kind
44
69
  };
70
+ var RING_BUFFER_DEFAULTS = {
71
+ MAX_EVENTS: 2e3,
72
+ MAX_AGE_MS: 6e4,
73
+ MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
74
+ };
45
75
  var EventType = {
46
76
  DOM_ADDED: "dom.added",
47
77
  DOM_REMOVED: "dom.removed",
@@ -206,136 +236,209 @@ var MessageKind = {
206
236
  EVENT: "event"
207
237
  };
208
238
 
209
- // ../protocol/dist/types.js
239
+ // ../protocol/dist/messages.js
210
240
  import { z } from "zod";
211
- var ElementQuerySchema = z.object({
212
- by: z.nativeEnum(QueryBy).optional(),
213
- value: z.string().optional(),
214
- role: z.string().optional(),
215
- name: z.string().optional(),
216
- text: z.string().optional(),
217
- label: z.string().optional(),
218
- placeholder: z.string().optional(),
219
- testid: z.string().optional(),
220
- alt: z.string().optional(),
241
+ var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
242
+ var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
243
+ var HumanControlDataSchema = z.object({
244
+ kind: z.nativeEnum(HumanControlKind),
245
+ text: z.string().optional()
246
+ });
247
+ var IrisEventSchema = z.object({
248
+ t: z.number(),
249
+ type: z.nativeEnum(EventType),
250
+ sessionId: sessionIdSchema,
251
+ /** Stable element reference this event concerns, when applicable (e.g. "e7"). */
252
+ ref: refSchema.optional(),
253
+ /** Event-type-specific payload. Kept open here; refined per observer at the edges. */
254
+ data: z.record(z.unknown()).default({})
255
+ });
256
+ var HelloMessageSchema = z.object({
257
+ kind: z.literal(MessageKind.HELLO),
258
+ protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
259
+ sessionId: sessionIdSchema,
260
+ url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
261
+ title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
262
+ adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
263
+ /** Optional browser/bridge pairing token. Required when the bridge configures one. */
264
+ token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
265
+ /** Whether the app has advertised a capability registry (iris.describe). */
266
+ hasCapabilities: z.boolean().optional()
267
+ });
268
+ var CommandMessageSchema = z.object({
269
+ kind: z.literal(MessageKind.COMMAND),
270
+ id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
271
+ sessionId: sessionIdSchema.optional(),
272
+ name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
273
+ args: z.record(z.unknown()).default({})
274
+ });
275
+ var CommandResultSchema = z.object({
276
+ kind: z.literal(MessageKind.COMMAND_RESULT),
277
+ id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
278
+ ok: z.boolean(),
279
+ result: z.unknown().optional(),
280
+ error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
281
+ });
282
+ var EventMessageSchema = z.object({
283
+ kind: z.literal(MessageKind.EVENT),
284
+ event: IrisEventSchema
285
+ });
286
+ var IrisMessageSchema = z.discriminatedUnion("kind", [
287
+ HelloMessageSchema,
288
+ CommandMessageSchema,
289
+ CommandResultSchema,
290
+ EventMessageSchema
291
+ ]);
292
+
293
+ // ../protocol/dist/security.js
294
+ var DANGEROUS_ACTION = /\b(delete|remove|destroy|erase|drop|terminate|revoke|reset|logout|log out|sign out|close account|cancel subscription|purchase|buy|pay|place order|confirm order|deploy|publish|send|transfer|withdraw|refund)\b/i;
295
+ function isLoopbackHostname(hostname) {
296
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
297
+ if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
298
+ return true;
299
+ }
300
+ const octets = normalized.split(".");
301
+ return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
302
+ if (!/^\d{1,3}$/.test(octet))
303
+ return false;
304
+ const value = Number(octet);
305
+ return value >= 0 && value <= 255;
306
+ });
307
+ }
308
+ function isDangerousActionText(text) {
309
+ return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
310
+ }
311
+
312
+ // ../protocol/dist/types.js
313
+ import { z as z2 } from "zod";
314
+ var ElementQuerySchema = z2.object({
315
+ by: z2.nativeEnum(QueryBy).optional(),
316
+ value: z2.string().optional(),
317
+ role: z2.string().optional(),
318
+ name: z2.string().optional(),
319
+ text: z2.string().optional(),
320
+ label: z2.string().optional(),
321
+ placeholder: z2.string().optional(),
322
+ testid: z2.string().optional(),
323
+ alt: z2.string().optional(),
221
324
  /** CSS selector or ref to scope the search. */
222
- scope: z.string().optional()
325
+ scope: z2.string().optional()
223
326
  });
224
- var CapabilityFlowSchema = z.object({
225
- name: z.string(),
226
- steps: z.array(z.string())
327
+ var CapabilityFlowSchema = z2.object({
328
+ name: z2.string(),
329
+ steps: z2.array(z2.string())
227
330
  });
228
- var CapabilitiesSchema = z.object({
229
- testids: z.array(z.string()),
230
- signals: z.array(z.string()),
231
- stores: z.array(z.string()),
232
- flows: z.array(CapabilityFlowSchema)
331
+ var CapabilitiesSchema = z2.object({
332
+ testids: z2.array(z2.string()),
333
+ signals: z2.array(z2.string()),
334
+ stores: z2.array(z2.string()),
335
+ flows: z2.array(CapabilityFlowSchema)
233
336
  });
234
- var ContractFileSchema = z.object({
235
- version: z.number(),
236
- generatedAt: z.number(),
337
+ var ContractFileSchema = z2.object({
338
+ version: z2.number(),
339
+ generatedAt: z2.number(),
237
340
  capabilities: CapabilitiesSchema
238
341
  });
239
- var RunEvidenceSchema = z.object({
240
- consoleErrors: z.number().optional(),
241
- networkErrors: z.number().optional(),
242
- driftSteps: z.number().optional()
342
+ var RunEvidenceSchema = z2.object({
343
+ consoleErrors: z2.number().optional(),
344
+ networkErrors: z2.number().optional(),
345
+ driftSteps: z2.number().optional()
243
346
  });
244
- var RunRecordSchema = z.object({
245
- kind: z.nativeEnum(RunKind),
246
- name: z.string(),
247
- status: z.nativeEnum(RunStatus),
248
- at: z.number(),
249
- summary: z.string().optional(),
347
+ var RunRecordSchema = z2.object({
348
+ kind: z2.nativeEnum(RunKind),
349
+ name: z2.string(),
350
+ status: z2.nativeEnum(RunStatus),
351
+ at: z2.number(),
352
+ summary: z2.string().optional(),
250
353
  evidence: RunEvidenceSchema.optional(),
251
- durationMs: z.number().optional()
354
+ durationMs: z2.number().optional()
252
355
  });
253
- var ProjectLearnedSchema = z.object({
254
- flows: z.array(z.string()).optional(),
255
- routes: z.array(z.string()).optional()
356
+ var ProjectLearnedSchema = z2.object({
357
+ flows: z2.array(z2.string()).optional(),
358
+ routes: z2.array(z2.string()).optional()
256
359
  });
257
- var ProjectFileSchema = z.object({
258
- version: z.number(),
360
+ var ProjectFileSchema = z2.object({
361
+ version: z2.number(),
259
362
  learned: ProjectLearnedSchema.optional(),
260
- runs: z.array(RunRecordSchema)
363
+ runs: z2.array(RunRecordSchema)
261
364
  });
262
- var FlowAnchorSchema = z.discriminatedUnion("kind", [
263
- z.object({ kind: z.literal(AnchorKind.TESTID), value: z.string().min(1) }),
264
- z.object({
265
- kind: z.literal(AnchorKind.ROLE),
266
- role: z.string().min(1),
267
- name: z.string().optional()
365
+ var FlowAnchorSchema = z2.discriminatedUnion("kind", [
366
+ z2.object({ kind: z2.literal(AnchorKind.TESTID), value: z2.string().min(1) }),
367
+ z2.object({
368
+ kind: z2.literal(AnchorKind.ROLE),
369
+ role: z2.string().min(1),
370
+ name: z2.string().optional()
268
371
  }),
269
- z.object({ kind: z.literal(AnchorKind.SIGNAL), name: z.string().min(1) })
372
+ z2.object({ kind: z2.literal(AnchorKind.SIGNAL), name: z2.string().min(1) })
270
373
  ]);
271
- var FlowExpectSchema = z.object({
272
- signal: z.string().optional(),
374
+ var FlowExpectSchema = z2.object({
375
+ signal: z2.string().optional(),
273
376
  /**
274
377
  * Optional payload shape an `assert-signal` annotation requires the signal
275
378
  * to match (the predicate DSL's signal.dataMatches). Additive/optional — a flow file with a
276
379
  * bare `signal` still parses, and the on-disk version stays FLOW_FILE_VERSION 1.
277
380
  */
278
- signalData: z.record(z.unknown()).optional(),
279
- net: z.object({
280
- method: z.string().optional(),
281
- urlContains: z.string().optional(),
282
- status: z.number().optional()
381
+ signalData: z2.record(z2.unknown()).optional(),
382
+ net: z2.object({
383
+ method: z2.string().optional(),
384
+ urlContains: z2.string().optional(),
385
+ status: z2.number().optional()
283
386
  }).optional(),
284
- element: z.object({
285
- testid: z.string().optional(),
286
- role: z.string().optional(),
287
- name: z.string().optional()
387
+ element: z2.object({
388
+ testid: z2.string().optional(),
389
+ role: z2.string().optional(),
390
+ name: z2.string().optional()
288
391
  }).optional()
289
392
  });
290
- var baseFlowStep = z.object({
291
- tool: z.string(),
393
+ var baseFlowStep = z2.object({
394
+ tool: z2.string(),
292
395
  anchor: FlowAnchorSchema,
293
- action: z.nativeEnum(ActionType).optional(),
294
- args: z.record(z.unknown()).optional(),
396
+ action: z2.nativeEnum(ActionType).optional(),
397
+ args: z2.record(z2.unknown()).optional(),
295
398
  expect: FlowExpectSchema.optional(),
296
- degraded: z.boolean().optional()
399
+ degraded: z2.boolean().optional()
297
400
  });
298
401
  var FlowStepSchema = baseFlowStep.extend({
299
- steps: z.lazy(() => z.array(FlowStepSchema).optional())
402
+ steps: z2.lazy(() => z2.array(FlowStepSchema).optional())
300
403
  });
301
- var FlowFileSchema = z.object({
302
- version: z.literal(FLOW_FILE_VERSION),
303
- name: z.string(),
404
+ var FlowFileSchema = z2.object({
405
+ version: z2.literal(FLOW_FILE_VERSION),
406
+ name: z2.string(),
304
407
  // FUTURE: fixtures/preconditions — schema slot reserved, unpopulated this cut. The recorder
305
408
  // never writes it and no fixture runner exists.
306
- fixture: z.string().optional(),
409
+ fixture: z2.string().optional(),
307
410
  /** From the injected clock (ms) — deterministic in tests, byte-stable on disk. */
308
- createdAt: z.number(),
309
- steps: z.array(FlowStepSchema),
411
+ createdAt: z2.number(),
412
+ steps: z2.array(FlowStepSchema),
310
413
  success: FlowExpectSchema.optional(),
311
414
  /**
312
415
  * Anchors whose CONTENT must not be asserted (e.g. LLM output). Replay asserts
313
416
  * presence, not words. Compiled from a `mark-dynamic` annotation.
314
417
  */
315
- dynamic: z.array(FlowAnchorSchema).optional()
418
+ dynamic: z2.array(FlowAnchorSchema).optional()
316
419
  });
317
- var RecordedFlowSchema = z.object({
318
- name: z.string(),
420
+ var RecordedFlowSchema = z2.object({
421
+ name: z2.string(),
319
422
  flow: FlowFileSchema
320
423
  });
321
- var AnnotationSchema = z.discriminatedUnion("kind", [
322
- z.object({
323
- kind: z.literal(AnnotationKind.ASSERT_SIGNAL),
324
- name: z.string().min(1),
325
- dataMatches: z.record(z.unknown()).optional()
424
+ var AnnotationSchema = z2.discriminatedUnion("kind", [
425
+ z2.object({
426
+ kind: z2.literal(AnnotationKind.ASSERT_SIGNAL),
427
+ name: z2.string().min(1),
428
+ dataMatches: z2.record(z2.unknown()).optional()
326
429
  }),
327
- z.object({
328
- kind: z.literal(AnnotationKind.ASSERT_VISIBLE),
329
- testid: z.string().min(1)
430
+ z2.object({
431
+ kind: z2.literal(AnnotationKind.ASSERT_VISIBLE),
432
+ testid: z2.string().min(1)
330
433
  }),
331
- z.object({
332
- kind: z.literal(AnnotationKind.MARK_DYNAMIC),
333
- testid: z.string().min(1)
434
+ z2.object({
435
+ kind: z2.literal(AnnotationKind.MARK_DYNAMIC),
436
+ testid: z2.string().min(1)
334
437
  }),
335
- z.object({
336
- kind: z.literal(AnnotationKind.SUCCESS_STATE),
337
- signal: z.string().min(1).optional(),
338
- testid: z.string().min(1).optional()
438
+ z2.object({
439
+ kind: z2.literal(AnnotationKind.SUCCESS_STATE),
440
+ signal: z2.string().min(1).optional(),
441
+ testid: z2.string().min(1).optional()
339
442
  })
340
443
  ]);
341
444
 
@@ -370,6 +473,96 @@ var RefRegistry = class {
370
473
  };
371
474
  var refs = new RefRegistry();
372
475
 
476
+ // ../browser/dist/security/serialization.js
477
+ var TRUNCATED_VALUE = "[TRUNCATED]";
478
+ var UNSERIALIZABLE_VALUE = "[UNSERIALIZABLE]";
479
+ var OMIT_VALUE = /* @__PURE__ */ Symbol("omit");
480
+ var MAX_KEY_LENGTH = 256;
481
+ var MAX_TOTAL_CHARACTERS = Math.floor(TRANSPORT_LIMITS.MAX_MESSAGE_BYTES / 8);
482
+ var MAX_TOTAL_NODES = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS * 5;
483
+ var SENSITIVE_KEY = /password|passwd|passcode|secret|token|authorization|api[-_]?key|access[-_]?key|private[-_]?key|client[-_]?secret|credit[-_]?card|card[-_]?number|cvv|cvc|ssn/i;
484
+ function isSensitiveKey(key) {
485
+ return SENSITIVE_KEY.test(key);
486
+ }
487
+ function boundedString(value, state, max) {
488
+ const allowed = Math.max(0, Math.min(max, state.remainingCharacters));
489
+ if (value.length <= allowed) {
490
+ state.remainingCharacters -= value.length;
491
+ return value;
492
+ }
493
+ const truncated = allowed <= TRUNCATED_VALUE.length ? TRUNCATED_VALUE.slice(0, allowed) : `${value.slice(0, allowed - TRUNCATED_VALUE.length)}${TRUNCATED_VALUE}`;
494
+ state.remainingCharacters -= truncated.length;
495
+ return truncated;
496
+ }
497
+ function sanitize(value, state, depth, key) {
498
+ if (key !== void 0 && isSensitiveKey(key))
499
+ return REDACTED_VALUE;
500
+ if (depth > TRANSPORT_LIMITS.MAX_SERIALIZE_DEPTH || state.nodes >= MAX_TOTAL_NODES) {
501
+ return TRUNCATED_VALUE;
502
+ }
503
+ state.nodes += 1;
504
+ if (value === null || typeof value === "boolean")
505
+ return value;
506
+ if (typeof value === "string") {
507
+ return boundedString(value, state, key?.toLowerCase() === "error" ? TRANSPORT_LIMITS.MAX_ERROR_LENGTH : TRANSPORT_LIMITS.MAX_STRING_LENGTH);
508
+ }
509
+ if (typeof value === "number")
510
+ return Number.isFinite(value) ? value : null;
511
+ if (typeof value === "bigint")
512
+ return value.toString();
513
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
514
+ return OMIT_VALUE;
515
+ }
516
+ if (value instanceof Date)
517
+ return value.toISOString();
518
+ if (value instanceof Error) {
519
+ return {
520
+ name: boundedString(value.name, state, 256),
521
+ message: boundedString(value.message, state, TRANSPORT_LIMITS.MAX_ERROR_LENGTH)
522
+ };
523
+ }
524
+ if (state.seen.has(value))
525
+ return "[CIRCULAR]";
526
+ state.seen.add(value);
527
+ try {
528
+ if (Array.isArray(value)) {
529
+ return value.slice(0, TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS).map((item) => {
530
+ const sanitized = sanitize(item, state, depth + 1);
531
+ return sanitized === OMIT_VALUE ? null : sanitized;
532
+ });
533
+ }
534
+ const out = /* @__PURE__ */ Object.create(null);
535
+ for (const rawKey of Object.keys(value).slice(0, TRANSPORT_LIMITS.MAX_OBJECT_KEYS)) {
536
+ const safeKey = boundedString(rawKey, state, MAX_KEY_LENGTH);
537
+ try {
538
+ const sanitized = sanitize(value[rawKey], state, depth + 1, rawKey);
539
+ if (sanitized !== OMIT_VALUE)
540
+ out[safeKey] = sanitized;
541
+ } catch {
542
+ out[safeKey] = UNSERIALIZABLE_VALUE;
543
+ }
544
+ }
545
+ return out;
546
+ } finally {
547
+ state.seen.delete(value);
548
+ }
549
+ }
550
+ function sanitizeForTransport(value) {
551
+ const sanitized = sanitize(value, {
552
+ seen: /* @__PURE__ */ new WeakSet(),
553
+ remainingCharacters: MAX_TOTAL_CHARACTERS,
554
+ nodes: 0
555
+ }, 0);
556
+ return sanitized === OMIT_VALUE ? null : sanitized;
557
+ }
558
+ function safeStringify(value) {
559
+ try {
560
+ return JSON.stringify(sanitizeForTransport(value));
561
+ } catch {
562
+ return JSON.stringify(UNSERIALIZABLE_VALUE);
563
+ }
564
+ }
565
+
373
566
  // ../browser/dist/dom/a11y.js
374
567
  var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
375
568
  "button",
@@ -536,6 +729,17 @@ function getStates(el) {
536
729
  }
537
730
  function getValue(el) {
538
731
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
732
+ const autocomplete = el.getAttribute("autocomplete") ?? "";
733
+ const identifiers = [
734
+ el.getAttribute("name") ?? "",
735
+ el.id,
736
+ el.getAttribute("data-testid") ?? "",
737
+ el.getAttribute("aria-label") ?? ""
738
+ ];
739
+ const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
740
+ if (el instanceof HTMLInputElement && el.type.toLowerCase() === "password" || sensitiveAutocomplete || identifiers.some(isSensitiveKey)) {
741
+ return REDACTED_VALUE;
742
+ }
539
743
  return el.value;
540
744
  }
541
745
  const valueNow = el.getAttribute("aria-valuenow");
@@ -1040,6 +1244,30 @@ var result = (ref, action, effect, settled, settleReason, warning) => {
1040
1244
  var FILL_LIKE = /* @__PURE__ */ new Set([ActionType.FILL, ActionType.TYPE, ActionType.CLEAR]);
1041
1245
  var isFillLike = (action) => FILL_LIKE.has(action);
1042
1246
  var CLICK_LIKE = /* @__PURE__ */ new Set([ActionType.CLICK, ActionType.DBLCLICK]);
1247
+ function dangerousActionContext(el) {
1248
+ const form = el.closest("form");
1249
+ return [
1250
+ getAccessibleName(el),
1251
+ el.textContent ?? "",
1252
+ el.getAttribute("value") ?? "",
1253
+ el.getAttribute("title") ?? "",
1254
+ el.getAttribute("aria-label") ?? "",
1255
+ el.getAttribute("href") ?? "",
1256
+ form?.getAttribute("action") ?? "",
1257
+ form?.textContent ?? ""
1258
+ ].join(" ");
1259
+ }
1260
+ function requiresDangerousConfirmation(text) {
1261
+ return isDangerousActionText(text);
1262
+ }
1263
+ function assertActionAllowed(el, action, args) {
1264
+ const canTrigger = action === ActionType.CLICK || action === ActionType.DBLCLICK || action === ActionType.DRAG || action === ActionType.SUBMIT || action === ActionType.PRESS && asString(args["key"], "Enter") === "Enter";
1265
+ const dragTarget = action === ActionType.DRAG ? refs.resolve(asString(args["toRef"])) : null;
1266
+ const context = dragTarget instanceof HTMLElement ? `${dangerousActionContext(el)} ${dangerousActionContext(dragTarget)}` : dangerousActionContext(el);
1267
+ if (canTrigger && requiresDangerousConfirmation(context) && args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
1268
+ throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
1269
+ }
1270
+ }
1043
1271
  var NO_GEOMETRY = { occluded: false, occludedBy: null, scrolledIntoView: false };
1044
1272
  function fireClickSequence(el) {
1045
1273
  const doc = el.ownerDocument;
@@ -1206,6 +1434,7 @@ async function dispatchFor(el, action, args) {
1206
1434
  }
1207
1435
  async function executeAction(ref, action, args = {}) {
1208
1436
  const el = requireElement(ref);
1437
+ assertActionAllowed(el, action, args);
1209
1438
  const visible = isVisible(el);
1210
1439
  const enabled = enabledOf(el);
1211
1440
  const prevFocus = activeRef(el);
@@ -1312,7 +1541,10 @@ async function dragElement(source, target, data) {
1312
1541
  }
1313
1542
  return dropPrevented;
1314
1543
  }
1315
- async function dispatchWebMcp(tool, params) {
1544
+ async function dispatchWebMcp(tool, params, confirmDangerous = false) {
1545
+ if (requiresDangerousConfirmation(tool) && !confirmDangerous) {
1546
+ throw new Error(`potentially destructive WebMCP tool blocked; retry with ${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
1547
+ }
1316
1548
  const mc = navigator.modelContext;
1317
1549
  if (mc === void 0 || typeof mc.callTool !== "function") {
1318
1550
  throw new Error("WebMCP (navigator.modelContext) not available on this page");
@@ -1359,7 +1591,7 @@ function readStores(only) {
1359
1591
  if (only !== void 0 && name !== only)
1360
1592
  continue;
1361
1593
  try {
1362
- out[name] = getter();
1594
+ out[name] = sanitizeForTransport(getter());
1363
1595
  } catch (error) {
1364
1596
  out[name] = { __error: error instanceof Error ? error.message : String(error) };
1365
1597
  }
@@ -1508,6 +1740,9 @@ function inspect(ref) {
1508
1740
  return {
1509
1741
  ...describe(el),
1510
1742
  tag: el.tagName.toLowerCase(),
1743
+ href: el.getAttribute("href") ?? void 0,
1744
+ formAction: el instanceof HTMLButtonElement || el instanceof HTMLInputElement ? el.form?.getAttribute("action") ?? void 0 : void 0,
1745
+ formText: el instanceof HTMLButtonElement || el instanceof HTMLInputElement ? el.form?.textContent ?? void 0 : void 0,
1511
1746
  box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1512
1747
  styles,
1513
1748
  component
@@ -1552,6 +1787,16 @@ function listAnimations() {
1552
1787
  });
1553
1788
  return { animations };
1554
1789
  }
1790
+ function resolveNavigationUrl(rawUrl, baseUrl) {
1791
+ if (rawUrl.length === 0 || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH)
1792
+ return null;
1793
+ try {
1794
+ const url = new URL(rawUrl, baseUrl);
1795
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
1796
+ } catch {
1797
+ return null;
1798
+ }
1799
+ }
1555
1800
  function createCommandRegistry() {
1556
1801
  const reg = /* @__PURE__ */ new Map();
1557
1802
  reg.set(IrisCommand.SNAPSHOT, (args) => buildSnapshot({
@@ -1564,7 +1809,7 @@ function createCommandRegistry() {
1564
1809
  const action = str(args["action"]) ?? "";
1565
1810
  if (action === ActionType.WEBMCP) {
1566
1811
  const inner = record(args["args"]);
1567
- return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]));
1812
+ return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]), inner[DANGEROUS_ACTION_CONFIRM_ARG] === true);
1568
1813
  }
1569
1814
  return executeAction(str(args["ref"]) ?? "", action, record(args["args"]));
1570
1815
  });
@@ -1591,9 +1836,12 @@ function createCommandRegistry() {
1591
1836
  return scrollContainer(str(args["ref"]), typeof dy === "number" ? dy : void 0, typeof fraction === "number" ? fraction : void 0);
1592
1837
  });
1593
1838
  reg.set(IrisCommand.NAVIGATE, (args) => {
1594
- const url = str(args["url"]);
1595
- if (url === void 0 || url.length === 0)
1839
+ const rawUrl = str(args["url"]);
1840
+ if (rawUrl === void 0 || rawUrl.length === 0)
1596
1841
  return { ok: false, reason: "url required" };
1842
+ const url = resolveNavigationUrl(rawUrl, window.location.href);
1843
+ if (url === null)
1844
+ return { ok: false, reason: "only http(s) navigation is allowed" };
1597
1845
  window.location.assign(url);
1598
1846
  return { ok: true, url };
1599
1847
  });
@@ -1644,6 +1892,7 @@ var Transport = class {
1644
1892
  for (const msg of this.#queue)
1645
1893
  ws.send(msg);
1646
1894
  this.#queue = [];
1895
+ this.#deps.onConnected?.();
1647
1896
  };
1648
1897
  ws.onmessage = (event) => {
1649
1898
  const data = event.data;
@@ -1685,12 +1934,23 @@ var Transport = class {
1685
1934
  } catch {
1686
1935
  return;
1687
1936
  }
1688
- const msg = parsed;
1689
- if (msg.kind !== MessageKind.COMMAND)
1937
+ const result2 = CommandMessageSchema.safeParse(parsed);
1938
+ if (!result2.success)
1939
+ return;
1940
+ const command = result2.data;
1941
+ const currentSessionId = this.#deps.hello().sessionId;
1942
+ if (command.sessionId !== void 0 && command.sessionId !== currentSessionId)
1690
1943
  return;
1691
- const command = parsed;
1692
- const outcome = await this.#deps.handleCommand(command);
1693
- this.#sendRaw(JSON.stringify({
1944
+ let outcome;
1945
+ try {
1946
+ outcome = await this.#deps.handleCommand(command);
1947
+ } catch (error) {
1948
+ outcome = {
1949
+ ok: false,
1950
+ error: error instanceof Error ? error.message : String(error)
1951
+ };
1952
+ }
1953
+ this.#sendRaw(safeStringify({
1694
1954
  kind: MessageKind.COMMAND_RESULT,
1695
1955
  id: command.id,
1696
1956
  ok: outcome.ok,
@@ -1699,7 +1959,7 @@ var Transport = class {
1699
1959
  }));
1700
1960
  }
1701
1961
  sendEvent(event) {
1702
- this.#sendRaw(JSON.stringify({ kind: MessageKind.EVENT, event }));
1962
+ this.#sendRaw(safeStringify({ kind: MessageKind.EVENT, event }));
1703
1963
  }
1704
1964
  #sendRaw(text) {
1705
1965
  if (this.#ws !== void 0 && this.#ws.readyState === WebSocket.OPEN) {
@@ -1818,13 +2078,14 @@ function methodOf(input, init) {
1818
2078
  return "GET";
1819
2079
  }
1820
2080
  function installNetwork(emit) {
1821
- const origFetch = window.fetch.bind(window);
2081
+ const origFetch = window.fetch;
2082
+ const callFetch = origFetch.bind(window);
1822
2083
  window.fetch = async (input, init) => {
1823
2084
  const start = performance.now();
1824
2085
  const method = methodOf(input, init);
1825
2086
  const url = urlOf(input);
1826
2087
  try {
1827
- const res = await origFetch(input, init);
2088
+ const res = await callFetch(input, init);
1828
2089
  emit(EventType.NET_REQUEST, {
1829
2090
  method,
1830
2091
  url,
@@ -1890,8 +2151,10 @@ function snapshotLocation() {
1890
2151
  };
1891
2152
  }
1892
2153
  function installRoute(emit) {
1893
- const origPush = history.pushState.bind(history);
1894
- const origReplace = history.replaceState.bind(history);
2154
+ const origPush = history.pushState;
2155
+ const origReplace = history.replaceState;
2156
+ const callPush = origPush.bind(history);
2157
+ const callReplace = origReplace.bind(history);
1895
2158
  const fire = (from) => {
1896
2159
  const to = snapshotLocation();
1897
2160
  if (to.href === from)
@@ -1906,12 +2169,12 @@ function installRoute(emit) {
1906
2169
  };
1907
2170
  history.pushState = (data, unused, url) => {
1908
2171
  const from = location.href;
1909
- origPush(data, unused, url ?? null);
2172
+ callPush(data, unused, url ?? null);
1910
2173
  fire(from);
1911
2174
  };
1912
2175
  history.replaceState = (data, unused, url) => {
1913
2176
  const from = location.href;
1914
- origReplace(data, unused, url ?? null);
2177
+ callReplace(data, unused, url ?? null);
1915
2178
  fire(from);
1916
2179
  };
1917
2180
  let lastHref = location.href;
@@ -1941,22 +2204,19 @@ function stringifyArgs(args) {
1941
2204
  return a;
1942
2205
  if (a instanceof Error)
1943
2206
  return a.message;
1944
- try {
1945
- return JSON.stringify(a);
1946
- } catch {
1947
- return String(a);
1948
- }
2207
+ return safeStringify(a);
1949
2208
  }).join(" ");
1950
2209
  }
1951
2210
  function installConsole(emit) {
1952
2211
  const methods = ["log", "warn", "error"];
1953
2212
  const originals2 = /* @__PURE__ */ new Map();
1954
2213
  for (const method of methods) {
1955
- const original = console[method].bind(console);
2214
+ const original = console[method];
1956
2215
  originals2.set(method, original);
2216
+ const callOriginal = original.bind(console);
1957
2217
  console[method] = (...args) => {
1958
2218
  emit(METHOD_EVENT[method], { message: stringifyArgs(args) });
1959
- original(...args);
2219
+ callOriginal(...args);
1960
2220
  };
1961
2221
  }
1962
2222
  const onError = (event) => {
@@ -3461,6 +3721,40 @@ function installRecorder(deps) {
3461
3721
  }
3462
3722
 
3463
3723
  // ../browser/dist/iris.js
3724
+ function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token) {
3725
+ let bridge;
3726
+ try {
3727
+ bridge = new URL(bridgeUrl);
3728
+ } catch {
3729
+ return { allowed: false, reason: "invalid Iris bridge URL" };
3730
+ }
3731
+ if (bridge.protocol !== "ws:" && bridge.protocol !== "wss:") {
3732
+ return { allowed: false, reason: "Iris bridge URL must use ws:// or wss://" };
3733
+ }
3734
+ if ((token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
3735
+ return {
3736
+ allowed: false,
3737
+ reason: `Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`
3738
+ };
3739
+ }
3740
+ const remoteBridge = !isLoopbackHostname(bridge.hostname);
3741
+ if (remoteBridge && bridge.protocol !== "wss:") {
3742
+ return { allowed: false, reason: "a non-local Iris bridge must use wss://" };
3743
+ }
3744
+ const remote = !isLoopbackHostname(pageHostname) || remoteBridge;
3745
+ if (!remote)
3746
+ return { allowed: true };
3747
+ if (!allowNonLocalhost) {
3748
+ return {
3749
+ allowed: false,
3750
+ reason: "Iris is disabled outside localhost unless allowNonLocalhost is explicitly enabled"
3751
+ };
3752
+ }
3753
+ if (token === void 0 || token.length === 0) {
3754
+ return { allowed: false, reason: "a pairing token is required outside localhost" };
3755
+ }
3756
+ return { allowed: true };
3757
+ }
3464
3758
  function str2(value, fallback = "") {
3465
3759
  return typeof value === "string" ? value : fallback;
3466
3760
  }
@@ -3489,6 +3783,7 @@ var Iris = class {
3489
3783
  #presenter;
3490
3784
  #recorder;
3491
3785
  #eventCount = 0;
3786
+ #token;
3492
3787
  /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
3493
3788
  #actHandle;
3494
3789
  connect(options = {}) {
@@ -3496,14 +3791,23 @@ var Iris = class {
3496
3791
  return;
3497
3792
  if (typeof window === "undefined" || typeof document === "undefined")
3498
3793
  return;
3499
- this.#session = resolveSessionLabel(options.session, () => `s${Date.now().toString(36)}`);
3794
+ const url = options.url ?? `ws://localhost:${String(IRIS_DEFAULT_PORT)}${IRIS_WS_PATH}`;
3795
+ const policy = connectionPolicy(window.location.hostname, url, options.allowNonLocalhost === true, options.token);
3796
+ if (!policy.allowed) {
3797
+ globalThis.console.warn(`[Iris] ${policy.reason ?? "connection blocked"}`);
3798
+ return;
3799
+ }
3800
+ this.#session = resolveSessionLabel(options.session, () => typeof globalThis.crypto?.randomUUID === "function" ? `s${globalThis.crypto.randomUUID()}` : `s${Date.now().toString(36)}`);
3801
+ this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
3500
3802
  this.#start = performance.now();
3501
3803
  this.#registry = createCommandRegistry();
3502
- const url = options.url ?? `ws://localhost:${String(IRIS_DEFAULT_PORT)}${IRIS_WS_PATH}`;
3503
3804
  this.#transport = new Transport({
3504
3805
  url,
3505
3806
  hello: () => this.#hello(),
3506
3807
  handleCommand: (command) => this.#handleCommand(command),
3808
+ // Show the presenter HUD as soon as the agent bridge connects — the user immediately sees
3809
+ // the glow border and narration panel, even before the first tool call lands.
3810
+ onConnected: () => this.#presenter?.sessionStart(),
3507
3811
  // Liveness fallback: if the bridge stays unreachable (the agent killed the server process),
3508
3812
  // no server-pushed end can arrive — so end the run we're presenting ourselves. A returning
3509
3813
  // agent revives it via the normal sessionStart() path on its next command.
@@ -3613,6 +3917,7 @@ var Iris = class {
3613
3917
  url: location.href,
3614
3918
  title: document.title,
3615
3919
  adapters: adapterNames(),
3920
+ ...this.#token === void 0 ? {} : { token: this.#token },
3616
3921
  hasCapabilities: hasCapabilities()
3617
3922
  };
3618
3923
  }