@syrin/iris 0.5.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,30 @@
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";
5
29
  var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
6
30
  var RunKind = {
7
31
  FLOW_REPLAY: "flow_replay",
@@ -43,6 +67,11 @@ var RecorderPhase = {
43
67
  ANNOTATING: "annotating"
44
68
  // recording paused, awaiting an annotation target/kind
45
69
  };
70
+ var RING_BUFFER_DEFAULTS = {
71
+ MAX_EVENTS: 2e3,
72
+ MAX_AGE_MS: 6e4,
73
+ MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
74
+ };
46
75
  var EventType = {
47
76
  DOM_ADDED: "dom.added",
48
77
  DOM_REMOVED: "dom.removed",
@@ -207,136 +236,209 @@ var MessageKind = {
207
236
  EVENT: "event"
208
237
  };
209
238
 
210
- // ../protocol/dist/types.js
239
+ // ../protocol/dist/messages.js
211
240
  import { z } from "zod";
212
- var ElementQuerySchema = z.object({
213
- by: z.nativeEnum(QueryBy).optional(),
214
- value: z.string().optional(),
215
- role: z.string().optional(),
216
- name: z.string().optional(),
217
- text: z.string().optional(),
218
- label: z.string().optional(),
219
- placeholder: z.string().optional(),
220
- testid: z.string().optional(),
221
- 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(),
222
324
  /** CSS selector or ref to scope the search. */
223
- scope: z.string().optional()
325
+ scope: z2.string().optional()
224
326
  });
225
- var CapabilityFlowSchema = z.object({
226
- name: z.string(),
227
- steps: z.array(z.string())
327
+ var CapabilityFlowSchema = z2.object({
328
+ name: z2.string(),
329
+ steps: z2.array(z2.string())
228
330
  });
229
- var CapabilitiesSchema = z.object({
230
- testids: z.array(z.string()),
231
- signals: z.array(z.string()),
232
- stores: z.array(z.string()),
233
- 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)
234
336
  });
235
- var ContractFileSchema = z.object({
236
- version: z.number(),
237
- generatedAt: z.number(),
337
+ var ContractFileSchema = z2.object({
338
+ version: z2.number(),
339
+ generatedAt: z2.number(),
238
340
  capabilities: CapabilitiesSchema
239
341
  });
240
- var RunEvidenceSchema = z.object({
241
- consoleErrors: z.number().optional(),
242
- networkErrors: z.number().optional(),
243
- driftSteps: z.number().optional()
342
+ var RunEvidenceSchema = z2.object({
343
+ consoleErrors: z2.number().optional(),
344
+ networkErrors: z2.number().optional(),
345
+ driftSteps: z2.number().optional()
244
346
  });
245
- var RunRecordSchema = z.object({
246
- kind: z.nativeEnum(RunKind),
247
- name: z.string(),
248
- status: z.nativeEnum(RunStatus),
249
- at: z.number(),
250
- 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(),
251
353
  evidence: RunEvidenceSchema.optional(),
252
- durationMs: z.number().optional()
354
+ durationMs: z2.number().optional()
253
355
  });
254
- var ProjectLearnedSchema = z.object({
255
- flows: z.array(z.string()).optional(),
256
- 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()
257
359
  });
258
- var ProjectFileSchema = z.object({
259
- version: z.number(),
360
+ var ProjectFileSchema = z2.object({
361
+ version: z2.number(),
260
362
  learned: ProjectLearnedSchema.optional(),
261
- runs: z.array(RunRecordSchema)
363
+ runs: z2.array(RunRecordSchema)
262
364
  });
263
- var FlowAnchorSchema = z.discriminatedUnion("kind", [
264
- z.object({ kind: z.literal(AnchorKind.TESTID), value: z.string().min(1) }),
265
- z.object({
266
- kind: z.literal(AnchorKind.ROLE),
267
- role: z.string().min(1),
268
- 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()
269
371
  }),
270
- 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) })
271
373
  ]);
272
- var FlowExpectSchema = z.object({
273
- signal: z.string().optional(),
374
+ var FlowExpectSchema = z2.object({
375
+ signal: z2.string().optional(),
274
376
  /**
275
377
  * Optional payload shape an `assert-signal` annotation requires the signal
276
378
  * to match (the predicate DSL's signal.dataMatches). Additive/optional — a flow file with a
277
379
  * bare `signal` still parses, and the on-disk version stays FLOW_FILE_VERSION 1.
278
380
  */
279
- signalData: z.record(z.unknown()).optional(),
280
- net: z.object({
281
- method: z.string().optional(),
282
- urlContains: z.string().optional(),
283
- 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()
284
386
  }).optional(),
285
- element: z.object({
286
- testid: z.string().optional(),
287
- role: z.string().optional(),
288
- name: z.string().optional()
387
+ element: z2.object({
388
+ testid: z2.string().optional(),
389
+ role: z2.string().optional(),
390
+ name: z2.string().optional()
289
391
  }).optional()
290
392
  });
291
- var baseFlowStep = z.object({
292
- tool: z.string(),
393
+ var baseFlowStep = z2.object({
394
+ tool: z2.string(),
293
395
  anchor: FlowAnchorSchema,
294
- action: z.nativeEnum(ActionType).optional(),
295
- args: z.record(z.unknown()).optional(),
396
+ action: z2.nativeEnum(ActionType).optional(),
397
+ args: z2.record(z2.unknown()).optional(),
296
398
  expect: FlowExpectSchema.optional(),
297
- degraded: z.boolean().optional()
399
+ degraded: z2.boolean().optional()
298
400
  });
299
401
  var FlowStepSchema = baseFlowStep.extend({
300
- steps: z.lazy(() => z.array(FlowStepSchema).optional())
402
+ steps: z2.lazy(() => z2.array(FlowStepSchema).optional())
301
403
  });
302
- var FlowFileSchema = z.object({
303
- version: z.literal(FLOW_FILE_VERSION),
304
- name: z.string(),
404
+ var FlowFileSchema = z2.object({
405
+ version: z2.literal(FLOW_FILE_VERSION),
406
+ name: z2.string(),
305
407
  // FUTURE: fixtures/preconditions — schema slot reserved, unpopulated this cut. The recorder
306
408
  // never writes it and no fixture runner exists.
307
- fixture: z.string().optional(),
409
+ fixture: z2.string().optional(),
308
410
  /** From the injected clock (ms) — deterministic in tests, byte-stable on disk. */
309
- createdAt: z.number(),
310
- steps: z.array(FlowStepSchema),
411
+ createdAt: z2.number(),
412
+ steps: z2.array(FlowStepSchema),
311
413
  success: FlowExpectSchema.optional(),
312
414
  /**
313
415
  * Anchors whose CONTENT must not be asserted (e.g. LLM output). Replay asserts
314
416
  * presence, not words. Compiled from a `mark-dynamic` annotation.
315
417
  */
316
- dynamic: z.array(FlowAnchorSchema).optional()
418
+ dynamic: z2.array(FlowAnchorSchema).optional()
317
419
  });
318
- var RecordedFlowSchema = z.object({
319
- name: z.string(),
420
+ var RecordedFlowSchema = z2.object({
421
+ name: z2.string(),
320
422
  flow: FlowFileSchema
321
423
  });
322
- var AnnotationSchema = z.discriminatedUnion("kind", [
323
- z.object({
324
- kind: z.literal(AnnotationKind.ASSERT_SIGNAL),
325
- name: z.string().min(1),
326
- 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()
327
429
  }),
328
- z.object({
329
- kind: z.literal(AnnotationKind.ASSERT_VISIBLE),
330
- testid: z.string().min(1)
430
+ z2.object({
431
+ kind: z2.literal(AnnotationKind.ASSERT_VISIBLE),
432
+ testid: z2.string().min(1)
331
433
  }),
332
- z.object({
333
- kind: z.literal(AnnotationKind.MARK_DYNAMIC),
334
- testid: z.string().min(1)
434
+ z2.object({
435
+ kind: z2.literal(AnnotationKind.MARK_DYNAMIC),
436
+ testid: z2.string().min(1)
335
437
  }),
336
- z.object({
337
- kind: z.literal(AnnotationKind.SUCCESS_STATE),
338
- signal: z.string().min(1).optional(),
339
- 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()
340
442
  })
341
443
  ]);
342
444
 
@@ -371,6 +473,96 @@ var RefRegistry = class {
371
473
  };
372
474
  var refs = new RefRegistry();
373
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
+
374
566
  // ../browser/dist/dom/a11y.js
375
567
  var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
376
568
  "button",
@@ -537,6 +729,17 @@ function getStates(el) {
537
729
  }
538
730
  function getValue(el) {
539
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
+ }
540
743
  return el.value;
541
744
  }
542
745
  const valueNow = el.getAttribute("aria-valuenow");
@@ -1041,6 +1244,30 @@ var result = (ref, action, effect, settled, settleReason, warning) => {
1041
1244
  var FILL_LIKE = /* @__PURE__ */ new Set([ActionType.FILL, ActionType.TYPE, ActionType.CLEAR]);
1042
1245
  var isFillLike = (action) => FILL_LIKE.has(action);
1043
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
+ }
1044
1271
  var NO_GEOMETRY = { occluded: false, occludedBy: null, scrolledIntoView: false };
1045
1272
  function fireClickSequence(el) {
1046
1273
  const doc = el.ownerDocument;
@@ -1207,6 +1434,7 @@ async function dispatchFor(el, action, args) {
1207
1434
  }
1208
1435
  async function executeAction(ref, action, args = {}) {
1209
1436
  const el = requireElement(ref);
1437
+ assertActionAllowed(el, action, args);
1210
1438
  const visible = isVisible(el);
1211
1439
  const enabled = enabledOf(el);
1212
1440
  const prevFocus = activeRef(el);
@@ -1313,7 +1541,10 @@ async function dragElement(source, target, data) {
1313
1541
  }
1314
1542
  return dropPrevented;
1315
1543
  }
1316
- 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
+ }
1317
1548
  const mc = navigator.modelContext;
1318
1549
  if (mc === void 0 || typeof mc.callTool !== "function") {
1319
1550
  throw new Error("WebMCP (navigator.modelContext) not available on this page");
@@ -1360,7 +1591,7 @@ function readStores(only) {
1360
1591
  if (only !== void 0 && name !== only)
1361
1592
  continue;
1362
1593
  try {
1363
- out[name] = getter();
1594
+ out[name] = sanitizeForTransport(getter());
1364
1595
  } catch (error) {
1365
1596
  out[name] = { __error: error instanceof Error ? error.message : String(error) };
1366
1597
  }
@@ -1509,6 +1740,9 @@ function inspect(ref) {
1509
1740
  return {
1510
1741
  ...describe(el),
1511
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,
1512
1746
  box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1513
1747
  styles,
1514
1748
  component
@@ -1553,6 +1787,16 @@ function listAnimations() {
1553
1787
  });
1554
1788
  return { animations };
1555
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
+ }
1556
1800
  function createCommandRegistry() {
1557
1801
  const reg = /* @__PURE__ */ new Map();
1558
1802
  reg.set(IrisCommand.SNAPSHOT, (args) => buildSnapshot({
@@ -1565,7 +1809,7 @@ function createCommandRegistry() {
1565
1809
  const action = str(args["action"]) ?? "";
1566
1810
  if (action === ActionType.WEBMCP) {
1567
1811
  const inner = record(args["args"]);
1568
- return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]));
1812
+ return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]), inner[DANGEROUS_ACTION_CONFIRM_ARG] === true);
1569
1813
  }
1570
1814
  return executeAction(str(args["ref"]) ?? "", action, record(args["args"]));
1571
1815
  });
@@ -1592,9 +1836,12 @@ function createCommandRegistry() {
1592
1836
  return scrollContainer(str(args["ref"]), typeof dy === "number" ? dy : void 0, typeof fraction === "number" ? fraction : void 0);
1593
1837
  });
1594
1838
  reg.set(IrisCommand.NAVIGATE, (args) => {
1595
- const url = str(args["url"]);
1596
- if (url === void 0 || url.length === 0)
1839
+ const rawUrl = str(args["url"]);
1840
+ if (rawUrl === void 0 || rawUrl.length === 0)
1597
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" };
1598
1845
  window.location.assign(url);
1599
1846
  return { ok: true, url };
1600
1847
  });
@@ -1687,12 +1934,23 @@ var Transport = class {
1687
1934
  } catch {
1688
1935
  return;
1689
1936
  }
1690
- const msg = parsed;
1691
- 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)
1692
1943
  return;
1693
- const command = parsed;
1694
- const outcome = await this.#deps.handleCommand(command);
1695
- 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({
1696
1954
  kind: MessageKind.COMMAND_RESULT,
1697
1955
  id: command.id,
1698
1956
  ok: outcome.ok,
@@ -1701,7 +1959,7 @@ var Transport = class {
1701
1959
  }));
1702
1960
  }
1703
1961
  sendEvent(event) {
1704
- this.#sendRaw(JSON.stringify({ kind: MessageKind.EVENT, event }));
1962
+ this.#sendRaw(safeStringify({ kind: MessageKind.EVENT, event }));
1705
1963
  }
1706
1964
  #sendRaw(text) {
1707
1965
  if (this.#ws !== void 0 && this.#ws.readyState === WebSocket.OPEN) {
@@ -1820,13 +2078,14 @@ function methodOf(input, init) {
1820
2078
  return "GET";
1821
2079
  }
1822
2080
  function installNetwork(emit) {
1823
- const origFetch = window.fetch.bind(window);
2081
+ const origFetch = window.fetch;
2082
+ const callFetch = origFetch.bind(window);
1824
2083
  window.fetch = async (input, init) => {
1825
2084
  const start = performance.now();
1826
2085
  const method = methodOf(input, init);
1827
2086
  const url = urlOf(input);
1828
2087
  try {
1829
- const res = await origFetch(input, init);
2088
+ const res = await callFetch(input, init);
1830
2089
  emit(EventType.NET_REQUEST, {
1831
2090
  method,
1832
2091
  url,
@@ -1892,8 +2151,10 @@ function snapshotLocation() {
1892
2151
  };
1893
2152
  }
1894
2153
  function installRoute(emit) {
1895
- const origPush = history.pushState.bind(history);
1896
- 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);
1897
2158
  const fire = (from) => {
1898
2159
  const to = snapshotLocation();
1899
2160
  if (to.href === from)
@@ -1908,12 +2169,12 @@ function installRoute(emit) {
1908
2169
  };
1909
2170
  history.pushState = (data, unused, url) => {
1910
2171
  const from = location.href;
1911
- origPush(data, unused, url ?? null);
2172
+ callPush(data, unused, url ?? null);
1912
2173
  fire(from);
1913
2174
  };
1914
2175
  history.replaceState = (data, unused, url) => {
1915
2176
  const from = location.href;
1916
- origReplace(data, unused, url ?? null);
2177
+ callReplace(data, unused, url ?? null);
1917
2178
  fire(from);
1918
2179
  };
1919
2180
  let lastHref = location.href;
@@ -1943,22 +2204,19 @@ function stringifyArgs(args) {
1943
2204
  return a;
1944
2205
  if (a instanceof Error)
1945
2206
  return a.message;
1946
- try {
1947
- return JSON.stringify(a);
1948
- } catch {
1949
- return String(a);
1950
- }
2207
+ return safeStringify(a);
1951
2208
  }).join(" ");
1952
2209
  }
1953
2210
  function installConsole(emit) {
1954
2211
  const methods = ["log", "warn", "error"];
1955
2212
  const originals2 = /* @__PURE__ */ new Map();
1956
2213
  for (const method of methods) {
1957
- const original = console[method].bind(console);
2214
+ const original = console[method];
1958
2215
  originals2.set(method, original);
2216
+ const callOriginal = original.bind(console);
1959
2217
  console[method] = (...args) => {
1960
2218
  emit(METHOD_EVENT[method], { message: stringifyArgs(args) });
1961
- original(...args);
2219
+ callOriginal(...args);
1962
2220
  };
1963
2221
  }
1964
2222
  const onError = (event) => {
@@ -3463,6 +3721,40 @@ function installRecorder(deps) {
3463
3721
  }
3464
3722
 
3465
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
+ }
3466
3758
  function str2(value, fallback = "") {
3467
3759
  return typeof value === "string" ? value : fallback;
3468
3760
  }
@@ -3491,6 +3783,7 @@ var Iris = class {
3491
3783
  #presenter;
3492
3784
  #recorder;
3493
3785
  #eventCount = 0;
3786
+ #token;
3494
3787
  /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
3495
3788
  #actHandle;
3496
3789
  connect(options = {}) {
@@ -3498,10 +3791,16 @@ var Iris = class {
3498
3791
  return;
3499
3792
  if (typeof window === "undefined" || typeof document === "undefined")
3500
3793
  return;
3501
- 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;
3502
3802
  this.#start = performance.now();
3503
3803
  this.#registry = createCommandRegistry();
3504
- const url = options.url ?? `ws://localhost:${String(IRIS_DEFAULT_PORT)}${IRIS_WS_PATH}`;
3505
3804
  this.#transport = new Transport({
3506
3805
  url,
3507
3806
  hello: () => this.#hello(),
@@ -3618,6 +3917,7 @@ var Iris = class {
3618
3917
  url: location.href,
3619
3918
  title: document.title,
3620
3919
  adapters: adapterNames(),
3920
+ ...this.#token === void 0 ? {} : { token: this.#token },
3621
3921
  hasCapabilities: hasCapabilities()
3622
3922
  };
3623
3923
  }