@termwright/protocol 0.2.0

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 ADDED
@@ -0,0 +1,1886 @@
1
+ // src/env.ts
2
+ import { randomBytes } from "crypto";
3
+ var ENV_ENDPOINT = "TERMWRIGHT_ENDPOINT";
4
+ var ENV_TOKEN = "TERMWRIGHT_TOKEN";
5
+ var ENV_PROTOCOL = "TERMWRIGHT_PROTOCOL";
6
+ var PROTOCOL_VERSION = 1;
7
+ var PROTOCOL_ID = "termwright/1";
8
+ var PROTOCOL_V2_ID = "termwright/2";
9
+ var SUPPORTED_PROTOCOL_IDS = [PROTOCOL_V2_ID, PROTOCOL_ID];
10
+ var TOKEN_BYTES = 32;
11
+ function generateToken() {
12
+ return randomBytes(TOKEN_BYTES).toString("base64url");
13
+ }
14
+
15
+ // src/errors.ts
16
+ var ProtocolViolation = class extends Error {
17
+ /** Machine-readable reason. */
18
+ code;
19
+ constructor(code, message) {
20
+ super(message);
21
+ this.name = "ProtocolViolation";
22
+ this.code = code;
23
+ }
24
+ };
25
+
26
+ // src/roles.ts
27
+ var SEMANTIC_ROLES = [
28
+ "application",
29
+ "region",
30
+ "dialog",
31
+ "alert",
32
+ "status",
33
+ "list",
34
+ "listitem",
35
+ "menu",
36
+ "menuitem",
37
+ "button",
38
+ "checkbox",
39
+ "radio",
40
+ "tab",
41
+ "textbox",
42
+ "heading",
43
+ "text",
44
+ "progressbar",
45
+ "separator",
46
+ "scrollbar",
47
+ "table",
48
+ "row",
49
+ "cell",
50
+ "generic"
51
+ ];
52
+ var SEMANTIC_ACTIONS = [
53
+ "focus",
54
+ "activate",
55
+ "toggle",
56
+ "setValue",
57
+ "scroll",
58
+ "select",
59
+ "expand"
60
+ ];
61
+
62
+ // src/limits.ts
63
+ var DEFAULT_LIMITS = Object.freeze({
64
+ maxFrameBytes: 1 * 1024 * 1024,
65
+ maxSnapshotBytes: 2 * 1024 * 1024,
66
+ maxNodes: 5e3,
67
+ maxDepth: 64,
68
+ maxStringBytes: 16 * 1024,
69
+ maxRelationTargets: 64,
70
+ maxQueuedFrames: 32,
71
+ maxPendingWaiters: 256,
72
+ maxSessions: 16,
73
+ maxLogRecordBytes: 32 * 1024,
74
+ maxLogQueue: 1e3
75
+ });
76
+ var ABSOLUTE_LIMITS = Object.freeze({
77
+ maxFrameBytes: 8 * 1024 * 1024,
78
+ maxSnapshotBytes: 8 * 1024 * 1024,
79
+ maxNodes: 5e4,
80
+ maxDepth: 256,
81
+ maxStringBytes: 256 * 1024,
82
+ maxRelationTargets: 1024,
83
+ maxQueuedFrames: 256,
84
+ maxPendingWaiters: 4096,
85
+ maxSessions: 128,
86
+ maxLogRecordBytes: 256 * 1024,
87
+ maxLogQueue: 1e4
88
+ });
89
+ var DEFAULT_NEGOTIATION_MS = 250;
90
+
91
+ // src/observation.ts
92
+ function intersectRects(a, b) {
93
+ const row = Math.max(a.row, b.row);
94
+ const column = Math.max(a.column, b.column);
95
+ return {
96
+ row,
97
+ column,
98
+ width: Math.max(0, Math.min(a.column + a.width, b.column + b.width) - column),
99
+ height: Math.max(0, Math.min(a.row + a.height, b.row + b.height) - row)
100
+ };
101
+ }
102
+ function rectArea(rect) {
103
+ return Math.max(0, rect.width) * Math.max(0, rect.height);
104
+ }
105
+ function viewportIntersection(rect, columns, rows) {
106
+ const intersection = intersectRects(rect, { row: 0, column: 0, width: columns, height: rows });
107
+ const area = rectArea(rect);
108
+ const visible = rectArea(intersection);
109
+ return Object.freeze({
110
+ rect: Object.freeze(intersection),
111
+ ratio: area === 0 ? 0 : visible / area,
112
+ fullyInside: area > 0 && visible === area
113
+ });
114
+ }
115
+ function spatialRelation(a, relation, b) {
116
+ const aBottom = a.row + a.height;
117
+ const bBottom = b.row + b.height;
118
+ const aRight = a.column + a.width;
119
+ const bRight = b.column + b.width;
120
+ switch (relation) {
121
+ case "contains":
122
+ return a.row <= b.row && a.column <= b.column && aBottom >= bBottom && aRight >= bRight;
123
+ case "inside":
124
+ return spatialRelation(b, "contains", a);
125
+ case "overlaps":
126
+ return rectArea(intersectRects(a, b)) > 0;
127
+ case "left-of":
128
+ return aRight <= b.column;
129
+ case "right-of":
130
+ return bRight <= a.column;
131
+ case "above":
132
+ return aBottom <= b.row;
133
+ case "below":
134
+ return bBottom <= a.row;
135
+ case "aligned-left":
136
+ return a.column === b.column;
137
+ case "aligned-right":
138
+ return aRight === bRight;
139
+ case "aligned-top":
140
+ return a.row === b.row;
141
+ case "aligned-bottom":
142
+ return aBottom === bBottom;
143
+ case "adjacent-horizontal":
144
+ return (aRight === b.column || bRight === a.column) && Math.max(a.row, b.row) < Math.min(aBottom, bBottom);
145
+ case "adjacent-vertical":
146
+ return (aBottom === b.row || bBottom === a.row) && Math.max(a.column, b.column) < Math.min(aRight, bRight);
147
+ }
148
+ }
149
+
150
+ // src/geometry-capabilities.ts
151
+ var FRAMEWORK_OBSERVATION_CAPABILITIES = Object.freeze([
152
+ { framework: "generic", identity: "none", attached: "supported", displayed: "supported", intendedRect: "supported", visibleRect: "supported", hitTest: "conditional", reason: "Grid matches are physical cells; pointer delivery still requires terminal mouse mode." },
153
+ { framework: "textual", identity: "stable", attached: "supported", displayed: "supported", intendedRect: "supported", visibleRect: "supported", hitTest: "supported", reason: "The compositor exposes intended/clipped regions and Screen.get_widget_at(), the same fresh-pointer routing lookup." },
154
+ { framework: "opentui", identity: "stable", attached: "supported", displayed: "supported", intendedRect: "supported", visibleRect: "unsupported", hitTest: "supported", reason: "The committed native hit grid proves fresh-pointer ownership; the renderer exposes no per-node visual clip rectangle." },
155
+ { framework: "ink", identity: "stable", attached: "supported", displayed: "supported", intendedRect: "conditional", visibleRect: "unsupported", hitTest: "unsupported", reason: "Intended bounds are conditional on a viewport-stable live region; Ink exposes neither clipping nor pointer ownership." },
156
+ { framework: "tview", identity: "stable", attached: "supported", displayed: "supported", intendedRect: "supported", visibleRect: "conditional", hitTest: "unsupported", reason: "Primitive rectangles do not identify the recipient after overlap." },
157
+ { framework: "ratatui", identity: "frame-local", attached: "supported", displayed: "conditional", intendedRect: "supported", visibleRect: "conditional", hitTest: "unsupported", reason: "Render areas are frame-local and buffer writes do not preserve widget ownership." },
158
+ { framework: "charm", identity: "frame-local", attached: "supported", displayed: "conditional", intendedRect: "unsupported", visibleRect: "unsupported", hitTest: "unsupported", reason: "Bubble Tea hands over a rendered string without attributable widget geometry." }
159
+ ]);
160
+ function frameworkObservationCapabilities(framework) {
161
+ return FRAMEWORK_OBSERVATION_CAPABILITIES.find((entry) => entry.framework === framework);
162
+ }
163
+ var weakest = (...values) => values.includes("unsupported") ? "unsupported" : values.includes("conditional") ? "conditional" : "supported";
164
+ var FRAMEWORK_OPERATION_CAPABILITIES = Object.freeze(
165
+ FRAMEWORK_OBSERVATION_CAPABILITIES.flatMap((row) => {
166
+ const visibility = weakest(row.displayed, row.visibleRect);
167
+ const viewport = weakest(row.intendedRect, row.visibleRect);
168
+ const reason = row.reason;
169
+ return [
170
+ { framework: row.framework, operation: "keyboard-actions", availability: "supported", reason: "Keyboard input is sent through the PTY and does not require geometry." },
171
+ {
172
+ framework: row.framework,
173
+ operation: "pointer-actions",
174
+ availability: row.hitTest === "unsupported" ? "unsupported" : "conditional",
175
+ reason: row.hitTest === "unsupported" ? reason : "Requires an exact hit recipient and terminal mouse reporting enabled by the application."
176
+ },
177
+ { framework: row.framework, operation: "toBeAttached", availability: "supported", reason: "Tree membership is observed directly." },
178
+ { framework: row.framework, operation: "toBeDetached", availability: "supported", reason: "Tree absence is observed directly without coercing missing layout facts." },
179
+ { framework: row.framework, operation: "toBeDisplayed", availability: row.displayed, reason },
180
+ { framework: row.framework, operation: "toBeHidden", availability: row.displayed, reason },
181
+ { framework: row.framework, operation: "toBeVisible", availability: visibility, reason },
182
+ { framework: row.framework, operation: "toBeOffscreen", availability: viewport, reason },
183
+ { framework: row.framework, operation: "toBeInViewport", availability: viewport, reason },
184
+ { framework: row.framework, operation: "toReceivePointerEvents", availability: row.hitTest, reason },
185
+ { framework: row.framework, operation: "toHaveBounds", availability: row.intendedRect, reason },
186
+ { framework: row.framework, operation: "toHaveSpatialRelation", availability: row.intendedRect, reason },
187
+ { framework: row.framework, operation: "cellSnapshot", availability: row.visibleRect, reason }
188
+ ];
189
+ })
190
+ );
191
+
192
+ // src/node-schema.ts
193
+ import { Buffer } from "buffer";
194
+ import { z } from "zod";
195
+
196
+ // src/probe/ir.ts
197
+ var PROBE_UNOBSERVABLE_FIELDS = [
198
+ "focused",
199
+ "disabled",
200
+ "checked",
201
+ "expanded",
202
+ "readonly",
203
+ "selected",
204
+ "busy",
205
+ "multiline",
206
+ "displayed",
207
+ "value",
208
+ "selectedIndex",
209
+ "textSelection",
210
+ "scroll",
211
+ "scrollExtent",
212
+ "intendedRect",
213
+ "visibleRect",
214
+ "paintOrder",
215
+ "text",
216
+ "parent"
217
+ ];
218
+ var PROBE_CAPABILITIES = [
219
+ /** Identities survive across frames and may be correlated. */
220
+ "stable-identity",
221
+ /** `visibleRect` is computed, not guessed. */
222
+ "visible-rect",
223
+ /** A render/layout call stream is reported. */
224
+ "operations",
225
+ /** Author annotations are readable. */
226
+ "annotations",
227
+ /** A frame-start signal is emitted. Absent for most frameworks — see below. */
228
+ "frame-begin",
229
+ /** `paintOrder` is reported, so occlusion can be reasoned about. */
230
+ "paint-order"
231
+ ];
232
+ var PROVENANCE_SOURCES = [
233
+ "annotation",
234
+ "recognizer",
235
+ "framework",
236
+ "correlation",
237
+ "heuristic"
238
+ ];
239
+
240
+ // src/node-schema.ts
241
+ function safeInt() {
242
+ return z.number().refine(Number.isSafeInteger, "expected a safe integer");
243
+ }
244
+ function nonNegativeInt() {
245
+ return z.number().refine((n) => Number.isSafeInteger(n) && n >= 0, "expected a non-negative safe integer");
246
+ }
247
+ function positiveInt() {
248
+ return z.number().refine((n) => Number.isSafeInteger(n) && n > 0, "expected a positive safe integer");
249
+ }
250
+ function boundedString(maxStringBytes) {
251
+ return z.string().refine(
252
+ (s) => Buffer.byteLength(s, "utf8") <= maxStringBytes,
253
+ `expected at most ${maxStringBytes} UTF-8 bytes`
254
+ );
255
+ }
256
+ var cache = /* @__PURE__ */ new WeakMap();
257
+ function build(limits) {
258
+ const text = boundedString(limits.maxStringBytes);
259
+ const relations = z.array(text).max(limits.maxRelationTargets);
260
+ const rect = z.strictObject({
261
+ row: safeInt(),
262
+ column: safeInt(),
263
+ width: nonNegativeInt(),
264
+ height: nonNegativeInt()
265
+ });
266
+ const observation = (value) => z.discriminatedUnion("status", [
267
+ z.strictObject({ status: z.literal("known"), value, evidence: z.enum(["adapter", "probe", "terminal-grid", "viewport-clip", "paint-order", "hit-grid", "legacy-v1"]) }),
268
+ z.strictObject({ status: z.literal("absent"), reason: z.enum(["detached", "not-displayed", "not-laid-out"]) }),
269
+ z.strictObject({ status: z.literal("unknown"), reason: z.enum(["not-reported", "temporary", "clip-unobservable", "legacy-unqualified"]) }),
270
+ z.strictObject({ status: z.literal("unsupported"), capability: text, reason: z.enum(["capability", "framework-unobservable", "not-negotiated"]) })
271
+ ]);
272
+ const state = z.strictObject({
273
+ disabled: z.boolean().optional(),
274
+ focused: z.boolean().optional(),
275
+ selected: z.boolean().optional(),
276
+ checked: z.union([z.boolean(), z.literal("mixed")]).optional(),
277
+ expanded: z.boolean().optional(),
278
+ modal: z.boolean().optional(),
279
+ busy: z.boolean().optional(),
280
+ hidden: z.boolean().optional(),
281
+ offscreen: z.boolean().optional(),
282
+ readonly: z.boolean().optional(),
283
+ multiline: z.boolean().optional(),
284
+ orientation: z.union([z.literal("horizontal"), z.literal("vertical")]).optional(),
285
+ level: positiveInt().optional(),
286
+ positionInSet: positiveInt().optional(),
287
+ setSize: nonNegativeInt().optional(),
288
+ scrollOffset: nonNegativeInt().optional(),
289
+ scrollExtent: nonNegativeInt().optional()
290
+ });
291
+ const textRange = z.strictObject({
292
+ startOffset: nonNegativeInt(),
293
+ endOffset: nonNegativeInt(),
294
+ rect
295
+ });
296
+ const extendedValue = z.lazy(
297
+ () => z.union([
298
+ z.null(),
299
+ z.boolean(),
300
+ z.number().finite().refine(
301
+ (value) => Math.abs(value) <= Number.MAX_SAFE_INTEGER,
302
+ "expected a finite JSON number in the safe range"
303
+ ),
304
+ text,
305
+ z.array(extendedValue).max(limits.maxRelationTargets),
306
+ z.record(text, extendedValue).refine(
307
+ (value) => Object.keys(value).length <= limits.maxRelationTargets,
308
+ `expected at most ${limits.maxRelationTargets} properties`
309
+ )
310
+ ])
311
+ );
312
+ const extended = z.record(text, extendedValue).refine(
313
+ (value) => Object.keys(value).length <= limits.maxRelationTargets,
314
+ `expected at most ${limits.maxRelationTargets} properties`
315
+ );
316
+ const nodeFields = {
317
+ id: text.refine((s) => s.length > 0, "node id must not be empty"),
318
+ parentId: text.optional(),
319
+ role: z.enum(SEMANTIC_ROLES),
320
+ name: text,
321
+ description: text.optional(),
322
+ value: text.optional(),
323
+ bounds: rect.optional(),
324
+ state: state.optional(),
325
+ extended: extended.optional(),
326
+ actions: z.array(z.enum(SEMANTIC_ACTIONS)).max(SEMANTIC_ACTIONS.length).optional(),
327
+ labelledBy: relations.optional(),
328
+ describedBy: relations.optional(),
329
+ textRanges: z.array(textRange).max(limits.maxRelationTargets).optional(),
330
+ testId: text.optional(),
331
+ frameworkType: text.optional(),
332
+ occlusion: z.enum(["known", "unknown"]).optional(),
333
+ p: z.enum(PROVENANCE_SOURCES).optional(),
334
+ px: z.record(text, z.enum(PROVENANCE_SOURCES)).optional()
335
+ };
336
+ const node = z.strictObject(nodeFields);
337
+ const geometry = z.strictObject({
338
+ displayed: observation(z.boolean()),
339
+ intendedRect: observation(rect),
340
+ visibleRect: observation(rect)
341
+ });
342
+ const nodeV2 = z.strictObject({
343
+ ...nodeFields,
344
+ bounds: z.never().optional(),
345
+ occlusion: z.never().optional(),
346
+ geometry
347
+ });
348
+ const cursor = z.strictObject({
349
+ row: nonNegativeInt(),
350
+ column: nonNegativeInt(),
351
+ visible: z.boolean(),
352
+ shape: z.union([z.literal("block"), z.literal("underline"), z.literal("bar")]).optional()
353
+ });
354
+ const snapshotV1 = z.strictObject({
355
+ v: z.literal(1),
356
+ sessionId: text.refine((s) => s.length > 0, "sessionId must not be empty"),
357
+ revision: positiveInt(),
358
+ columns: positiveInt(),
359
+ rows: positiveInt(),
360
+ cursor: cursor.optional(),
361
+ rootIds: z.array(text).max(limits.maxNodes),
362
+ nodes: z.array(node).max(limits.maxNodes)
363
+ });
364
+ const hitRun = z.strictObject({
365
+ rect: z.strictObject({
366
+ row: nonNegativeInt(),
367
+ column: nonNegativeInt(),
368
+ width: positiveInt(),
369
+ height: z.literal(1)
370
+ }),
371
+ recipientId: text
372
+ });
373
+ const hitGrid = z.strictObject({
374
+ // Canonical row runs make ambiguity validation linear and keep hostile
375
+ // snapshots from forcing an O(n²) rectangle-overlap check.
376
+ regions: z.array(hitRun).max(limits.maxNodes).superRefine((regions, ctx) => {
377
+ let previous;
378
+ for (let index = 0; index < regions.length; index += 1) {
379
+ const current = regions[index];
380
+ if (previous !== void 0 && (current.rect.row < previous.rect.row || current.rect.row === previous.rect.row && current.rect.column < previous.rect.column + previous.rect.width)) {
381
+ ctx.addIssue({
382
+ code: "custom",
383
+ path: [index, "rect"],
384
+ message: "hit regions must be non-overlapping row-major runs"
385
+ });
386
+ return;
387
+ }
388
+ previous = current;
389
+ }
390
+ })
391
+ });
392
+ const snapshotV2 = z.strictObject({
393
+ v: z.literal(2),
394
+ sessionId: text.refine((s) => s.length > 0, "sessionId must not be empty"),
395
+ revision: positiveInt(),
396
+ columns: positiveInt(),
397
+ rows: positiveInt(),
398
+ cursor: cursor.optional(),
399
+ rootIds: z.array(text).max(limits.maxNodes),
400
+ nodes: z.array(nodeV2).max(limits.maxNodes),
401
+ coordinateSpace: observation(z.enum(["viewport-cells", "framework-local-cells"])),
402
+ hitGrid: observation(hitGrid)
403
+ });
404
+ const snapshot = z.discriminatedUnion("v", [snapshotV1, snapshotV2]);
405
+ return {
406
+ text,
407
+ node,
408
+ cursor,
409
+ snapshot,
410
+ snapshotV1,
411
+ snapshotV2,
412
+ nodeKeys: Object.freeze(Object.keys(node.shape)),
413
+ stateKeys: Object.freeze(Object.keys(state.shape))
414
+ };
415
+ }
416
+ function treeSchemas(limits) {
417
+ const cached = cache.get(limits);
418
+ if (cached !== void 0) return cached;
419
+ const built = build(limits);
420
+ cache.set(limits, built);
421
+ return built;
422
+ }
423
+
424
+ // src/node-keys.ts
425
+ var schemas = treeSchemas(DEFAULT_LIMITS);
426
+ var SEMANTIC_NODE_KEYS = Object.freeze(
427
+ schemas.nodeKeys
428
+ );
429
+ var SEMANTIC_STATE_KEYS = Object.freeze(
430
+ schemas.stateKeys
431
+ );
432
+
433
+ // src/logs.ts
434
+ import { Buffer as Buffer2 } from "buffer";
435
+
436
+ // src/framing.ts
437
+ import { types } from "util";
438
+ var FRAME_HEADER_BYTES = 4;
439
+ var FRAME_PROJECTION_DEPTH = DEFAULT_LIMITS.maxDepth;
440
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
441
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
442
+ var encoder = new TextEncoder();
443
+ var decoder = new TextDecoder("utf-8", { fatal: true });
444
+ function assertPositiveByteCeiling(maxFrameBytes) {
445
+ if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0) {
446
+ throw new ProtocolViolation(
447
+ "frame-malformed",
448
+ "maxFrameBytes must be a positive safe integer"
449
+ );
450
+ }
451
+ }
452
+ var BufferedFrameDecoder = class {
453
+ #maxFrameBytes;
454
+ #buffer;
455
+ /** Offset of the first unconsumed byte in `#buffer`. */
456
+ #start = 0;
457
+ /** Offset just past the last buffered byte in `#buffer`. */
458
+ #end = 0;
459
+ #failure = null;
460
+ constructor(maxFrameBytes) {
461
+ assertPositiveByteCeiling(maxFrameBytes);
462
+ this.#maxFrameBytes = maxFrameBytes;
463
+ this.#buffer = new Uint8Array(0);
464
+ }
465
+ get buffered() {
466
+ return this.#end - this.#start;
467
+ }
468
+ push(chunk) {
469
+ if (this.#failure !== null) {
470
+ throw new ProtocolViolation(
471
+ "decoder-poisoned",
472
+ `decoder failed earlier (${this.#failure.code}) and accepts no further input`
473
+ );
474
+ }
475
+ try {
476
+ return this.#pushOrThrow(chunk);
477
+ } catch (error) {
478
+ this.#failure = error instanceof ProtocolViolation ? error : new ProtocolViolation("frame-malformed", "frame decoding failed");
479
+ this.#buffer = new Uint8Array(0);
480
+ this.#start = 0;
481
+ this.#end = 0;
482
+ throw this.#failure;
483
+ }
484
+ }
485
+ #pushOrThrow(chunk) {
486
+ this.#append(chunk);
487
+ const messages = [];
488
+ for (; ; ) {
489
+ const available = this.#end - this.#start;
490
+ if (available < FRAME_HEADER_BYTES) break;
491
+ const length = this.#readLength();
492
+ if (length === 0) {
493
+ throw new ProtocolViolation("frame-malformed", "frame length must be non-zero");
494
+ }
495
+ if (length > this.#maxFrameBytes) {
496
+ throw new ProtocolViolation(
497
+ "frame-oversized",
498
+ `frame declares ${length} bytes, ceiling is ${this.#maxFrameBytes}`
499
+ );
500
+ }
501
+ if (available < FRAME_HEADER_BYTES + length) break;
502
+ const bodyStart = this.#start + FRAME_HEADER_BYTES;
503
+ const body = this.#buffer.subarray(bodyStart, bodyStart + length);
504
+ messages.push(decodeBody(body));
505
+ this.#start = bodyStart + length;
506
+ }
507
+ this.#compact();
508
+ if (this.buffered > this.#maxFrameBytes + FRAME_HEADER_BYTES) {
509
+ throw new ProtocolViolation(
510
+ "frame-oversized",
511
+ `buffered ${this.buffered} bytes without a complete frame`
512
+ );
513
+ }
514
+ return messages;
515
+ }
516
+ #readLength() {
517
+ const b = this.#buffer;
518
+ const i = this.#start;
519
+ return b[i] * 16777216 + (b[i + 1] << 16 | b[i + 2] << 8 | b[i + 3]) >>> 0;
520
+ }
521
+ #append(chunk) {
522
+ const kept = this.#end - this.#start;
523
+ const needed = kept + chunk.length;
524
+ if (needed > this.#buffer.length - this.#start) {
525
+ const next = new Uint8Array(needed);
526
+ next.set(this.#buffer.subarray(this.#start, this.#end), 0);
527
+ this.#buffer = next;
528
+ this.#start = 0;
529
+ this.#end = kept;
530
+ }
531
+ this.#buffer.set(chunk, this.#end);
532
+ this.#end += chunk.length;
533
+ }
534
+ #compact() {
535
+ if (this.#start === 0) return;
536
+ const kept = this.#end - this.#start;
537
+ if (kept === 0) {
538
+ this.#buffer = new Uint8Array(0);
539
+ } else {
540
+ const next = new Uint8Array(kept);
541
+ next.set(this.#buffer.subarray(this.#start, this.#end), 0);
542
+ this.#buffer = next;
543
+ }
544
+ this.#start = 0;
545
+ this.#end = kept;
546
+ }
547
+ };
548
+ function decodeBody(body) {
549
+ let text;
550
+ try {
551
+ text = decoder.decode(body);
552
+ } catch {
553
+ throw new ProtocolViolation("frame-encoding", "frame body is not valid UTF-8");
554
+ }
555
+ let parsed;
556
+ try {
557
+ parsed = JSON.parse(text);
558
+ } catch {
559
+ throw new ProtocolViolation("frame-malformed", "frame body is not valid JSON");
560
+ }
561
+ return projectDto(parsed, FRAME_PROJECTION_DEPTH);
562
+ }
563
+ function createFrameDecoder(maxFrameBytes) {
564
+ return new BufferedFrameDecoder(maxFrameBytes);
565
+ }
566
+ function encodeFrame(message, maxFrameBytes) {
567
+ assertPositiveByteCeiling(maxFrameBytes);
568
+ let text;
569
+ try {
570
+ text = JSON.stringify(message);
571
+ } catch {
572
+ throw new ProtocolViolation("frame-malformed", "message is not JSON-serialisable");
573
+ }
574
+ if (text === void 0) {
575
+ throw new ProtocolViolation("dto-scalar", "message serialises to undefined");
576
+ }
577
+ const body = encoder.encode(text);
578
+ if (body.length > maxFrameBytes) {
579
+ throw new ProtocolViolation(
580
+ "frame-oversized",
581
+ `encoded frame is ${body.length} bytes, ceiling is ${maxFrameBytes}`
582
+ );
583
+ }
584
+ const frame = new Uint8Array(FRAME_HEADER_BYTES + body.length);
585
+ const n = body.length;
586
+ frame[0] = n >>> 24 & 255;
587
+ frame[1] = n >>> 16 & 255;
588
+ frame[2] = n >>> 8 & 255;
589
+ frame[3] = n & 255;
590
+ frame.set(body, FRAME_HEADER_BYTES);
591
+ return frame;
592
+ }
593
+ function projectScalar(value, path) {
594
+ if (value === null) return null;
595
+ switch (typeof value) {
596
+ case "boolean":
597
+ return value;
598
+ case "number":
599
+ if (!Number.isFinite(value)) {
600
+ throw new ProtocolViolation("dto-scalar", `non-finite number at ${path}`);
601
+ }
602
+ return value;
603
+ case "string":
604
+ if (LONE_SURROGATE.test(value)) {
605
+ throw new ProtocolViolation("dto-string", `unpaired surrogate at ${path}`);
606
+ }
607
+ return value;
608
+ default:
609
+ throw new ProtocolViolation(
610
+ "dto-scalar",
611
+ `value of type ${typeof value} is not JSON-representable at ${path}`
612
+ );
613
+ }
614
+ }
615
+ function projectNode(value, depth, maxDepth, seen, path) {
616
+ if (value === null || typeof value !== "object") {
617
+ return projectScalar(value, path);
618
+ }
619
+ if (depth > maxDepth) {
620
+ throw new ProtocolViolation("dto-depth", `nesting exceeds ${maxDepth} at ${path}`);
621
+ }
622
+ if (types.isProxy(value)) {
623
+ throw new ProtocolViolation("dto-prototype", `proxy at ${path}`);
624
+ }
625
+ if (seen.has(value)) {
626
+ throw new ProtocolViolation("dto-alias", `value is reachable more than once at ${path}`);
627
+ }
628
+ seen.add(value);
629
+ if (Object.getOwnPropertySymbols(value).length > 0) {
630
+ throw new ProtocolViolation("dto-symbol", `symbol-keyed property at ${path}`);
631
+ }
632
+ const proto = Object.getPrototypeOf(value);
633
+ const result = Array.isArray(value) ? projectArray(value, proto, depth, maxDepth, seen, path) : projectObject(value, proto, depth, maxDepth, seen, path);
634
+ return Object.freeze(result);
635
+ }
636
+ function projectArray(value, proto, depth, maxDepth, seen, path) {
637
+ if (proto !== Array.prototype) {
638
+ throw new ProtocolViolation("dto-prototype", `array with exotic prototype at ${path}`);
639
+ }
640
+ const length = value.length;
641
+ const out = new Array(length);
642
+ for (let i = 0; i < length; i += 1) {
643
+ const descriptor = Object.getOwnPropertyDescriptor(value, i);
644
+ if (descriptor === void 0) {
645
+ throw new ProtocolViolation("dto-sparse", `hole at ${path}[${i}]`);
646
+ }
647
+ if (!("value" in descriptor)) {
648
+ throw new ProtocolViolation("dto-accessor", `accessor at ${path}[${i}]`);
649
+ }
650
+ out[i] = projectNode(descriptor.value, depth + 1, maxDepth, seen, `${path}[${i}]`);
651
+ }
652
+ if (Object.getOwnPropertyNames(value).length !== length + 1) {
653
+ throw new ProtocolViolation("dto-sparse", `array carries extra own properties at ${path}`);
654
+ }
655
+ return out;
656
+ }
657
+ function projectObject(value, proto, depth, maxDepth, seen, path) {
658
+ if (proto !== Object.prototype && proto !== null) {
659
+ throw new ProtocolViolation("dto-prototype", `non-plain object at ${path}`);
660
+ }
661
+ const out = {};
662
+ for (const key of Object.getOwnPropertyNames(value)) {
663
+ if (RESERVED_KEYS.has(key)) {
664
+ throw new ProtocolViolation("dto-key", `reserved property name "${key}" at ${path}`);
665
+ }
666
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
667
+ if (!("value" in descriptor)) {
668
+ throw new ProtocolViolation("dto-accessor", `accessor property "${key}" at ${path}`);
669
+ }
670
+ if (!descriptor.enumerable) {
671
+ throw new ProtocolViolation("dto-key", `non-enumerable property "${key}" at ${path}`);
672
+ }
673
+ if (LONE_SURROGATE.test(key)) {
674
+ throw new ProtocolViolation("dto-string", `unpaired surrogate in key at ${path}`);
675
+ }
676
+ out[key] = projectNode(descriptor.value, depth + 1, maxDepth, seen, `${path}.${key}`);
677
+ }
678
+ return out;
679
+ }
680
+ function projectDto(value, maxDepth) {
681
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) {
682
+ throw new ProtocolViolation("dto-depth", "maxDepth must be a non-negative safe integer");
683
+ }
684
+ return projectNode(value, 0, maxDepth, /* @__PURE__ */ new Set(), "$");
685
+ }
686
+
687
+ // src/logs.ts
688
+ var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
689
+ var LOG_LEVEL_SEVERITY = Object.freeze({
690
+ trace: 10,
691
+ debug: 20,
692
+ info: 30,
693
+ warn: 40,
694
+ error: 50,
695
+ fatal: 60
696
+ });
697
+ var MAX_LOG_ATTRS = 64;
698
+ function fail(code, detail) {
699
+ return { ok: false, code, detail };
700
+ }
701
+ var LEVELS = new Set(LOG_LEVELS);
702
+ function isSafeNonNegative(value) {
703
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
704
+ }
705
+ function validateLogRecord(value, limits) {
706
+ let projected;
707
+ try {
708
+ projected = projectDto(value, limits.maxDepth);
709
+ } catch (error) {
710
+ if (error instanceof ProtocolViolation) {
711
+ return fail(error.code === "dto-depth" ? "depth" : "schema", error.message);
712
+ }
713
+ return fail("schema", "value could not be projected into a plain DTO");
714
+ }
715
+ const serialised = JSON.stringify(projected);
716
+ if (serialised === void 0) {
717
+ return fail("schema", "log record is not a JSON object");
718
+ }
719
+ const bytes = Buffer2.byteLength(serialised, "utf8");
720
+ if (bytes > limits.maxLogRecordBytes) {
721
+ return fail("bytes", `log record is ${bytes} bytes, ceiling is ${limits.maxLogRecordBytes}`);
722
+ }
723
+ if (typeof projected !== "object" || projected === null || Array.isArray(projected)) {
724
+ return fail("schema", "log record must be an object");
725
+ }
726
+ const record = projected;
727
+ for (const key of Object.keys(record)) {
728
+ if (!["ts", "level", "message", "attrs", "logger", "seq", "revision"].includes(key)) {
729
+ return fail("schema", `unknown log record property "${key}"`);
730
+ }
731
+ }
732
+ if (!isSafeNonNegative(record["ts"]) || record["ts"] === 0) {
733
+ return fail("schema", "ts must be a positive safe integer (epoch milliseconds)");
734
+ }
735
+ if (typeof record["level"] !== "string" || !LEVELS.has(record["level"])) {
736
+ return fail("schema", `level must be one of ${LOG_LEVELS.join(", ")}`);
737
+ }
738
+ if (typeof record["message"] !== "string") {
739
+ return fail("schema", "message must be a string");
740
+ }
741
+ if (Buffer2.byteLength(record["message"], "utf8") > limits.maxStringBytes) {
742
+ return fail("string-bytes", `message exceeds ${limits.maxStringBytes} UTF-8 bytes`);
743
+ }
744
+ if (!isSafeNonNegative(record["seq"])) {
745
+ return fail("schema", "seq must be a non-negative safe integer");
746
+ }
747
+ if (record["logger"] !== void 0) {
748
+ if (typeof record["logger"] !== "string") {
749
+ return fail("schema", "logger must be a string");
750
+ }
751
+ if (Buffer2.byteLength(record["logger"], "utf8") > limits.maxStringBytes) {
752
+ return fail("string-bytes", `logger exceeds ${limits.maxStringBytes} UTF-8 bytes`);
753
+ }
754
+ }
755
+ if (record["revision"] !== void 0) {
756
+ if (!isSafeNonNegative(record["revision"]) || record["revision"] === 0) {
757
+ return fail("revision", "revision must be a positive safe integer");
758
+ }
759
+ }
760
+ const attrs = record["attrs"];
761
+ if (attrs !== void 0) {
762
+ if (typeof attrs !== "object" || attrs === null || Array.isArray(attrs)) {
763
+ return fail("schema", "attrs must be a flat object");
764
+ }
765
+ const entries = Object.entries(attrs);
766
+ if (entries.length > MAX_LOG_ATTRS) {
767
+ return fail("count", `attrs carries ${entries.length} keys, ceiling is ${MAX_LOG_ATTRS}`);
768
+ }
769
+ for (const [key, attrValue] of entries) {
770
+ if (Buffer2.byteLength(key, "utf8") > limits.maxStringBytes) {
771
+ return fail("string-bytes", `attribute key "${key}" exceeds the string ceiling`);
772
+ }
773
+ const type = typeof attrValue;
774
+ if (attrValue !== null && type !== "string" && type !== "number" && type !== "boolean") {
775
+ return fail("schema", `attribute "${key}" must be a string, number, boolean or null`);
776
+ }
777
+ if (type === "number" && !Number.isFinite(attrValue)) {
778
+ return fail("schema", `attribute "${key}" must be a finite number`);
779
+ }
780
+ if (type === "string" && Buffer2.byteLength(attrValue, "utf8") > limits.maxStringBytes) {
781
+ return fail("string-bytes", `attribute "${key}" exceeds the string ceiling`);
782
+ }
783
+ }
784
+ }
785
+ return { ok: true, record: projected };
786
+ }
787
+
788
+ // src/delta.ts
789
+ import { Buffer as Buffer4 } from "buffer";
790
+ import { z as z3 } from "zod";
791
+
792
+ // src/validate.ts
793
+ import { Buffer as Buffer3 } from "buffer";
794
+ import "zod";
795
+ function fail2(code, detail) {
796
+ return { ok: false, code, detail };
797
+ }
798
+ function codeForIssue(issue) {
799
+ const path = issue.path.map(String);
800
+ if (path.includes("role")) return "unknown-role";
801
+ if (path.includes("revision")) return "revision";
802
+ if (path.includes("bounds") || path.includes("rect")) return "bad-rect";
803
+ if (issue.code === "custom" && issue.message?.includes("hit regions")) return "bad-rect";
804
+ if (issue.code === "too_big" && (path.includes("nodes") || path.includes("rootIds"))) {
805
+ return "count";
806
+ }
807
+ if (issue.code === "custom" && typeof issue.message === "string" && issue.message.includes("UTF-8 bytes")) {
808
+ return "string-bytes";
809
+ }
810
+ return "schema";
811
+ }
812
+ function describeIssue(issue) {
813
+ const where = issue.path.length > 0 ? issue.path.map(String).join(".") : "<root>";
814
+ return `${where}: ${issue.message}`;
815
+ }
816
+ function rectIntersectsViewport(rect, columns, rows) {
817
+ if (rect.width === 0 || rect.height === 0) return false;
818
+ return rect.column < columns && rect.row < rows && rect.column + rect.width > 0 && rect.row + rect.height > 0;
819
+ }
820
+ function checkNodeShape(node, snapshot, ids, limits) {
821
+ if (node.role === "generic" && (node.frameworkType === void 0 || node.frameworkType === "")) {
822
+ return fail2(
823
+ "schema",
824
+ `node ${node.id} has role 'generic' without a frameworkType; an unrecognised widget must name what the framework called it`
825
+ );
826
+ }
827
+ if (node.state?.offscreen === true && node.state.hidden !== true) {
828
+ return fail2(
829
+ "schema",
830
+ `node ${node.id}: state.offscreen implies state.hidden \u2014 every cell is outside the visible area, so the node cannot also be visible`
831
+ );
832
+ }
833
+ if (node.bounds !== void 0) {
834
+ const { width, height, row, column } = node.bounds;
835
+ if (!Number.isSafeInteger(row + height) || !Number.isSafeInteger(column + width)) {
836
+ return fail2("bad-rect", `node ${node.id}: bounds overflow the safe-integer range`);
837
+ }
838
+ if (node.state?.hidden !== true && !rectIntersectsViewport(node.bounds, snapshot.columns, snapshot.rows)) {
839
+ return fail2(
840
+ "bad-rect",
841
+ `node ${node.id}: bounds do not intersect the ${snapshot.columns}x${snapshot.rows} viewport and the node is not hidden`
842
+ );
843
+ }
844
+ }
845
+ for (const range of node.textRanges ?? []) {
846
+ if (range.endOffset < range.startOffset) {
847
+ return fail2("bad-rect", `node ${node.id}: text range ends before it starts`);
848
+ }
849
+ if (!Number.isSafeInteger(range.rect.row + range.rect.height)) {
850
+ return fail2("bad-rect", `node ${node.id}: text range rect overflows the safe-integer range`);
851
+ }
852
+ }
853
+ for (const [field, targets] of [
854
+ ["labelledBy", node.labelledBy],
855
+ ["describedBy", node.describedBy]
856
+ ]) {
857
+ if (targets === void 0) continue;
858
+ if (targets.length > limits.maxRelationTargets) {
859
+ return fail2("count", `node ${node.id}: ${field} exceeds ${limits.maxRelationTargets} targets`);
860
+ }
861
+ for (const target of targets) {
862
+ if (!ids.has(target)) {
863
+ return fail2("missing-parent", `node ${node.id}: ${field} references unknown node ${target}`);
864
+ }
865
+ }
866
+ }
867
+ return null;
868
+ }
869
+ function computeDepths(nodes, byId) {
870
+ const depths = /* @__PURE__ */ new Map();
871
+ for (const start of nodes) {
872
+ if (depths.has(start.id)) continue;
873
+ const chain = [];
874
+ const onChain = /* @__PURE__ */ new Set();
875
+ let current = start;
876
+ while (current !== void 0 && !depths.has(current.id)) {
877
+ if (onChain.has(current.id)) return { cycleAt: current.id };
878
+ onChain.add(current.id);
879
+ chain.push(current.id);
880
+ current = current.parentId === void 0 ? void 0 : byId.get(current.parentId);
881
+ }
882
+ let depth = current === void 0 ? 0 : depths.get(current.id);
883
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
884
+ depth += 1;
885
+ depths.set(chain[i], depth);
886
+ }
887
+ }
888
+ return { depths };
889
+ }
890
+ function validateSnapshot(value, limits) {
891
+ let projected;
892
+ try {
893
+ projected = projectDto(value, limits.maxDepth);
894
+ } catch (error) {
895
+ if (error instanceof ProtocolViolation) {
896
+ return fail2(error.code === "dto-depth" ? "depth" : "schema", error.message);
897
+ }
898
+ return fail2("schema", "value could not be projected into a plain DTO");
899
+ }
900
+ const serialised = JSON.stringify(projected);
901
+ if (serialised === void 0) {
902
+ return fail2("schema", "snapshot is not a JSON object");
903
+ }
904
+ const bytes = Buffer3.byteLength(serialised, "utf8");
905
+ if (bytes > limits.maxSnapshotBytes) {
906
+ return fail2("bytes", `snapshot is ${bytes} bytes, ceiling is ${limits.maxSnapshotBytes}`);
907
+ }
908
+ const parsed = treeSchemas(limits).snapshot.safeParse(projected);
909
+ if (!parsed.success) {
910
+ const issue = parsed.error.issues[0];
911
+ return fail2(codeForIssue(issue), describeIssue(issue));
912
+ }
913
+ const snapshot = projected;
914
+ if (snapshot.nodes.length > limits.maxNodes) {
915
+ return fail2("count", `snapshot carries ${snapshot.nodes.length} nodes, ceiling is ${limits.maxNodes}`);
916
+ }
917
+ const byId = /* @__PURE__ */ new Map();
918
+ for (const node of snapshot.nodes) {
919
+ if (byId.has(node.id)) {
920
+ return fail2("duplicate-id", `node id ${node.id} appears more than once`);
921
+ }
922
+ byId.set(node.id, node);
923
+ }
924
+ const rootIds = /* @__PURE__ */ new Set();
925
+ for (const id of snapshot.rootIds) {
926
+ if (rootIds.has(id)) {
927
+ return fail2("duplicate-id", `root id ${id} appears more than once`);
928
+ }
929
+ rootIds.add(id);
930
+ const node = byId.get(id);
931
+ if (node === void 0) {
932
+ return fail2("missing-parent", `rootIds references unknown node ${id}`);
933
+ }
934
+ if (node.parentId !== void 0) {
935
+ return fail2("schema", `root node ${id} declares a parent`);
936
+ }
937
+ }
938
+ const ids = new Set(byId.keys());
939
+ if (snapshot.v === 2) {
940
+ if (snapshot.coordinateSpace?.status === "known" && snapshot.coordinateSpace.value !== "viewport-cells") {
941
+ }
942
+ if (snapshot.hitGrid?.status === "known") {
943
+ for (const region of snapshot.hitGrid.value.regions) {
944
+ if (!ids.has(region.recipientId)) {
945
+ return fail2("missing-parent", `hitGrid references unknown recipient ${region.recipientId}`);
946
+ }
947
+ if (!rectIntersectsViewport(region.rect, snapshot.columns, snapshot.rows)) {
948
+ return fail2("bad-rect", `hitGrid region for ${region.recipientId} does not intersect the viewport`);
949
+ }
950
+ }
951
+ }
952
+ }
953
+ for (const node of snapshot.nodes) {
954
+ if (node.parentId === void 0) {
955
+ if (!rootIds.has(node.id)) {
956
+ return fail2("schema", `parentless node ${node.id} is missing from rootIds`);
957
+ }
958
+ } else if (!byId.has(node.parentId)) {
959
+ return fail2("missing-parent", `node ${node.id} references unknown parent ${node.parentId}`);
960
+ } else if (node.parentId === node.id) {
961
+ return fail2("cycle", `node ${node.id} is its own parent`);
962
+ }
963
+ const problem = checkNodeShape(node, snapshot, ids, limits);
964
+ if (problem !== null) return problem;
965
+ }
966
+ const depthResult = computeDepths(snapshot.nodes, byId);
967
+ if ("cycleAt" in depthResult) {
968
+ return fail2("cycle", `parent chain through node ${depthResult.cycleAt} is cyclic`);
969
+ }
970
+ for (const [id, depth] of depthResult.depths) {
971
+ if (depth > limits.maxDepth) {
972
+ return fail2("depth", `node ${id} sits at depth ${depth}, ceiling is ${limits.maxDepth}`);
973
+ }
974
+ }
975
+ if (snapshot.cursor !== void 0) {
976
+ const { row, column } = snapshot.cursor;
977
+ if (row >= snapshot.rows || column >= snapshot.columns) {
978
+ return fail2("bad-rect", `cursor (${row}, ${column}) lies outside the viewport`);
979
+ }
980
+ }
981
+ return { ok: true, snapshot };
982
+ }
983
+
984
+ // src/delta.ts
985
+ function fail3(code, detail) {
986
+ return { ok: false, code, detail };
987
+ }
988
+ var DELTA_KEYS = ["baseRevision", "revision", "changed", "removed", "rootIds", "cursor"];
989
+ function validateTreeDelta(value, limits) {
990
+ let projected;
991
+ try {
992
+ projected = projectDto(value, limits.maxDepth);
993
+ } catch (error) {
994
+ if (error instanceof ProtocolViolation) {
995
+ return fail3(error.code === "dto-depth" ? "depth" : "schema", error.message);
996
+ }
997
+ return fail3("schema", "value could not be projected into a plain DTO");
998
+ }
999
+ const serialised = JSON.stringify(projected);
1000
+ if (serialised === void 0) {
1001
+ return fail3("schema", "delta is not a JSON object");
1002
+ }
1003
+ const bytes = Buffer4.byteLength(serialised, "utf8");
1004
+ if (bytes > limits.maxSnapshotBytes) {
1005
+ return fail3("bytes", `delta is ${bytes} bytes, ceiling is ${limits.maxSnapshotBytes}`);
1006
+ }
1007
+ if (typeof projected !== "object" || projected === null || Array.isArray(projected)) {
1008
+ return fail3("schema", "delta must be an object");
1009
+ }
1010
+ const delta = projected;
1011
+ for (const key of Object.keys(delta)) {
1012
+ if (!DELTA_KEYS.includes(key)) return fail3("schema", `unknown delta property "${key}"`);
1013
+ }
1014
+ const { text, node, cursor } = treeSchemas(limits);
1015
+ const parsed = deltaSchema(text, node, cursor, limits).safeParse(delta);
1016
+ if (!parsed.success) {
1017
+ const issue = parsed.error.issues[0];
1018
+ const path = issue.path.map(String);
1019
+ const where = path.length > 0 ? path.join(".") : "<root>";
1020
+ const code = path.includes("role") ? "unknown-role" : path.includes("revision") || path.includes("baseRevision") ? "revision" : path.includes("bounds") || path.includes("rect") ? "bad-rect" : issue.code === "too_big" ? "count" : "schema";
1021
+ return fail3(code, `${where}: ${issue.message}`);
1022
+ }
1023
+ const typed = delta;
1024
+ if (typed.revision <= typed.baseRevision) {
1025
+ return fail3(
1026
+ "revision",
1027
+ `revision ${typed.revision} must be greater than baseRevision ${typed.baseRevision}`
1028
+ );
1029
+ }
1030
+ const total = typed.changed.length + typed.removed.length;
1031
+ if (total > limits.maxNodes) {
1032
+ return fail3("count", `delta touches ${total} nodes, ceiling is ${limits.maxNodes}`);
1033
+ }
1034
+ const changedIds = /* @__PURE__ */ new Set();
1035
+ for (const entry of typed.changed) {
1036
+ if (changedIds.has(entry.id)) {
1037
+ return fail3("duplicate-id", `node id ${entry.id} appears twice in changed`);
1038
+ }
1039
+ changedIds.add(entry.id);
1040
+ if (entry.parentId === entry.id) {
1041
+ return fail3("cycle", `node ${entry.id} is its own parent`);
1042
+ }
1043
+ }
1044
+ const removedIds = /* @__PURE__ */ new Set();
1045
+ for (const id of typed.removed) {
1046
+ if (removedIds.has(id)) return fail3("duplicate-id", `node id ${id} appears twice in removed`);
1047
+ removedIds.add(id);
1048
+ if (changedIds.has(id)) {
1049
+ return fail3("schema", `node id ${id} is both changed and removed`);
1050
+ }
1051
+ }
1052
+ if (typed.rootIds !== void 0) {
1053
+ const seen = /* @__PURE__ */ new Set();
1054
+ for (const id of typed.rootIds) {
1055
+ if (seen.has(id)) return fail3("duplicate-id", `root id ${id} appears twice`);
1056
+ seen.add(id);
1057
+ }
1058
+ }
1059
+ return { ok: true, delta: typed };
1060
+ }
1061
+ var deltaCache = /* @__PURE__ */ new WeakMap();
1062
+ function deltaSchema(text, node, cursor, limits) {
1063
+ const cached = deltaCache.get(limits);
1064
+ if (cached !== void 0) return cached;
1065
+ const built = z3.strictObject({
1066
+ baseRevision: z3.number().refine((n) => Number.isSafeInteger(n) && n > 0, "expected a positive safe integer"),
1067
+ revision: z3.number().refine((n) => Number.isSafeInteger(n) && n > 0, "expected a positive safe integer"),
1068
+ changed: z3.array(node).max(limits.maxNodes),
1069
+ removed: z3.array(text).max(limits.maxNodes),
1070
+ rootIds: z3.array(text).max(limits.maxNodes).optional(),
1071
+ cursor: cursor.optional()
1072
+ });
1073
+ deltaCache.set(limits, built);
1074
+ return built;
1075
+ }
1076
+ function applyTreeDelta(base, delta, limits) {
1077
+ if (delta.baseRevision !== base.revision) {
1078
+ return {
1079
+ ok: false,
1080
+ code: "revision",
1081
+ detail: `delta is based on revision ${delta.baseRevision} but the held snapshot is revision ${base.revision}; request a full snapshot instead of patching`
1082
+ };
1083
+ }
1084
+ const byId = /* @__PURE__ */ new Map();
1085
+ for (const node of base.nodes) byId.set(node.id, node);
1086
+ const childrenOf = /* @__PURE__ */ new Map();
1087
+ for (const node of base.nodes) {
1088
+ if (node.parentId === void 0) continue;
1089
+ const siblings = childrenOf.get(node.parentId);
1090
+ if (siblings === void 0) childrenOf.set(node.parentId, [node.id]);
1091
+ else siblings.push(node.id);
1092
+ }
1093
+ for (const id of delta.removed) {
1094
+ if (!byId.has(id)) {
1095
+ return {
1096
+ ok: false,
1097
+ code: "missing-parent",
1098
+ detail: `delta removes unknown node ${id}; the producer's base disagrees with ours, so the tree must be resynchronised rather than patched`
1099
+ };
1100
+ }
1101
+ const pending = [id];
1102
+ while (pending.length > 0) {
1103
+ const current = pending.pop();
1104
+ if (!byId.delete(current)) continue;
1105
+ const children = childrenOf.get(current);
1106
+ if (children !== void 0) pending.push(...children);
1107
+ }
1108
+ }
1109
+ for (const node of delta.changed) byId.set(node.id, node);
1110
+ const rootIds = delta.rootIds ?? base.rootIds.filter((id) => byId.has(id));
1111
+ const composed = {
1112
+ v: 1,
1113
+ sessionId: base.sessionId,
1114
+ revision: delta.revision,
1115
+ columns: base.columns,
1116
+ rows: base.rows,
1117
+ // Absent cursor means unchanged, so the base's carries over.
1118
+ ...(delta.cursor ?? base.cursor) === void 0 ? {} : { cursor: delta.cursor ?? base.cursor },
1119
+ rootIds,
1120
+ nodes: [...byId.values()]
1121
+ };
1122
+ return validateSnapshot(composed, limits);
1123
+ }
1124
+
1125
+ // src/accesskit.ts
1126
+ import { createHash } from "crypto";
1127
+ var ACCESSKIT_ROOT_TREE_ID = "00000000-0000-0000-0000-000000000000";
1128
+ var ACCESSKIT_ROLE_BY_SEMANTIC_ROLE = Object.freeze({
1129
+ application: "application",
1130
+ region: "region",
1131
+ dialog: "dialog",
1132
+ alert: "alert",
1133
+ status: "status",
1134
+ list: "list",
1135
+ listitem: "listItem",
1136
+ menu: "menu",
1137
+ menuitem: "menuItem",
1138
+ button: "button",
1139
+ checkbox: "checkBox",
1140
+ radio: "radioButton",
1141
+ tab: "tab",
1142
+ textbox: "textInput",
1143
+ heading: "heading",
1144
+ text: "label",
1145
+ progressbar: "progressIndicator",
1146
+ separator: "splitter",
1147
+ scrollbar: "scrollBar",
1148
+ table: "table",
1149
+ row: "row",
1150
+ cell: "cell",
1151
+ generic: "genericContainer"
1152
+ });
1153
+ var ACCESSKIT_ACTION_BY_SEMANTIC_ACTION = Object.freeze({
1154
+ focus: "focus",
1155
+ activate: "click",
1156
+ toggle: "click",
1157
+ setValue: "setValue",
1158
+ expand: "expand",
1159
+ scroll: "scrollIntoView"
1160
+ });
1161
+ var NODE_ID_BITS = 53n;
1162
+ var NODE_ID_MASK = (1n << NODE_ID_BITS) - 1n;
1163
+ function accessKitNodeId(id) {
1164
+ const digest = createHash("sha256").update(id, "utf8").digest();
1165
+ const value = digest.readBigUInt64BE(0) & NODE_ID_MASK;
1166
+ return value === 0n ? 1 : Number(value);
1167
+ }
1168
+ function toggledFor(checked) {
1169
+ if (checked === void 0) return void 0;
1170
+ if (checked === "mixed") return "mixed";
1171
+ return checked ? "true" : "false";
1172
+ }
1173
+ function accessKitRoleFor(node) {
1174
+ if (node.role === "textbox" && node.state?.multiline === true) return "multilineTextInput";
1175
+ return ACCESSKIT_ROLE_BY_SEMANTIC_ROLE[node.role];
1176
+ }
1177
+ function actionsFor(actions) {
1178
+ if (actions === void 0 || actions.length === 0) return void 0;
1179
+ const mapped = /* @__PURE__ */ new Set();
1180
+ for (const action of actions) {
1181
+ const target = ACCESSKIT_ACTION_BY_SEMANTIC_ACTION[action];
1182
+ if (target !== void 0) mapped.add(target);
1183
+ }
1184
+ return mapped.size === 0 ? void 0 : [...mapped];
1185
+ }
1186
+ function boundsFor(rect, cellSize) {
1187
+ return {
1188
+ x0: rect.column * cellSize.width,
1189
+ y0: rect.row * cellSize.height,
1190
+ x1: (rect.column + rect.width) * cellSize.width,
1191
+ y1: (rect.row + rect.height) * cellSize.height
1192
+ };
1193
+ }
1194
+ function toAccessKitTreeUpdate(snapshot, options = {}) {
1195
+ const idOf = /* @__PURE__ */ new Map();
1196
+ const seen = /* @__PURE__ */ new Map();
1197
+ for (const node of snapshot.nodes) {
1198
+ const mapped = accessKitNodeId(node.id);
1199
+ const previous = seen.get(mapped);
1200
+ if (previous !== void 0) {
1201
+ throw new ProtocolViolation(
1202
+ "dto-key",
1203
+ `node ids "${previous}" and "${node.id}" collide in the AccessKit id space`
1204
+ );
1205
+ }
1206
+ seen.set(mapped, node.id);
1207
+ idOf.set(node.id, mapped);
1208
+ }
1209
+ const childrenOf = /* @__PURE__ */ new Map();
1210
+ for (const node of snapshot.nodes) {
1211
+ if (node.parentId === void 0) continue;
1212
+ const siblings = childrenOf.get(node.parentId);
1213
+ if (siblings === void 0) childrenOf.set(node.parentId, [idOf.get(node.id)]);
1214
+ else siblings.push(idOf.get(node.id));
1215
+ }
1216
+ const relation = (ids) => {
1217
+ if (ids === void 0 || ids.length === 0) return void 0;
1218
+ const mapped = ids.map((id) => idOf.get(id)).filter((id) => id !== void 0);
1219
+ return mapped.length === 0 ? void 0 : mapped;
1220
+ };
1221
+ const cellBounds = {};
1222
+ const nodes = [];
1223
+ let focus;
1224
+ for (const node of snapshot.nodes) {
1225
+ const id = idOf.get(node.id);
1226
+ const state = node.state;
1227
+ if (state?.focused === true && focus === void 0) focus = id;
1228
+ if (node.bounds !== void 0) cellBounds[String(id)] = node.bounds;
1229
+ const accessKitNode = {
1230
+ role: accessKitRoleFor(node),
1231
+ ...node.name === "" ? {} : { label: node.name },
1232
+ ...node.description === void 0 ? {} : { description: node.description },
1233
+ ...node.value === void 0 ? {} : { value: node.value },
1234
+ ...childrenOf.has(node.id) ? { children: childrenOf.get(node.id) } : {},
1235
+ ...node.bounds !== void 0 && options.cellSize !== void 0 ? { bounds: boundsFor(node.bounds, options.cellSize) } : {},
1236
+ ...actionsFor(node.actions) === void 0 ? {} : { actions: actionsFor(node.actions) },
1237
+ ...relation(node.labelledBy) === void 0 ? {} : { labelledBy: relation(node.labelledBy) },
1238
+ ...relation(node.describedBy) === void 0 ? {} : { describedBy: relation(node.describedBy) },
1239
+ ...state?.disabled === void 0 ? {} : { disabled: state.disabled },
1240
+ ...state?.selected === void 0 ? {} : { selected: state.selected },
1241
+ ...state?.expanded === void 0 ? {} : { expanded: state.expanded },
1242
+ ...state?.busy === void 0 ? {} : { busy: state.busy },
1243
+ ...state?.modal === void 0 ? {} : { modal: state.modal },
1244
+ ...state?.hidden === void 0 ? {} : { hidden: state.hidden },
1245
+ ...state?.readonly === void 0 ? {} : { readOnly: state.readonly },
1246
+ ...toggledFor(state?.checked) === void 0 ? {} : { toggled: toggledFor(state?.checked) }
1247
+ };
1248
+ nodes.push(Object.freeze([id, Object.freeze(accessKitNode)]));
1249
+ }
1250
+ const rootId = snapshot.rootIds[0];
1251
+ const root = rootId === void 0 ? void 0 : idOf.get(rootId);
1252
+ const update = {
1253
+ nodes: Object.freeze(nodes),
1254
+ ...root === void 0 ? {} : {
1255
+ tree: Object.freeze({
1256
+ root,
1257
+ ...options.toolkitName === void 0 ? {} : { toolkitName: options.toolkitName },
1258
+ ...options.toolkitVersion === void 0 ? {} : { toolkitVersion: options.toolkitVersion }
1259
+ })
1260
+ },
1261
+ treeId: options.treeId ?? ACCESSKIT_ROOT_TREE_ID,
1262
+ focus: focus ?? root ?? 0
1263
+ };
1264
+ return Object.freeze({ update: Object.freeze(update), cellBounds: Object.freeze(cellBounds) });
1265
+ }
1266
+
1267
+ // src/probe/bounds.ts
1268
+ function intersect(a, b) {
1269
+ const row = Math.max(a.row, b.row);
1270
+ const column = Math.max(a.column, b.column);
1271
+ const bottom = Math.min(a.row + a.height, b.row + b.height);
1272
+ const right = Math.min(a.column + a.width, b.column + b.width);
1273
+ const height = Math.max(0, bottom - row);
1274
+ const width = Math.max(0, right - column);
1275
+ return { rect: { row, column, width, height }, empty: width === 0 || height === 0 };
1276
+ }
1277
+ function resolveNodeBounds(geometry, options = {}) {
1278
+ const occlusion = options.paintOrderKnown === true ? "known" : "unknown";
1279
+ if (geometry?.visibleRect !== void 0) {
1280
+ const rect = geometry.visibleRect;
1281
+ return {
1282
+ rect,
1283
+ occlusion,
1284
+ source: "visible",
1285
+ clippedAway: rect.width === 0 || rect.height === 0
1286
+ };
1287
+ }
1288
+ if (geometry?.intendedRect === void 0) return void 0;
1289
+ if (options.clip !== void 0) {
1290
+ const { rect, empty } = intersect(geometry.intendedRect, options.clip);
1291
+ const occupiedCells = geometry.intendedRect.width > 0 && geometry.intendedRect.height > 0;
1292
+ return { rect, occlusion, source: "clipped", clippedAway: empty && occupiedCells };
1293
+ }
1294
+ return {
1295
+ rect: geometry.intendedRect,
1296
+ occlusion,
1297
+ source: "intended",
1298
+ clippedAway: false
1299
+ };
1300
+ }
1301
+
1302
+ // src/probe/validate.ts
1303
+ import { Buffer as Buffer5 } from "buffer";
1304
+ import { z as z4 } from "zod";
1305
+ function fail4(code, detail) {
1306
+ return { ok: false, code, detail };
1307
+ }
1308
+ var safeInt2 = z4.number().refine(Number.isSafeInteger, "expected a safe integer");
1309
+ var nonNegative = z4.number().refine((n) => Number.isSafeInteger(n) && n >= 0, "expected a non-negative safe integer");
1310
+ var positive = z4.number().refine((n) => Number.isSafeInteger(n) && n > 0, "expected a positive safe integer");
1311
+ var cache2 = /* @__PURE__ */ new WeakMap();
1312
+ function buildFrameSchema(limits) {
1313
+ const text = z4.string().refine(
1314
+ (s) => Buffer5.byteLength(s, "utf8") <= limits.maxStringBytes,
1315
+ `expected at most ${limits.maxStringBytes} UTF-8 bytes`
1316
+ );
1317
+ const rect = z4.strictObject({
1318
+ row: safeInt2,
1319
+ column: safeInt2,
1320
+ width: nonNegative,
1321
+ height: nonNegative
1322
+ });
1323
+ const identity = z4.strictObject({
1324
+ kind: z4.enum(["stable", "frame-local"]),
1325
+ value: text.min(1)
1326
+ });
1327
+ const extendedValue = z4.lazy(
1328
+ () => z4.union([
1329
+ z4.null(),
1330
+ z4.boolean(),
1331
+ z4.number().finite().refine(
1332
+ (value) => Math.abs(value) <= Number.MAX_SAFE_INTEGER,
1333
+ "expected a finite JSON number in the safe range"
1334
+ ),
1335
+ text,
1336
+ z4.array(extendedValue).max(limits.maxRelationTargets),
1337
+ z4.record(text, extendedValue).refine(
1338
+ (value) => Object.keys(value).length <= limits.maxRelationTargets,
1339
+ `expected at most ${limits.maxRelationTargets} properties`
1340
+ )
1341
+ ])
1342
+ );
1343
+ const extended = z4.record(text, extendedValue).refine(
1344
+ (value) => Object.keys(value).length <= limits.maxRelationTargets,
1345
+ `expected at most ${limits.maxRelationTargets} properties`
1346
+ );
1347
+ const relations = z4.array(text.min(1)).max(limits.maxRelationTargets);
1348
+ const state = z4.strictObject({
1349
+ focused: z4.boolean().optional(),
1350
+ disabled: z4.boolean().optional(),
1351
+ checked: z4.union([z4.boolean(), z4.literal("mixed")]).optional(),
1352
+ expanded: z4.boolean().optional(),
1353
+ readonly: z4.boolean().optional(),
1354
+ selected: z4.boolean().optional(),
1355
+ busy: z4.boolean().optional(),
1356
+ multiline: z4.boolean().optional(),
1357
+ displayed: z4.boolean().optional(),
1358
+ value: text.optional(),
1359
+ selectedIndex: nonNegative.optional(),
1360
+ textSelection: z4.strictObject({ start: nonNegative, end: nonNegative }).optional(),
1361
+ scroll: z4.strictObject({ row: nonNegative, column: nonNegative }).optional(),
1362
+ scrollExtent: z4.strictObject({ rows: nonNegative, columns: nonNegative }).optional()
1363
+ });
1364
+ const object = z4.strictObject({
1365
+ identity,
1366
+ frameworkType: text.min(1),
1367
+ parent: text.optional(),
1368
+ geometry: z4.strictObject({ intendedRect: rect.optional(), visibleRect: rect.optional() }).optional(),
1369
+ state: state.optional(),
1370
+ text: text.optional(),
1371
+ accessibility: z4.strictObject({ role: text.optional(), name: text.optional(), description: text.optional() }).optional(),
1372
+ annotations: z4.strictObject({
1373
+ role: text.optional(),
1374
+ name: text.optional(),
1375
+ testId: text.optional(),
1376
+ description: text.optional(),
1377
+ extended: extended.optional(),
1378
+ actions: z4.array(z4.enum(SEMANTIC_ACTIONS)).max(SEMANTIC_ACTIONS.length).optional(),
1379
+ labelledBy: relations.optional(),
1380
+ describedBy: relations.optional()
1381
+ }).optional(),
1382
+ paintOrder: safeInt2.optional(),
1383
+ unobservable: z4.array(z4.enum(PROBE_UNOBSERVABLE_FIELDS)).max(PROBE_UNOBSERVABLE_FIELDS.length).optional()
1384
+ });
1385
+ const operation = z4.strictObject({
1386
+ kind: z4.enum(["render", "layout"]),
1387
+ ordinal: nonNegative,
1388
+ target: identity.optional(),
1389
+ frameworkType: text.optional(),
1390
+ intendedRect: rect.optional()
1391
+ });
1392
+ return z4.strictObject({
1393
+ frame: positive,
1394
+ objects: z4.array(object).max(limits.maxNodes),
1395
+ operations: z4.array(operation).max(limits.maxNodes).optional()
1396
+ });
1397
+ }
1398
+ function frameSchema(limits) {
1399
+ const cached = cache2.get(limits);
1400
+ if (cached !== void 0) return cached;
1401
+ const built = buildFrameSchema(limits);
1402
+ cache2.set(limits, built);
1403
+ return built;
1404
+ }
1405
+ var probeInfoSchema = z4.strictObject({
1406
+ framework: z4.string().min(1).max(128),
1407
+ frameworkVersion: z4.string().max(128).optional(),
1408
+ probeVersion: z4.string().min(1).max(128),
1409
+ identityKind: z4.enum(["stable", "frame-local"]),
1410
+ capabilities: z4.array(z4.enum(PROBE_CAPABILITIES)).max(PROBE_CAPABILITIES.length)
1411
+ });
1412
+ function validateProbeInfo(value) {
1413
+ const parsed = probeInfoSchema.safeParse(value);
1414
+ if (!parsed.success) {
1415
+ const issue = parsed.error.issues[0];
1416
+ const where = issue.path.length > 0 ? issue.path.map(String).join(".") : "<root>";
1417
+ return { ok: false, detail: `${where}: ${issue.message}` };
1418
+ }
1419
+ const info = parsed.data;
1420
+ if (info.identityKind === "frame-local" && info.capabilities.includes("stable-identity")) {
1421
+ return {
1422
+ ok: false,
1423
+ detail: "a probe declaring identityKind 'frame-local' must not claim the 'stable-identity' capability: nothing in an immediate-mode frame survives to be correlated"
1424
+ };
1425
+ }
1426
+ return { ok: true, info: Object.freeze(info) };
1427
+ }
1428
+ function validateProbeFrame(value, limits) {
1429
+ let projected;
1430
+ try {
1431
+ projected = projectDto(value, limits.maxDepth);
1432
+ } catch (error) {
1433
+ if (error instanceof ProtocolViolation) {
1434
+ return fail4(error.code === "dto-depth" ? "depth" : "schema", error.message);
1435
+ }
1436
+ return fail4("schema", "value could not be projected into a plain DTO");
1437
+ }
1438
+ const serialised = JSON.stringify(projected);
1439
+ if (serialised === void 0) return fail4("schema", "probe frame is not a JSON object");
1440
+ const bytes = Buffer5.byteLength(serialised, "utf8");
1441
+ if (bytes > limits.maxSnapshotBytes) {
1442
+ return fail4("bytes", `probe frame is ${bytes} bytes, ceiling is ${limits.maxSnapshotBytes}`);
1443
+ }
1444
+ const parsed = frameSchema(limits).safeParse(projected);
1445
+ if (!parsed.success) {
1446
+ const issue = parsed.error.issues[0];
1447
+ const path = issue.path.map(String);
1448
+ const where = path.length > 0 ? path.join(".") : "<root>";
1449
+ const code = path.includes("intendedRect") || path.includes("visibleRect") ? "bad-rect" : path.includes("frame") ? "revision" : issue.code === "too_big" ? "count" : "schema";
1450
+ return fail4(code, `${where}: ${issue.message}`);
1451
+ }
1452
+ const frame = projected;
1453
+ const seen = /* @__PURE__ */ new Set();
1454
+ for (const object of frame.objects) {
1455
+ if (seen.has(object.identity.value)) {
1456
+ return fail4("duplicate-id", `identity ${object.identity.value} appears twice in the frame`);
1457
+ }
1458
+ seen.add(object.identity.value);
1459
+ }
1460
+ for (const object of frame.objects) {
1461
+ if (object.parent !== void 0 && !seen.has(object.parent)) {
1462
+ return fail4(
1463
+ "missing-parent",
1464
+ `object ${object.identity.value} names parent ${object.parent}, which is not in the frame`
1465
+ );
1466
+ }
1467
+ if (object.parent === object.identity.value) {
1468
+ return fail4("cycle", `object ${object.identity.value} is its own parent`);
1469
+ }
1470
+ const unobservable = object.unobservable;
1471
+ if (unobservable === void 0) continue;
1472
+ const declared = new Set(unobservable);
1473
+ if (declared.size !== unobservable.length) {
1474
+ return fail4("duplicate-id", `object ${object.identity.value} repeats an unobservable field`);
1475
+ }
1476
+ for (const [field, present] of [
1477
+ ["text", object.text !== void 0],
1478
+ ["parent", object.parent !== void 0],
1479
+ ["intendedRect", object.geometry?.intendedRect !== void 0],
1480
+ ["visibleRect", object.geometry?.visibleRect !== void 0],
1481
+ ["paintOrder", object.paintOrder !== void 0]
1482
+ ]) {
1483
+ if (declared.has(field) && present) {
1484
+ return fail4(
1485
+ "schema",
1486
+ `object ${object.identity.value} reports ${field} and also declares it unobservable`
1487
+ );
1488
+ }
1489
+ }
1490
+ for (const [field, value_] of Object.entries(object.state ?? {})) {
1491
+ if (declared.has(field) && value_ !== void 0) {
1492
+ return fail4(
1493
+ "schema",
1494
+ `object ${object.identity.value} reports state.${field} and also declares it unobservable`
1495
+ );
1496
+ }
1497
+ }
1498
+ }
1499
+ return { ok: true, frame };
1500
+ }
1501
+ function validateProbeAnnotations(value, limits) {
1502
+ const result = validateProbeFrame(
1503
+ {
1504
+ frame: 1,
1505
+ objects: [
1506
+ {
1507
+ identity: { kind: "stable", value: "a" },
1508
+ frameworkType: "A",
1509
+ annotations: value
1510
+ }
1511
+ ]
1512
+ },
1513
+ limits
1514
+ );
1515
+ if (!result.ok) return { ok: false, code: result.code, detail: result.detail };
1516
+ const annotations = result.frame.objects[0]?.annotations;
1517
+ if (annotations === void 0) {
1518
+ return {
1519
+ ok: false,
1520
+ code: "schema",
1521
+ detail: "annotations: expected an annotation object"
1522
+ };
1523
+ }
1524
+ try {
1525
+ encodeFrame({ annotations }, limits.maxFrameBytes);
1526
+ } catch (error) {
1527
+ return {
1528
+ ok: false,
1529
+ code: error instanceof ProtocolViolation && error.code === "frame-oversized" ? "bytes" : "schema",
1530
+ detail: error instanceof Error ? error.message : "annotations could not be framed"
1531
+ };
1532
+ }
1533
+ return { ok: true, annotations };
1534
+ }
1535
+
1536
+ // src/messages.ts
1537
+ import { z as z5 } from "zod";
1538
+ var ADAPTER_CAPABILITIES = [
1539
+ "tree",
1540
+ "bounds",
1541
+ "absolute-bounds",
1542
+ "states",
1543
+ "actions",
1544
+ "text-ranges",
1545
+ "render-revisions",
1546
+ "tree-diffs",
1547
+ "logs",
1548
+ "qualified-observations",
1549
+ "pointer-hit-grid"
1550
+ ];
1551
+ var MAX_IDENTIFIER_LENGTH = 1024;
1552
+ var identifier = z5.string().max(MAX_IDENTIFIER_LENGTH);
1553
+ var nonEmptyIdentifier = identifier.min(1);
1554
+ var safeIndex = z5.number().refine((n) => Number.isSafeInteger(n) && n >= 0, "expected a non-negative safe integer");
1555
+ var revisionNumber = z5.number().refine((n) => Number.isSafeInteger(n) && n > 0, "expected a positive safe integer");
1556
+ var limitsSchema = z5.object({
1557
+ maxFrameBytes: revisionNumber,
1558
+ maxSnapshotBytes: revisionNumber,
1559
+ maxNodes: revisionNumber,
1560
+ maxDepth: revisionNumber,
1561
+ maxStringBytes: revisionNumber,
1562
+ maxRelationTargets: revisionNumber,
1563
+ maxQueuedFrames: revisionNumber,
1564
+ maxPendingWaiters: revisionNumber,
1565
+ maxSessions: revisionNumber,
1566
+ maxLogRecordBytes: revisionNumber,
1567
+ maxLogQueue: revisionNumber
1568
+ });
1569
+ var errorFields = {
1570
+ type: z5.literal("error"),
1571
+ code: z5.enum(["bad-token", "bad-version", "malformed", "limit-exceeded", "internal"]),
1572
+ message: z5.string().max(MAX_IDENTIFIER_LENGTH)
1573
+ };
1574
+ var errorSchema = z5.strictObject(errorFields);
1575
+ var errorFromDriverSchema = z5.object(errorFields);
1576
+ var helloSchema = z5.strictObject({
1577
+ type: z5.literal("hello"),
1578
+ protocol: z5.union([z5.literal(PROTOCOL_ID), z5.literal(PROTOCOL_V2_ID)]),
1579
+ token: nonEmptyIdentifier,
1580
+ adapter: z5.strictObject({ name: nonEmptyIdentifier, version: nonEmptyIdentifier }),
1581
+ capabilities: z5.array(z5.enum(ADAPTER_CAPABILITIES)).max(ADAPTER_CAPABILITIES.length),
1582
+ probe: probeInfoSchema.optional()
1583
+ });
1584
+ var frameBeginSchema = z5.strictObject({
1585
+ type: z5.literal("frame-begin"),
1586
+ revision: revisionNumber
1587
+ });
1588
+ var revisionCommitSchema = z5.strictObject({
1589
+ type: z5.literal("revision-commit"),
1590
+ revision: revisionNumber
1591
+ });
1592
+ var snapshotEnvelopeSchema = z5.strictObject({
1593
+ type: z5.literal("snapshot"),
1594
+ snapshot: z5.unknown()
1595
+ });
1596
+ var treeDeltaTypeSchema = z5.object({ type: z5.literal("tree-delta") });
1597
+ var logEnvelopeSchema = z5.strictObject({
1598
+ type: z5.literal("log"),
1599
+ record: z5.unknown()
1600
+ });
1601
+ var getTreeResultSchema = z5.strictObject({
1602
+ type: z5.literal("get-tree-result"),
1603
+ requestId: safeIndex,
1604
+ snapshot: z5.unknown().optional(),
1605
+ error: z5.string().max(MAX_IDENTIFIER_LENGTH).optional()
1606
+ }).refine(
1607
+ (m) => m.snapshot === void 0 !== (m.error === void 0),
1608
+ "exactly one of snapshot or error must be present"
1609
+ );
1610
+ var helloAckSchema = z5.object({
1611
+ type: z5.literal("hello-ack"),
1612
+ protocol: z5.union([z5.literal(PROTOCOL_ID), z5.literal(PROTOCOL_V2_ID)]),
1613
+ sessionId: nonEmptyIdentifier,
1614
+ limits: limitsSchema,
1615
+ subscribe: z5.enum(["snapshots", "revisions", "diffs"]),
1616
+ marker: z5.object({ enabled: z5.boolean() }),
1617
+ logs: z5.object({
1618
+ enabled: z5.boolean(),
1619
+ maxRecordsPerSecond: revisionNumber,
1620
+ burst: safeIndex
1621
+ }).optional()
1622
+ });
1623
+ var getTreeRequestSchema = z5.object({
1624
+ type: z5.literal("get-tree"),
1625
+ requestId: safeIndex,
1626
+ revision: revisionNumber.optional()
1627
+ });
1628
+ function malformed(detail) {
1629
+ return { ok: false, code: "malformed", detail };
1630
+ }
1631
+ function project(value, limits) {
1632
+ try {
1633
+ return { ok: true, message: projectDto(value, limits.maxDepth) };
1634
+ } catch (error) {
1635
+ const detail = error instanceof ProtocolViolation ? error.message : "value is not a plain JSON DTO";
1636
+ return error instanceof ProtocolViolation && error.code === "dto-depth" ? { ok: false, code: "limit-exceeded", detail } : malformed(detail);
1637
+ }
1638
+ }
1639
+ function messageType(value) {
1640
+ if (typeof value !== "object" || value === null) return null;
1641
+ const type = value.type;
1642
+ return typeof type === "string" ? type : null;
1643
+ }
1644
+ function check(schema, value) {
1645
+ const result = schema.safeParse(value);
1646
+ if (result.success) return null;
1647
+ const issue = result.error.issues[0];
1648
+ const where = issue.path.length > 0 ? issue.path.map(String).join(".") : "<root>";
1649
+ return `${where}: ${issue.message}`;
1650
+ }
1651
+ function checkSnapshot(value, limits) {
1652
+ const result = validateSnapshot(value, limits);
1653
+ if (result.ok) return null;
1654
+ const overCapacity = result.code === "bytes" || result.code === "count" || result.code === "depth" || result.code === "string-bytes";
1655
+ return {
1656
+ ok: false,
1657
+ code: overCapacity ? "limit-exceeded" : "malformed",
1658
+ detail: `snapshot ${result.code}: ${result.detail}`
1659
+ };
1660
+ }
1661
+ function checkLogRecord(value, limits) {
1662
+ const result = validateLogRecord(value, limits);
1663
+ if (result.ok) return null;
1664
+ const overCapacity = result.code === "bytes" || result.code === "count" || result.code === "depth" || result.code === "string-bytes";
1665
+ return {
1666
+ ok: false,
1667
+ code: overCapacity ? "limit-exceeded" : "malformed",
1668
+ detail: `log record ${result.code}: ${result.detail}`
1669
+ };
1670
+ }
1671
+ function checkTreeDelta(value, limits) {
1672
+ const result = validateTreeDelta(value, limits);
1673
+ if (result.ok) return null;
1674
+ const overCapacity = result.code === "bytes" || result.code === "count" || result.code === "depth" || result.code === "string-bytes";
1675
+ return {
1676
+ ok: false,
1677
+ code: overCapacity ? "limit-exceeded" : "malformed",
1678
+ detail: `tree delta ${result.code}: ${result.detail}`
1679
+ };
1680
+ }
1681
+ function parseAdapterMessage(value, limits) {
1682
+ const projected = project(value, limits);
1683
+ if (!projected.ok) return projected;
1684
+ const dto = projected.message;
1685
+ switch (messageType(dto)) {
1686
+ case "hello": {
1687
+ const protocol = dto.protocol;
1688
+ if (typeof protocol === "string" && protocol !== PROTOCOL_ID && protocol !== PROTOCOL_V2_ID) {
1689
+ return { ok: false, code: "bad-version", detail: `unsupported protocol ${protocol}` };
1690
+ }
1691
+ const issue = check(helloSchema, dto);
1692
+ if (issue !== null) return malformed(issue);
1693
+ const candidate = dto;
1694
+ const qualified = candidate.capabilities.includes("qualified-observations");
1695
+ if (candidate.protocol === PROTOCOL_V2_ID !== qualified) {
1696
+ return malformed(
1697
+ candidate.protocol === PROTOCOL_V2_ID ? "termwright/2 requires the 'qualified-observations' capability" : "'qualified-observations' requires termwright/2"
1698
+ );
1699
+ }
1700
+ if (candidate.capabilities.includes("pointer-hit-grid") && !qualified) {
1701
+ return malformed("'pointer-hit-grid' requires qualified observations");
1702
+ }
1703
+ const probe = dto.probe;
1704
+ if (probe !== void 0) {
1705
+ const checked = validateProbeInfo(probe);
1706
+ if (!checked.ok) return malformed(`probe: ${checked.detail}`);
1707
+ }
1708
+ return { ok: true, message: dto };
1709
+ }
1710
+ case "revision-commit": {
1711
+ const issue = check(revisionCommitSchema, dto);
1712
+ return issue === null ? { ok: true, message: dto } : malformed(issue);
1713
+ }
1714
+ case "snapshot": {
1715
+ const issue = check(snapshotEnvelopeSchema, dto);
1716
+ if (issue !== null) return malformed(issue);
1717
+ const bad = checkSnapshot(dto.snapshot, limits);
1718
+ return bad ?? { ok: true, message: dto };
1719
+ }
1720
+ case "get-tree-result": {
1721
+ const issue = check(getTreeResultSchema, dto);
1722
+ if (issue !== null) return malformed(issue);
1723
+ const envelope = dto;
1724
+ if (envelope.snapshot !== void 0) {
1725
+ const bad = checkSnapshot(envelope.snapshot, limits);
1726
+ if (bad !== null) return bad;
1727
+ }
1728
+ return { ok: true, message: dto };
1729
+ }
1730
+ case "frame-begin": {
1731
+ const issue = check(frameBeginSchema, dto);
1732
+ return issue === null ? { ok: true, message: dto } : malformed(issue);
1733
+ }
1734
+ case "tree-delta": {
1735
+ const issue = check(treeDeltaTypeSchema, dto);
1736
+ if (issue !== null) return malformed(issue);
1737
+ const { type: _type, ...body } = dto;
1738
+ const bad = checkTreeDelta(body, limits);
1739
+ return bad ?? { ok: true, message: dto };
1740
+ }
1741
+ case "log": {
1742
+ const issue = check(logEnvelopeSchema, dto);
1743
+ if (issue !== null) return malformed(issue);
1744
+ const bad = checkLogRecord(dto.record, limits);
1745
+ return bad ?? { ok: true, message: dto };
1746
+ }
1747
+ case "error": {
1748
+ const issue = check(errorSchema, dto);
1749
+ return issue === null ? { ok: true, message: dto } : malformed(issue);
1750
+ }
1751
+ default:
1752
+ return malformed("unknown or missing message type");
1753
+ }
1754
+ }
1755
+ function parseDriverMessage(value, limits) {
1756
+ const projected = project(value, limits);
1757
+ if (!projected.ok) return projected;
1758
+ const dto = projected.message;
1759
+ switch (messageType(dto)) {
1760
+ case "hello-ack": {
1761
+ const protocol = dto.protocol;
1762
+ if (typeof protocol === "string" && protocol !== PROTOCOL_ID && protocol !== PROTOCOL_V2_ID) {
1763
+ return { ok: false, code: "bad-version", detail: `unsupported protocol ${protocol}` };
1764
+ }
1765
+ const issue = check(helloAckSchema, dto);
1766
+ return issue === null ? { ok: true, message: dto } : malformed(issue);
1767
+ }
1768
+ case "get-tree": {
1769
+ const issue = check(getTreeRequestSchema, dto);
1770
+ return issue === null ? { ok: true, message: dto } : malformed(issue);
1771
+ }
1772
+ case "error": {
1773
+ const issue = check(errorFromDriverSchema, dto);
1774
+ return issue === null ? { ok: true, message: dto } : malformed(issue);
1775
+ }
1776
+ default:
1777
+ return malformed("unknown or missing message type");
1778
+ }
1779
+ }
1780
+
1781
+ // src/marker.ts
1782
+ import { Buffer as Buffer6 } from "buffer";
1783
+ import { createHmac, timingSafeEqual } from "crypto";
1784
+ var MARKER_OSC_CODE = 8487;
1785
+ var MARKER_OSC_PREFIX = "twm;";
1786
+ var MARKER_MAC_BYTES = 16;
1787
+ var MARKER_MAC_CHARS = 22;
1788
+ var REVISION_TEXT = /^[1-9][0-9]{0,15}$/;
1789
+ var MAC_TEXT = new RegExp(`^[A-Za-z0-9_-]{${MARKER_MAC_CHARS}}$`);
1790
+ var BEL = "\x07";
1791
+ var ST = "\x1B\\";
1792
+ function computeMac(token, sessionId, revision) {
1793
+ return createHmac("sha256", token).update(`${sessionId}:${revision}`, "utf8").digest().subarray(0, MARKER_MAC_BYTES).toString("base64url");
1794
+ }
1795
+ function encodeMarker(token, sessionId, revision) {
1796
+ if (token.length === 0) {
1797
+ throw new ProtocolViolation("marker-argument", "token must not be empty");
1798
+ }
1799
+ if (sessionId.length === 0) {
1800
+ throw new ProtocolViolation("marker-argument", "sessionId must not be empty");
1801
+ }
1802
+ if (!Number.isSafeInteger(revision) || revision <= 0) {
1803
+ throw new ProtocolViolation("marker-argument", "revision must be a positive safe integer");
1804
+ }
1805
+ const mac = computeMac(token, sessionId, revision);
1806
+ return `\x1B]${MARKER_OSC_CODE};${MARKER_OSC_PREFIX}${revision};${mac}${BEL}`;
1807
+ }
1808
+ function verifyMarkerPayload(payload, token, sessionId) {
1809
+ if (token.length === 0 || sessionId.length === 0) return null;
1810
+ let text = payload;
1811
+ if (text.endsWith(BEL)) text = text.slice(0, -BEL.length);
1812
+ else if (text.endsWith(ST)) text = text.slice(0, -ST.length);
1813
+ if (!text.startsWith(MARKER_OSC_PREFIX)) return null;
1814
+ const body = text.slice(MARKER_OSC_PREFIX.length);
1815
+ const separator = body.indexOf(";");
1816
+ if (separator < 0) return null;
1817
+ const revisionText = body.slice(0, separator);
1818
+ const mac = body.slice(separator + 1);
1819
+ if (!REVISION_TEXT.test(revisionText)) return null;
1820
+ if (!MAC_TEXT.test(mac)) return null;
1821
+ const revision = Number(revisionText);
1822
+ if (!Number.isSafeInteger(revision) || revision <= 0) return null;
1823
+ const expected = Buffer6.from(computeMac(token, sessionId, revision), "utf8");
1824
+ const actual = Buffer6.from(mac, "utf8");
1825
+ if (expected.length !== actual.length) return null;
1826
+ if (!timingSafeEqual(expected, actual)) return null;
1827
+ return Object.freeze({ revision, mac });
1828
+ }
1829
+ export {
1830
+ ABSOLUTE_LIMITS,
1831
+ ACCESSKIT_ROLE_BY_SEMANTIC_ROLE,
1832
+ ACCESSKIT_ROOT_TREE_ID,
1833
+ ADAPTER_CAPABILITIES,
1834
+ DEFAULT_LIMITS,
1835
+ DEFAULT_NEGOTIATION_MS,
1836
+ ENV_ENDPOINT,
1837
+ ENV_PROTOCOL,
1838
+ ENV_TOKEN,
1839
+ FRAMEWORK_OBSERVATION_CAPABILITIES,
1840
+ FRAMEWORK_OPERATION_CAPABILITIES,
1841
+ FRAME_HEADER_BYTES,
1842
+ LOG_LEVELS,
1843
+ LOG_LEVEL_SEVERITY,
1844
+ MARKER_MAC_BYTES,
1845
+ MARKER_OSC_CODE,
1846
+ MARKER_OSC_PREFIX,
1847
+ MAX_LOG_ATTRS,
1848
+ PROBE_CAPABILITIES,
1849
+ PROBE_UNOBSERVABLE_FIELDS,
1850
+ PROTOCOL_ID,
1851
+ PROTOCOL_V2_ID,
1852
+ PROTOCOL_VERSION,
1853
+ PROVENANCE_SOURCES,
1854
+ ProtocolViolation,
1855
+ SEMANTIC_ACTIONS,
1856
+ SEMANTIC_NODE_KEYS,
1857
+ SEMANTIC_ROLES,
1858
+ SEMANTIC_STATE_KEYS,
1859
+ SUPPORTED_PROTOCOL_IDS,
1860
+ TOKEN_BYTES,
1861
+ accessKitNodeId,
1862
+ applyTreeDelta,
1863
+ createFrameDecoder,
1864
+ encodeFrame,
1865
+ encodeMarker,
1866
+ frameworkObservationCapabilities,
1867
+ generateToken,
1868
+ intersectRects,
1869
+ parseAdapterMessage,
1870
+ parseDriverMessage,
1871
+ probeInfoSchema,
1872
+ projectDto,
1873
+ rectArea,
1874
+ resolveNodeBounds,
1875
+ spatialRelation,
1876
+ toAccessKitTreeUpdate,
1877
+ validateLogRecord,
1878
+ validateProbeAnnotations,
1879
+ validateProbeFrame,
1880
+ validateProbeInfo,
1881
+ validateSnapshot,
1882
+ validateTreeDelta,
1883
+ verifyMarkerPayload,
1884
+ viewportIntersection
1885
+ };
1886
+ //# sourceMappingURL=index.js.map