@webskill/sdk 0.28.1 → 0.30.0-next.1

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.
Files changed (37) hide show
  1. package/dist/agent.d.ts +2 -2
  2. package/dist/agent.js +32 -766
  3. package/dist/{approval-5BmspsRi.js → approval-DnUytrkq.js} +6 -1
  4. package/dist/browser.d.ts +9 -9
  5. package/dist/browser.js +131 -38
  6. package/dist/controller.d.ts +88 -0
  7. package/dist/controller.js +3 -0
  8. package/dist/device-DMUgqNzr.js +1782 -0
  9. package/dist/{dist-CTsxblSD.js → dist-Ww6TChcf.js} +2 -2
  10. package/dist/{geometry-BXMPNuM6.js → geometry-1xQbK4hB.js} +14 -78
  11. package/dist/governance.d.ts +3 -3
  12. package/dist/{index-DJeYqzlE.d.ts → index-1ASrUwzn.d.ts} +2 -2
  13. package/dist/{index-CkauSvus.d.ts → index-Cj2HedfL.d.ts} +4 -3
  14. package/dist/{index-DU8sKyQq.d.ts → index-UbNuzd2a.d.ts} +4 -2
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +5 -6
  17. package/dist/{linkedDocument-0iiVPd5t.js → linkedDocument-GE-VV_Gj.js} +112 -2
  18. package/dist/mcp.d.ts +2 -2
  19. package/dist/node.d.ts +3 -3
  20. package/dist/node.js +2 -2
  21. package/dist/{openUiLibrary-BnhYUlqY.js → openUiLibrary-BjRDYyTe.js} +1 -1
  22. package/dist/{skillVersionStore-S4_HJ_Fl-DzZM_UIw.d.ts → skillVersionStore-S4_HJ_Fl-5K8jrAg_.d.ts} +1 -1
  23. package/dist/testing.d.ts +88 -2
  24. package/dist/testing.js +1287 -1
  25. package/dist/toolSource-DMCTnSVg.js +743 -0
  26. package/dist/types-BXRIoabj.js +76 -0
  27. package/dist/types-DmcXN3qR-Dy7ZDstc.d.ts +265 -0
  28. package/dist/{types-wKp3YBSM-167azx1H.d.ts → types-wKp3YBSM-CSeFUL9R.d.ts} +3 -2
  29. package/dist/ui-react.css +2687 -0
  30. package/dist/ui-react.d.ts +1045 -5
  31. package/dist/ui-react.js +5878 -820
  32. package/dist/ui-vue.d.ts +1 -1
  33. package/dist/ui.d.ts +3 -3
  34. package/dist/ui.js +1 -1
  35. package/dist/{webSkillApi-rX2e3SVB.js → webSkillApi-TeWPPFDn.js} +1 -1
  36. package/package.json +18 -4
  37. package/dist/kind-Dc8x0HWz.js +0 -110
@@ -0,0 +1,1782 @@
1
+ import { t as WebSkillError } from "./errors-BDZNpC13.js";
2
+ import { l as PERCEIVE_PAGE_TOOL, n as FOCUS_PAGE_TAB_TOOL, o as PAGE_ACTION_TOOL, r as LIST_PAGE_TABS_TOOL, t as CLOSE_PAGE_TAB_TOOL } from "./toolSource-DMCTnSVg.js";
3
+ import { Validator } from "@cfworker/json-schema";
4
+
5
+ //#region ../controller/src/constants.ts
6
+ /** @stable */
7
+ const CONTROLLER_PROTOCOL_VERSION = "0.1";
8
+ /** @experimental */
9
+ const CONTROLLER_TOOL_NAMES = [
10
+ "browser_status",
11
+ PERCEIVE_PAGE_TOOL,
12
+ PAGE_ACTION_TOOL,
13
+ LIST_PAGE_TABS_TOOL,
14
+ FOCUS_PAGE_TAB_TOOL,
15
+ CLOSE_PAGE_TAB_TOOL
16
+ ];
17
+ /** @experimental */
18
+ const CONTROLLER_PROTOCOL_LIMITS = {
19
+ maxMessageBytes: 16 * 1024 * 1024,
20
+ oversizeWindowMs: 6e4,
21
+ oversizeThreshold: 3,
22
+ invocationContextMaxBytes: 4096,
23
+ pairingCodeTtlMaxMs: 6e5,
24
+ pairingAttemptsMax: 5,
25
+ deviceConfirmTimeoutMs: 12e4,
26
+ grantTtlMaxMs: 36e5,
27
+ heartbeatIntervalMs: 2e4,
28
+ heartbeatTimeoutMs: 45e3,
29
+ reconnectMaxDelayMs: 3e4,
30
+ localAuthorizationTimeoutMs: 12e4,
31
+ idempotencyCacheEntries: 256,
32
+ idempotencyCacheTtlMs: 6e5
33
+ };
34
+ const CONTROLLER_JSONRPC_CODES = {
35
+ CONTROLLER_MESSAGE_INVALID: -32602,
36
+ CONTROLLER_CAPABILITY_MISSING: -32602,
37
+ CONTROLLER_NOT_PAIRED: -32001,
38
+ CONTROLLER_PROTOCOL_UNSUPPORTED: -32002,
39
+ CONTROLLER_PAIRING_REJECTED: -32003,
40
+ CONTROLLER_PAIRING_RATE_LIMITED: -32004,
41
+ CONTROLLER_HUMAN_IN_CONTROL: -32e3,
42
+ CONTROLLER_INVOCATION_DUPLICATE: -32005
43
+ };
44
+ /** @experimental */
45
+ const CONTROLLER_ERROR_RETRYABLE = {
46
+ CONTROLLER_PROTOCOL_UNSUPPORTED: false,
47
+ CONTROLLER_MESSAGE_INVALID: false,
48
+ CONTROLLER_NOT_PAIRED: false,
49
+ CONTROLLER_PAIRING_REJECTED: false,
50
+ CONTROLLER_PAIRING_RATE_LIMITED: true,
51
+ CONTROLLER_CAPABILITY_MISSING: false,
52
+ CONTROLLER_HUMAN_IN_CONTROL: true,
53
+ CONTROLLER_INVOCATION_DUPLICATE: false
54
+ };
55
+ const CONTROLLER_WRITE_TOOLS = [
56
+ PAGE_ACTION_TOOL,
57
+ FOCUS_PAGE_TAB_TOOL,
58
+ CLOSE_PAGE_TAB_TOOL
59
+ ];
60
+
61
+ //#endregion
62
+ //#region ../controller/src/errors.ts
63
+ var ControllerRpcError = class extends Error {
64
+ code;
65
+ retryable;
66
+ constructor(code, message) {
67
+ super(message);
68
+ this.name = "ControllerRpcError";
69
+ this.code = code;
70
+ this.retryable = CONTROLLER_ERROR_RETRYABLE[code];
71
+ }
72
+ };
73
+ function controllerError(code, message) {
74
+ return new ControllerRpcError(code, message);
75
+ }
76
+ function asControllerRpcError(error) {
77
+ if (error instanceof ControllerRpcError) return error;
78
+ if (error instanceof WebSkillError && error.code.startsWith("CONTROLLER_")) return new ControllerRpcError(error.code, error.message);
79
+ return new ControllerRpcError("CONTROLLER_MESSAGE_INVALID", error instanceof Error ? error.message : String(error));
80
+ }
81
+ function toJsonRpcError(error) {
82
+ const normalized = asControllerRpcError(error);
83
+ return {
84
+ code: CONTROLLER_JSONRPC_CODES[normalized.code],
85
+ message: normalized.message,
86
+ data: {
87
+ code: normalized.code,
88
+ retryable: normalized.retryable
89
+ }
90
+ };
91
+ }
92
+
93
+ //#endregion
94
+ //#region ../controller/src/jsonRpc.ts
95
+ function byteLength(value) {
96
+ return new TextEncoder().encode(value).byteLength;
97
+ }
98
+ function isJsonRpcObject(value) {
99
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value.jsonrpc === "2.0";
100
+ }
101
+
102
+ //#endregion
103
+ //#region ../controller/schema/protocol.schema.json
104
+ var protocol_schema_default = {
105
+ $schema: "https://json-schema.org/draft/2020-12/schema",
106
+ $id: "https://webskill.dev/schema/controller/0.1/protocol.schema.json",
107
+ title: "Protocol",
108
+ type: "object",
109
+ $defs: {
110
+ "ProtocolVersion": {
111
+ "title": "ProtocolVersion",
112
+ "type": "string",
113
+ "pattern": "^[0-9]+\\.[0-9]+$",
114
+ "maxLength": 16
115
+ },
116
+ "IsoDateTime": {
117
+ "title": "IsoDateTime",
118
+ "type": "string",
119
+ "format": "date-time",
120
+ "maxLength": 40
121
+ },
122
+ "Base64Url": {
123
+ "title": "Base64Url",
124
+ "type": "string",
125
+ "pattern": "^[A-Za-z0-9_-]*$",
126
+ "maxLength": 8192
127
+ },
128
+ "Origin": {
129
+ "title": "Origin",
130
+ "type": "string",
131
+ "pattern": "^https?://[^/]+$",
132
+ "maxLength": 2048
133
+ },
134
+ "BoundedString": {
135
+ "title": "BoundedString",
136
+ "type": "string",
137
+ "maxLength": 4096
138
+ },
139
+ "InvocationMeta": {
140
+ "title": "InvocationMeta",
141
+ "type": "object",
142
+ "required": ["webskill/invocationId"],
143
+ "properties": {
144
+ "webskill/invocationId": {
145
+ "type": "string",
146
+ "pattern": "^[A-Za-z0-9._:-]{1,128}$"
147
+ },
148
+ "webskill/deviceEpoch": { "$ref": "#/$defs/BoundedString" },
149
+ "webskill/sequence": {
150
+ "type": "integer",
151
+ "minimum": 1
152
+ },
153
+ "webskill/context": {
154
+ "type": "object",
155
+ "maxProperties": 128
156
+ }
157
+ },
158
+ "additionalProperties": true
159
+ },
160
+ "Principal": {
161
+ "title": "Principal",
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "required": ["displayName"],
165
+ "properties": { "displayName": { "$ref": "#/$defs/BoundedString" } }
166
+ },
167
+ "HelloParams": {
168
+ "title": "HelloParams",
169
+ "type": "object",
170
+ "additionalProperties": false,
171
+ "required": [
172
+ "protocolVersions",
173
+ "hostName",
174
+ "hostVersion"
175
+ ],
176
+ "properties": {
177
+ "protocolVersions": {
178
+ "type": "array",
179
+ "minItems": 1,
180
+ "maxItems": 8,
181
+ "items": { "$ref": "#/$defs/ProtocolVersion" }
182
+ },
183
+ "hostName": { "$ref": "#/$defs/BoundedString" },
184
+ "hostVersion": { "$ref": "#/$defs/BoundedString" },
185
+ "deviceFingerprint": { "$ref": "#/$defs/BoundedString" }
186
+ }
187
+ },
188
+ "HelloResult": {
189
+ "title": "HelloResult",
190
+ "type": "object",
191
+ "additionalProperties": false,
192
+ "required": [
193
+ "protocolVersion",
194
+ "controllerInstanceId",
195
+ "controllerName",
196
+ "excludedOrigins"
197
+ ],
198
+ "properties": {
199
+ "protocolVersion": { "$ref": "#/$defs/ProtocolVersion" },
200
+ "controllerInstanceId": { "$ref": "#/$defs/BoundedString" },
201
+ "controllerName": { "$ref": "#/$defs/BoundedString" },
202
+ "excludedOrigins": {
203
+ "type": "array",
204
+ "maxItems": 128,
205
+ "items": { "$ref": "#/$defs/Origin" }
206
+ }
207
+ }
208
+ },
209
+ "PairStartParams": {
210
+ "title": "PairStartParams",
211
+ "type": "object",
212
+ "additionalProperties": false,
213
+ "required": ["publicKey", "deviceName"],
214
+ "properties": {
215
+ "publicKey": { "$ref": "#/$defs/Base64Url" },
216
+ "deviceName": { "$ref": "#/$defs/BoundedString" }
217
+ }
218
+ },
219
+ "PairStartResult": {
220
+ "title": "PairStartResult",
221
+ "type": "object",
222
+ "additionalProperties": false,
223
+ "required": [
224
+ "sessionId",
225
+ "code",
226
+ "expiresAt"
227
+ ],
228
+ "properties": {
229
+ "sessionId": { "$ref": "#/$defs/BoundedString" },
230
+ "code": {
231
+ "type": "string",
232
+ "pattern": "^[A-Z0-9-]+$",
233
+ "maxLength": 32
234
+ },
235
+ "expiresAt": { "$ref": "#/$defs/IsoDateTime" }
236
+ }
237
+ },
238
+ "PairApprovedParams": {
239
+ "title": "PairApprovedParams",
240
+ "type": "object",
241
+ "additionalProperties": false,
242
+ "required": [
243
+ "sessionId",
244
+ "approvalNonce",
245
+ "controllerInstanceId",
246
+ "principal",
247
+ "capabilities",
248
+ "excludedOrigins"
249
+ ],
250
+ "properties": {
251
+ "sessionId": { "$ref": "#/$defs/BoundedString" },
252
+ "approvalNonce": { "$ref": "#/$defs/Base64Url" },
253
+ "controllerInstanceId": { "$ref": "#/$defs/BoundedString" },
254
+ "principal": { "$ref": "#/$defs/Principal" },
255
+ "capabilities": {
256
+ "type": "array",
257
+ "maxItems": 128,
258
+ "items": { "$ref": "#/$defs/BoundedString" }
259
+ },
260
+ "excludedOrigins": {
261
+ "type": "array",
262
+ "maxItems": 128,
263
+ "items": { "$ref": "#/$defs/Origin" }
264
+ }
265
+ }
266
+ },
267
+ "PairRejectedParams": {
268
+ "title": "PairRejectedParams",
269
+ "type": "object",
270
+ "additionalProperties": false,
271
+ "required": ["sessionId", "code"],
272
+ "properties": {
273
+ "sessionId": { "$ref": "#/$defs/BoundedString" },
274
+ "code": { "$ref": "#/$defs/BoundedString" }
275
+ }
276
+ },
277
+ "PairConfirmParams": {
278
+ "title": "PairConfirmParams",
279
+ "type": "object",
280
+ "additionalProperties": false,
281
+ "required": ["sessionId", "accepted"],
282
+ "properties": {
283
+ "sessionId": { "$ref": "#/$defs/BoundedString" },
284
+ "accepted": { "type": "boolean" },
285
+ "signature": { "$ref": "#/$defs/Base64Url" }
286
+ }
287
+ },
288
+ "PairConfirmResult": {
289
+ "title": "PairConfirmResult",
290
+ "type": "object",
291
+ "additionalProperties": false,
292
+ "properties": {
293
+ "grant": { "$ref": "#/$defs/BoundedString" },
294
+ "expiresAt": { "$ref": "#/$defs/IsoDateTime" }
295
+ }
296
+ },
297
+ "AuthPresentParams": {
298
+ "title": "AuthPresentParams",
299
+ "type": "object",
300
+ "additionalProperties": false,
301
+ "required": ["grant"],
302
+ "properties": { "grant": { "$ref": "#/$defs/BoundedString" } }
303
+ },
304
+ "AuthPresentResult": {
305
+ "title": "AuthPresentResult",
306
+ "type": "object",
307
+ "additionalProperties": false,
308
+ "required": ["expiresAt"],
309
+ "properties": { "expiresAt": { "$ref": "#/$defs/IsoDateTime" } }
310
+ },
311
+ "AuthChallengeParams": {
312
+ "title": "AuthChallengeParams",
313
+ "type": "object",
314
+ "additionalProperties": false,
315
+ "properties": {}
316
+ },
317
+ "AuthChallengeResult": {
318
+ "title": "AuthChallengeResult",
319
+ "type": "object",
320
+ "additionalProperties": false,
321
+ "required": ["nonce", "expiresAt"],
322
+ "properties": {
323
+ "nonce": { "$ref": "#/$defs/Base64Url" },
324
+ "expiresAt": { "$ref": "#/$defs/IsoDateTime" }
325
+ }
326
+ },
327
+ "AuthRenewParams": {
328
+ "title": "AuthRenewParams",
329
+ "type": "object",
330
+ "additionalProperties": false,
331
+ "required": ["nonce", "signature"],
332
+ "properties": {
333
+ "nonce": { "$ref": "#/$defs/Base64Url" },
334
+ "signature": { "$ref": "#/$defs/Base64Url" }
335
+ }
336
+ },
337
+ "AuthRenewResult": {
338
+ "title": "AuthRenewResult",
339
+ "type": "object",
340
+ "additionalProperties": false,
341
+ "required": ["grant", "expiresAt"],
342
+ "properties": {
343
+ "grant": { "$ref": "#/$defs/BoundedString" },
344
+ "expiresAt": { "$ref": "#/$defs/IsoDateTime" }
345
+ }
346
+ },
347
+ "AuthRevokedParams": {
348
+ "title": "AuthRevokedParams",
349
+ "type": "object",
350
+ "additionalProperties": false,
351
+ "required": ["reason"],
352
+ "properties": { "reason": { "$ref": "#/$defs/BoundedString" } }
353
+ },
354
+ "OriginsParams": {
355
+ "title": "OriginsParams",
356
+ "type": "object",
357
+ "additionalProperties": false,
358
+ "required": ["excludedOrigins"],
359
+ "properties": { "excludedOrigins": {
360
+ "type": "array",
361
+ "maxItems": 128,
362
+ "items": { "$ref": "#/$defs/Origin" }
363
+ } }
364
+ },
365
+ "HumanControlParams": {
366
+ "title": "HumanControlParams",
367
+ "type": "object",
368
+ "additionalProperties": false,
369
+ "required": ["inControl", "at"],
370
+ "properties": {
371
+ "inControl": { "type": "boolean" },
372
+ "at": { "$ref": "#/$defs/IsoDateTime" }
373
+ }
374
+ },
375
+ "ReadyParams": {
376
+ "title": "ReadyParams",
377
+ "type": "object",
378
+ "additionalProperties": false,
379
+ "required": [
380
+ "deviceEpoch",
381
+ "writeHighWatermark",
382
+ "hostVersion",
383
+ "writeStateRecoverable"
384
+ ],
385
+ "properties": {
386
+ "deviceEpoch": { "$ref": "#/$defs/BoundedString" },
387
+ "writeHighWatermark": {
388
+ "type": "integer",
389
+ "minimum": 0
390
+ },
391
+ "hostVersion": { "$ref": "#/$defs/BoundedString" },
392
+ "writeStateRecoverable": { "type": "boolean" }
393
+ }
394
+ },
395
+ "PingParams": {
396
+ "title": "PingParams",
397
+ "type": "object",
398
+ "additionalProperties": false,
399
+ "properties": {}
400
+ },
401
+ "PingResult": {
402
+ "title": "PingResult",
403
+ "type": "object",
404
+ "additionalProperties": false,
405
+ "properties": {}
406
+ },
407
+ "ByeParams": {
408
+ "title": "ByeParams",
409
+ "type": "object",
410
+ "additionalProperties": false,
411
+ "required": ["code", "message"],
412
+ "properties": {
413
+ "code": { "$ref": "#/$defs/BoundedString" },
414
+ "message": { "$ref": "#/$defs/BoundedString" }
415
+ }
416
+ }
417
+ }
418
+ };
419
+
420
+ //#endregion
421
+ //#region ../controller/src/validate.ts
422
+ /** @experimental */
423
+ const CONTROLLER_PROTOCOL_SCHEMA = protocol_schema_default;
424
+ const METHOD_DEFINITIONS = {
425
+ "webskill/hello": "Hello",
426
+ "webskill/pair.start": "PairStart",
427
+ "webskill/pair.approved": "PairApproved",
428
+ "webskill/pair.rejected": "PairRejected",
429
+ "webskill/pair.confirm": "PairConfirm",
430
+ "webskill/auth.present": "AuthPresent",
431
+ "webskill/auth.challenge": "AuthChallenge",
432
+ "webskill/auth.renew": "AuthRenew",
433
+ "webskill/auth.revoked": "AuthRevoked",
434
+ "webskill/origins": "Origins",
435
+ "webskill/control.human": "HumanControl",
436
+ "webskill/ready": "Ready",
437
+ "webskill/ping": "Ping",
438
+ "webskill/bye": "Bye"
439
+ };
440
+ /** @experimental */
441
+ function validateControllerMessage(method, kind, value) {
442
+ const definition = METHOD_DEFINITIONS[method];
443
+ if (!definition) return {
444
+ valid: false,
445
+ errors: [`Unknown controller method: ${method}`]
446
+ };
447
+ const def = protocol_schema_default.$defs[`${definition}${kind === "params" ? "Params" : "Result"}`];
448
+ if (!def) return {
449
+ valid: false,
450
+ errors: [`Missing schema for ${method} ${kind}`]
451
+ };
452
+ const result = new Validator({
453
+ ...def,
454
+ $schema: "https://json-schema.org/draft/2020-12/schema",
455
+ $defs: protocol_schema_default.$defs
456
+ }, "2020-12", false).validate(value);
457
+ return {
458
+ valid: result.valid,
459
+ errors: result.errors.map((error) => `${error.instanceLocation || "/"}: ${error.error}`)
460
+ };
461
+ }
462
+
463
+ //#endregion
464
+ //#region ../controller/src/transport.ts
465
+ var SocketChannel = class {
466
+ options;
467
+ socket;
468
+ pending = /* @__PURE__ */ new Map();
469
+ sequence = 0;
470
+ opened = false;
471
+ closed = false;
472
+ connectPromise;
473
+ oversizeTimes = [];
474
+ maxMessageBytes;
475
+ defaultTimeoutMs;
476
+ mcpCallbacks = {};
477
+ constructor(options) {
478
+ this.options = options;
479
+ this.maxMessageBytes = options.maxMessageBytes ?? CONTROLLER_PROTOCOL_LIMITS.maxMessageBytes;
480
+ this.defaultTimeoutMs = options.requestTimeoutMs ?? CONTROLLER_PROTOCOL_LIMITS.heartbeatTimeoutMs;
481
+ }
482
+ async connect() {
483
+ if (this.opened && !this.closed) return;
484
+ if (this.connectPromise) return await this.connectPromise;
485
+ this.connectPromise = this.openSocket();
486
+ try {
487
+ await this.connectPromise;
488
+ } finally {
489
+ this.connectPromise = void 0;
490
+ }
491
+ }
492
+ async openSocket() {
493
+ const socket = this.options.socket ?? this.createSocket();
494
+ this.socket = socket;
495
+ if (this.options.socket) {
496
+ this.opened = true;
497
+ this.attach(socket);
498
+ return;
499
+ }
500
+ await new Promise((resolve, reject) => {
501
+ socket.onopen = () => {
502
+ this.opened = true;
503
+ resolve();
504
+ };
505
+ socket.onerror = (event) => {
506
+ const error = /* @__PURE__ */ new Error(`Controller WebSocket failed: ${String(event)}`);
507
+ this.options.onerror?.(error);
508
+ if (!this.opened) reject(error);
509
+ };
510
+ this.attach(socket);
511
+ });
512
+ }
513
+ createSocket() {
514
+ const Ctor = this.options.WebSocket ?? globalThis.WebSocket;
515
+ if (!Ctor) throw new Error("WebSocket is not available in this environment");
516
+ return new Ctor(this.options.url);
517
+ }
518
+ emitError(error) {
519
+ this.options.onerror?.(error);
520
+ this.mcpCallbacks.onerror?.(error);
521
+ }
522
+ attach(socket) {
523
+ socket.onclose = () => {
524
+ if (!this.opened) return;
525
+ this.handleClose();
526
+ };
527
+ socket.onmessage = (event) => {
528
+ this.handleMessage(event.data).catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
529
+ };
530
+ }
531
+ sendRaw(value) {
532
+ if (!this.socket || !this.opened || this.closed) throw new Error("Controller WebSocket is not connected");
533
+ const text = JSON.stringify(value);
534
+ if (byteLength(text) > this.maxMessageBytes) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Controller message exceeds the protocol size limit");
535
+ this.socket.send(text);
536
+ }
537
+ validate(method, kind, value) {
538
+ const result = validateControllerMessage(method, kind, value);
539
+ if (!result.valid) throw controllerError("CONTROLLER_MESSAGE_INVALID", `Invalid ${method} ${kind}: ${result.errors.join("; ")}`);
540
+ }
541
+ async request(method, params, timeoutMs) {
542
+ await this.connect();
543
+ this.validate(method, "params", params);
544
+ const id = `ws-${++this.sequence}`;
545
+ const promise = new Promise((resolve, reject) => {
546
+ const timeout = setTimeout(() => {
547
+ this.pending.delete(id);
548
+ reject(/* @__PURE__ */ new Error(`Controller request timed out: ${method}`));
549
+ }, timeoutMs ?? this.defaultTimeoutMs);
550
+ this.pending.set(id, {
551
+ method,
552
+ resolve,
553
+ reject,
554
+ timeout
555
+ });
556
+ });
557
+ try {
558
+ this.sendRaw({
559
+ jsonrpc: "2.0",
560
+ id,
561
+ method,
562
+ params
563
+ });
564
+ } catch (error) {
565
+ const pending = this.pending.get(id);
566
+ if (pending?.timeout) clearTimeout(pending.timeout);
567
+ this.pending.delete(id);
568
+ throw error;
569
+ }
570
+ return await promise;
571
+ }
572
+ async notify(method, params) {
573
+ await this.connect();
574
+ this.validate(method, "params", params);
575
+ this.sendRaw({
576
+ jsonrpc: "2.0",
577
+ method,
578
+ params
579
+ });
580
+ }
581
+ registerOversize(message) {
582
+ const now = Date.now();
583
+ const windowMs = CONTROLLER_PROTOCOL_LIMITS.oversizeWindowMs;
584
+ this.oversizeTimes = this.oversizeTimes.filter((time) => now - time <= windowMs);
585
+ this.oversizeTimes.push(now);
586
+ if (message?.id !== void 0) this.sendRaw({
587
+ jsonrpc: "2.0",
588
+ id: message.id,
589
+ error: toJsonRpcError(controllerError("CONTROLLER_MESSAGE_INVALID", "Controller message exceeds the protocol size limit"))
590
+ });
591
+ if (this.oversizeTimes.length >= CONTROLLER_PROTOCOL_LIMITS.oversizeThreshold) this.socket?.close(1009, "Controller message size limit exceeded repeatedly");
592
+ }
593
+ async handleMessage(raw) {
594
+ const text = typeof raw === "string" ? raw : String(raw);
595
+ if (byteLength(text) > this.maxMessageBytes) {
596
+ let parsed;
597
+ try {
598
+ const candidate = JSON.parse(text);
599
+ if (isJsonRpcObject(candidate)) parsed = candidate;
600
+ } catch {
601
+ parsed = void 0;
602
+ }
603
+ this.registerOversize(parsed);
604
+ return;
605
+ }
606
+ let message;
607
+ try {
608
+ message = JSON.parse(text);
609
+ } catch {
610
+ this.options.onerror?.(/* @__PURE__ */ new Error("Invalid JSON received on the controller channel"));
611
+ return;
612
+ }
613
+ if (!isJsonRpcObject(message)) {
614
+ this.options.onerror?.(/* @__PURE__ */ new Error("Invalid JSON-RPC object received on the controller channel"));
615
+ return;
616
+ }
617
+ if (message.id !== void 0 && message.method === void 0) {
618
+ const pending = this.pending.get(message.id);
619
+ if (!pending) {
620
+ this.options.onmessage?.(message);
621
+ return;
622
+ }
623
+ this.pending.delete(message.id);
624
+ if (pending.timeout) clearTimeout(pending.timeout);
625
+ if (message.error) {
626
+ const code = message.error.data?.code ?? "CONTROLLER_MESSAGE_INVALID";
627
+ pending.reject(new ControllerRpcError(code, message.error.message));
628
+ return;
629
+ }
630
+ try {
631
+ this.validate(pending.method, "result", message.result);
632
+ pending.resolve(message.result);
633
+ } catch (error) {
634
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
635
+ }
636
+ return;
637
+ }
638
+ if (typeof message.method !== "string") return;
639
+ if (!message.method.startsWith("webskill/")) {
640
+ if (this.options.isAuthorized?.() !== true) {
641
+ if (message.id !== void 0) this.sendRaw({
642
+ jsonrpc: "2.0",
643
+ id: message.id,
644
+ error: toJsonRpcError(controllerError("CONTROLLER_NOT_PAIRED", "The controller device is not paired"))
645
+ });
646
+ return;
647
+ }
648
+ this.options.onmessage?.(message);
649
+ return;
650
+ }
651
+ if (message.id === void 0) {
652
+ try {
653
+ this.validate(message.method, "params", message.params);
654
+ await this.options.onControlNotification?.(message.method, message.params);
655
+ } catch (error) {
656
+ this.options.onerror?.(error instanceof Error ? error : new Error(String(error)));
657
+ }
658
+ return;
659
+ }
660
+ try {
661
+ this.validate(message.method, "params", message.params);
662
+ const result = await this.options.onControlRequest?.(message.method, message.params);
663
+ this.validate(message.method, "result", result);
664
+ this.sendRaw({
665
+ jsonrpc: "2.0",
666
+ id: message.id,
667
+ result
668
+ });
669
+ } catch (error) {
670
+ this.sendRaw({
671
+ jsonrpc: "2.0",
672
+ id: message.id,
673
+ error: toJsonRpcError(error)
674
+ });
675
+ }
676
+ }
677
+ handleClose() {
678
+ if (this.closed) return;
679
+ this.closed = true;
680
+ this.opened = false;
681
+ const error = /* @__PURE__ */ new Error("Controller WebSocket closed");
682
+ for (const pending of this.pending.values()) {
683
+ if (pending.timeout) clearTimeout(pending.timeout);
684
+ pending.reject(error);
685
+ }
686
+ this.pending.clear();
687
+ this.options.onclose?.();
688
+ this.mcpCallbacks.onclose?.();
689
+ }
690
+ async close(code = 1e3, reason = "Controller channel closed") {
691
+ if (!this.socket || this.closed) return;
692
+ this.socket.close(code, reason);
693
+ this.handleClose();
694
+ }
695
+ };
696
+ /** @experimental */
697
+ function createWebSocketControllerTransport(options) {
698
+ const channel = new SocketChannel(options);
699
+ const transport = {
700
+ get connected() {
701
+ return channel.opened && !channel.closed;
702
+ },
703
+ onControlRequest: void 0,
704
+ onControlNotification: void 0,
705
+ onmessage: void 0,
706
+ onerror: void 0,
707
+ onclose: void 0,
708
+ connect: () => channel.connect(),
709
+ start: () => channel.connect(),
710
+ request: (method, params, timeoutMs) => channel.request(method, params, timeoutMs),
711
+ notify: (method, params) => channel.notify(method, params),
712
+ send: async (message) => channel.sendRaw(message),
713
+ close: (code, reason) => channel.close(code, reason)
714
+ };
715
+ for (const key of [
716
+ "onControlRequest",
717
+ "onControlNotification",
718
+ "onmessage",
719
+ "onerror",
720
+ "onclose"
721
+ ]) {
722
+ const isMcpLifecycle = key === "onerror" || key === "onclose";
723
+ Object.defineProperty(transport, key, {
724
+ get: () => isMcpLifecycle ? channel.mcpCallbacks[key] : options[key],
725
+ set: (value) => {
726
+ if (isMcpLifecycle) channel.mcpCallbacks[key] = value;
727
+ else options[key] = value;
728
+ },
729
+ enumerable: true,
730
+ configurable: true
731
+ });
732
+ }
733
+ return transport;
734
+ }
735
+
736
+ //#endregion
737
+ //#region ../controller/src/device/crypto.ts
738
+ function bytesToBase64Url(bytes) {
739
+ let binary = "";
740
+ const chunk = 32768;
741
+ for (let index = 0; index < bytes.length; index += chunk) binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
742
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
743
+ }
744
+ function base64UrlToBytes(value) {
745
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
746
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
747
+ const binary = atob(padded);
748
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
749
+ }
750
+ async function generateDeviceKeyPair() {
751
+ return await crypto.subtle.generateKey({
752
+ name: "ECDSA",
753
+ namedCurve: "P-256"
754
+ }, false, ["sign", "verify"]);
755
+ }
756
+ async function exportPublicKey(keyPair) {
757
+ return bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey)));
758
+ }
759
+ async function signDeviceText(keyPair, value) {
760
+ const signature = await crypto.subtle.sign({
761
+ name: "ECDSA",
762
+ hash: "SHA-256"
763
+ }, keyPair.privateKey, new TextEncoder().encode(value));
764
+ return bytesToBase64Url(new Uint8Array(signature));
765
+ }
766
+ async function fingerprintOfBytes(bytes, bytesToShow = 10) {
767
+ const buffer = new ArrayBuffer(bytes.byteLength);
768
+ new Uint8Array(buffer).set(bytes);
769
+ return [...new Uint8Array(await crypto.subtle.digest("SHA-256", buffer)).subarray(0, bytesToShow)].map((byte) => byte.toString(16).padStart(2, "0")).join("").toUpperCase().match(/.{1,4}/g)?.join("-") ?? "";
770
+ }
771
+ async function deviceFingerprint(keyPair) {
772
+ return await fingerprintOfBytes(base64UrlToBytes(await exportPublicKey(keyPair)), 10);
773
+ }
774
+ /** @experimental */
775
+ async function instanceFingerprint(instanceId) {
776
+ return await fingerprintOfBytes(new TextEncoder().encode(instanceId), 8);
777
+ }
778
+ function stableJson(value) {
779
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
780
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
781
+ const object = value;
782
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
783
+ }
784
+ async function argsDigest(args) {
785
+ return bytesToBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(stableJson(args)))));
786
+ }
787
+ function randomToken(bytes = 32) {
788
+ return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
789
+ }
790
+ function originFromControllerUrl(raw) {
791
+ const url = new URL(raw);
792
+ if (url.protocol === "wss:") return `https://${url.host}`;
793
+ if (url.protocol === "ws:") return `http://${url.host}`;
794
+ throw new WebSkillError("VALIDATION_FAILED", "Controller URL must use ws:// or wss://");
795
+ }
796
+ function isLoopbackHost(hostname) {
797
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
798
+ return host === "::1" || host === "localhost" || host.endsWith(".localhost") || /^127(?:\.[0-9]{1,3}){3}$/.test(host);
799
+ }
800
+ function assertControllerUrl(raw) {
801
+ let url;
802
+ try {
803
+ url = new URL(raw);
804
+ } catch {
805
+ throw new WebSkillError("VALIDATION_FAILED", "Controller URL must be an absolute WebSocket URL");
806
+ }
807
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new WebSkillError("VALIDATION_FAILED", "Controller URL must use ws:// or wss://");
808
+ if (url.protocol === "ws:" && !isLoopbackHost(url.hostname)) throw new WebSkillError("VALIDATION_FAILED", "Non-loopback controllers must use wss://");
809
+ return url;
810
+ }
811
+
812
+ //#endregion
813
+ //#region ../controller/src/device/tools.ts
814
+ function isWriteTool(name) {
815
+ return CONTROLLER_WRITE_TOOLS.includes(name);
816
+ }
817
+ function browserStatusTool() {
818
+ return {
819
+ name: "browser_status",
820
+ description: "Read the extension version, protocol version, connection state, human-control state and excluded origins.",
821
+ inputSchema: {
822
+ type: "object",
823
+ properties: {},
824
+ additionalProperties: false
825
+ }
826
+ };
827
+ }
828
+ async function listControllerTools(sources) {
829
+ const tools = [browserStatusTool()];
830
+ for (const source of sources) {
831
+ const specs = await source.listToolSpecs();
832
+ for (const spec of specs) {
833
+ if (!CONTROLLER_TOOL_NAMES.includes(spec.name)) continue;
834
+ tools.push({
835
+ name: spec.name,
836
+ ...spec.description === void 0 ? {} : { description: spec.description },
837
+ inputSchema: spec.inputSchema
838
+ });
839
+ }
840
+ }
841
+ return tools;
842
+ }
843
+ async function controllerInstructions(sources) {
844
+ const parts = [];
845
+ for (const source of sources) {
846
+ if (!(await source.listToolSpecs()).some((spec) => CONTROLLER_TOOL_NAMES.includes(spec.name))) continue;
847
+ const prompt = await source.systemPrompt?.();
848
+ if (prompt) parts.push(prompt);
849
+ }
850
+ return parts.length > 0 ? parts.join("\n\n") : void 0;
851
+ }
852
+ async function callControllerTool(sources, name, args, invocationId) {
853
+ const source = sources.find((candidate) => candidate.canHandle(name));
854
+ if (!source) throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Controller tool is unavailable: ${name}`);
855
+ return await source.call(name, args, {
856
+ runId: invocationId ?? `controller-${name}`,
857
+ ...invocationId === void 0 ? {} : { invocationId }
858
+ });
859
+ }
860
+ function toolResultError(error) {
861
+ const value = error;
862
+ const code = value?.code ?? "TOOL_EXECUTION_FAILED";
863
+ const message = value?.message ?? String(error);
864
+ const retryable = value?.data?.retryable === true || value?.details?.retryable === true || code in CONTROLLER_ERROR_RETRYABLE && CONTROLLER_ERROR_RETRYABLE[code];
865
+ return {
866
+ isError: true,
867
+ content: [{
868
+ type: "text",
869
+ text: message
870
+ }],
871
+ _meta: { "webskill/error": {
872
+ code,
873
+ message,
874
+ retryable
875
+ } }
876
+ };
877
+ }
878
+ function mapToolResult(result) {
879
+ if (!result.ok) return toolResultError(result.error ?? {
880
+ code: "TOOL_EXECUTION_FAILED",
881
+ message: "Controller tool failed"
882
+ });
883
+ const content = [];
884
+ let structuredContent;
885
+ for (const part of result.content) if (part.type === "text") content.push({
886
+ type: "text",
887
+ text: part.text ?? ""
888
+ });
889
+ else if (part.type === "json") {
890
+ const data = part.data ?? part.text;
891
+ content.push({
892
+ type: "text",
893
+ text: typeof data === "string" ? data : JSON.stringify(data)
894
+ });
895
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) structuredContent = data;
896
+ } else if (part.type === "image") content.push({
897
+ type: "image",
898
+ mimeType: part.mimeType,
899
+ data: part.data
900
+ });
901
+ else return toolResultError(new ControllerRpcError("CONTROLLER_CAPABILITY_MISSING", `Controller result type is unsupported: ${part.type}`));
902
+ return {
903
+ content,
904
+ ...structuredContent === void 0 ? {} : { structuredContent }
905
+ };
906
+ }
907
+
908
+ //#endregion
909
+ //#region ../controller/src/device/index.ts
910
+ const INVOCATION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
911
+ function errorCode(error) {
912
+ if (error instanceof WebSkillError) return error.code;
913
+ if (error instanceof ControllerRpcError) return error.code;
914
+ return "TOOL_EXECUTION_FAILED";
915
+ }
916
+ var DeviceSession = class {
917
+ options;
918
+ stateValue = "idle";
919
+ human = false;
920
+ transport;
921
+ pairing;
922
+ pendingPairing;
923
+ pendingApproval;
924
+ pairingCodeValue;
925
+ controllerInstance;
926
+ controllerName = "Controller";
927
+ excluded = /* @__PURE__ */ new Set();
928
+ keyPair;
929
+ connectPromise;
930
+ reconnectTimer;
931
+ heartbeatTimer;
932
+ renewTimer;
933
+ pairingTimer;
934
+ reconnectAttempt = 0;
935
+ intentionalClose = false;
936
+ mcpServer;
937
+ mcpStart;
938
+ writeQueue = Promise.resolve();
939
+ writeCache = /* @__PURE__ */ new Map();
940
+ restoredPairing = false;
941
+ writeState;
942
+ writeRecoverable = true;
943
+ lifecycleGeneration = 0;
944
+ lastToolNames = "";
945
+ constructor(options) {
946
+ this.options = options;
947
+ this.excluded.add(originFromControllerUrl(options.url));
948
+ for (const origin of options.excludedOrigins ?? []) this.excluded.add(origin);
949
+ }
950
+ get state() {
951
+ return this.stateValue;
952
+ }
953
+ get humanInControl() {
954
+ return this.human;
955
+ }
956
+ get writeStateRecoverable() {
957
+ return this.writeRecoverable;
958
+ }
959
+ get excludedOrigins() {
960
+ return [...this.excluded].sort();
961
+ }
962
+ get pairingCode() {
963
+ return this.pairingCodeValue;
964
+ }
965
+ get controllerInstanceId() {
966
+ return this.controllerInstance;
967
+ }
968
+ get controllerOrigin() {
969
+ return originFromControllerUrl(this.options.url);
970
+ }
971
+ now() {
972
+ return this.options.now?.() ?? Date.now();
973
+ }
974
+ setState(state) {
975
+ if (this.stateValue === state) return;
976
+ this.stateValue = state;
977
+ this.options.onStateChange?.(state);
978
+ }
979
+ reportError(error) {
980
+ const code = errorCode(error);
981
+ this.options.onError?.({
982
+ code,
983
+ message: error instanceof Error ? error.message : String(error)
984
+ });
985
+ }
986
+ audit(event) {
987
+ const audited = event.type === "controller.call" && typeof event.data["tool"] === "string" ? {
988
+ ...event,
989
+ data: {
990
+ ...event.data,
991
+ operation: isWriteTool(event.data["tool"]) ? "write" : "read"
992
+ }
993
+ } : event;
994
+ const pending = Promise.resolve(this.options.audit?.(audited));
995
+ pending.catch((error) => this.reportError(error));
996
+ return pending;
997
+ }
998
+ async connect() {
999
+ if (this.transport?.connected && [
1000
+ "ready",
1001
+ "unpaired",
1002
+ "pairing",
1003
+ "awaiting-confirmation"
1004
+ ].includes(this.stateValue)) return;
1005
+ if (this.connectPromise) return await this.connectPromise;
1006
+ this.intentionalClose = false;
1007
+ this.connectPromise = this.open();
1008
+ try {
1009
+ await this.connectPromise;
1010
+ } finally {
1011
+ this.connectPromise = void 0;
1012
+ }
1013
+ }
1014
+ async open() {
1015
+ assertControllerUrl(this.options.url);
1016
+ this.clearReconnect();
1017
+ this.setState("connecting");
1018
+ const transport = createWebSocketControllerTransport({
1019
+ url: this.options.url,
1020
+ ...this.options.WebSocket === void 0 ? {} : { WebSocket: this.options.WebSocket },
1021
+ isAuthorized: () => this.stateValue === "ready",
1022
+ onControlRequest: (method, params) => this.onControlRequest(method, params),
1023
+ onControlNotification: (method, params) => this.onControlNotification(method, params),
1024
+ onclose: () => this.onTransportClose(),
1025
+ onerror: (error) => this.reportError(error)
1026
+ });
1027
+ this.transport = transport;
1028
+ try {
1029
+ await transport.connect();
1030
+ this.setState("handshaking");
1031
+ const hello = await transport.request("webskill/hello", {
1032
+ protocolVersions: ["0.1"],
1033
+ hostName: this.options.deviceName,
1034
+ hostVersion: this.options.hostVersion,
1035
+ ...this.keyPair === void 0 ? {} : { deviceFingerprint: await deviceFingerprint(this.keyPair) }
1036
+ });
1037
+ if (hello.protocolVersion !== "0.1") throw controllerError("CONTROLLER_PROTOCOL_UNSUPPORTED", `Controller protocol ${hello.protocolVersion} is not supported`);
1038
+ this.controllerInstance = hello.controllerInstanceId;
1039
+ this.controllerName = hello.controllerName;
1040
+ this.mergeOrigins(hello.excludedOrigins);
1041
+ this.startHeartbeat();
1042
+ this.pairing = await this.options.store.loadPairing();
1043
+ if (this.pairing) this.mergeOrigins(this.pairing.declaredOrigins);
1044
+ this.options.onExcludedOriginsChange?.(this.excludedOrigins);
1045
+ if (!this.pairing) {
1046
+ this.setState("unpaired");
1047
+ return;
1048
+ }
1049
+ if (this.pairing.controllerInstanceId !== hello.controllerInstanceId) {
1050
+ const error = controllerError("CONTROLLER_NOT_PAIRED", "Controller instance changed; pair the device again");
1051
+ this.reportError(error);
1052
+ this.setState("failed");
1053
+ this.intentionalClose = true;
1054
+ await transport.close(1e3, "Controller instance changed");
1055
+ throw error;
1056
+ }
1057
+ this.restoredPairing = true;
1058
+ const writeState = await this.options.store.loadWriteState?.();
1059
+ this.writeState = writeState?.epoch === this.pairing.sessionId && Number.isSafeInteger(writeState.highWatermark) && writeState.highWatermark >= 0 ? writeState : void 0;
1060
+ this.writeRecoverable = this.writeState !== void 0;
1061
+ await this.authorizeExistingPairing();
1062
+ } catch (error) {
1063
+ this.reportError(error);
1064
+ this.stopHeartbeat();
1065
+ if (!this.intentionalClose && this.pairing) {
1066
+ this.setState("offline");
1067
+ this.scheduleReconnect();
1068
+ } else this.setState("failed");
1069
+ throw error;
1070
+ }
1071
+ }
1072
+ async authorizeExistingPairing() {
1073
+ const record = this.pairing;
1074
+ if (!record) return;
1075
+ this.setState("authorizing");
1076
+ try {
1077
+ const result = await this.transport.request("webskill/auth.present", { grant: record.grant });
1078
+ this.pairing = {
1079
+ ...record,
1080
+ grantExpiresAt: result.expiresAt
1081
+ };
1082
+ await this.options.store.savePairing(this.pairing);
1083
+ await this.becomeReady();
1084
+ } catch (error) {
1085
+ if (error instanceof ControllerRpcError && error.code === "CONTROLLER_NOT_PAIRED") try {
1086
+ await this.renewGrant();
1087
+ await this.becomeReady();
1088
+ return;
1089
+ } catch {
1090
+ await this.options.store.savePairing(void 0);
1091
+ this.pairing = void 0;
1092
+ this.setState("unpaired");
1093
+ return;
1094
+ }
1095
+ throw error;
1096
+ }
1097
+ }
1098
+ async renewGrant() {
1099
+ const record = this.pairing;
1100
+ if (!record) throw controllerError("CONTROLLER_NOT_PAIRED", "No paired device grant is available");
1101
+ const keyPair = this.keyPair ?? await this.options.store.loadKeyPair();
1102
+ if (!keyPair) throw controllerError("CONTROLLER_NOT_PAIRED", "The device key pair is unavailable");
1103
+ this.keyPair = keyPair;
1104
+ const challenge = await this.transport.request("webskill/auth.challenge", {});
1105
+ if (Date.parse(challenge.expiresAt) <= this.now()) throw controllerError("CONTROLLER_NOT_PAIRED", "The grant renewal challenge has expired");
1106
+ const signature = await signDeviceText(keyPair, [
1107
+ "webskill-controller/0.1",
1108
+ "auth.renew",
1109
+ record.controllerInstanceId,
1110
+ challenge.nonce
1111
+ ].join("\n"));
1112
+ const renewed = await this.transport.request("webskill/auth.renew", {
1113
+ nonce: challenge.nonce,
1114
+ signature
1115
+ });
1116
+ this.pairing = {
1117
+ ...record,
1118
+ grant: renewed.grant,
1119
+ grantExpiresAt: renewed.expiresAt
1120
+ };
1121
+ await this.options.store.savePairing(this.pairing);
1122
+ }
1123
+ async startWriteEpoch(sessionId) {
1124
+ this.restoredPairing = false;
1125
+ this.writeRecoverable = true;
1126
+ this.writeCache.clear();
1127
+ this.writeState = {
1128
+ epoch: sessionId,
1129
+ highWatermark: 0
1130
+ };
1131
+ await this.options.store.saveWriteState?.(this.writeState);
1132
+ }
1133
+ async persistWriteState() {
1134
+ if (this.writeState) await this.options.store.saveWriteState?.(this.writeState);
1135
+ }
1136
+ async becomeReady() {
1137
+ this.reconnectAttempt = 0;
1138
+ await this.startMcp();
1139
+ const epoch = this.pairing?.sessionId ?? this.writeState?.epoch;
1140
+ if (!epoch) throw controllerError("CONTROLLER_NOT_PAIRED", "The device has no pairing epoch for the ready notification");
1141
+ this.setState("ready");
1142
+ await this.transport?.notify("webskill/ready", {
1143
+ deviceEpoch: epoch,
1144
+ writeHighWatermark: this.writeState?.highWatermark ?? 0,
1145
+ hostVersion: this.options.hostVersion,
1146
+ writeStateRecoverable: this.writeRecoverable
1147
+ });
1148
+ this.audit({
1149
+ type: "controller.connection",
1150
+ data: {
1151
+ state: "ready",
1152
+ controllerOrigin: this.controllerOrigin,
1153
+ controllerInstance: this.controllerInstance ?? ""
1154
+ }
1155
+ });
1156
+ this.scheduleRenewal();
1157
+ }
1158
+ async startPairing() {
1159
+ if (this.stateValue === "ready") throw controllerError("CONTROLLER_PAIRING_REJECTED", "The device is already paired");
1160
+ if (!this.transport?.connected || [
1161
+ "idle",
1162
+ "offline",
1163
+ "failed"
1164
+ ].includes(this.stateValue)) await this.connect();
1165
+ if (!this.transport?.connected || this.stateValue !== "unpaired") throw controllerError("CONTROLLER_PAIRING_REJECTED", "The controller is not ready for pairing");
1166
+ this.keyPair = await this.options.store.loadKeyPair() ?? await generateDeviceKeyPair();
1167
+ await this.options.store.saveKeyPair(this.keyPair);
1168
+ const publicKey = await exportPublicKey(this.keyPair);
1169
+ const fingerprint = await deviceFingerprint(this.keyPair);
1170
+ const started = await this.transport.request("webskill/pair.start", {
1171
+ publicKey,
1172
+ deviceName: this.options.deviceName
1173
+ });
1174
+ this.pendingPairing = {
1175
+ ...started,
1176
+ publicKey,
1177
+ deviceFingerprint: fingerprint
1178
+ };
1179
+ this.pendingApproval = void 0;
1180
+ this.pairingCodeValue = {
1181
+ ...started,
1182
+ deviceFingerprint: fingerprint
1183
+ };
1184
+ this.setState("pairing");
1185
+ this.audit({
1186
+ type: "controller.pairing",
1187
+ data: {
1188
+ phase: "started",
1189
+ sessionId: started.sessionId
1190
+ }
1191
+ });
1192
+ const ttl = Math.max(1, Date.parse(started.expiresAt) - this.now());
1193
+ this.pairingTimer = setTimeout(() => {
1194
+ if (this.stateValue === "pairing" || this.stateValue === "awaiting-confirmation") {
1195
+ this.pendingPairing = void 0;
1196
+ this.pendingApproval = void 0;
1197
+ this.pairingCodeValue = void 0;
1198
+ this.setState("unpaired");
1199
+ this.reportError(controllerError("CONTROLLER_PAIRING_REJECTED", "The pairing session has expired"));
1200
+ }
1201
+ }, ttl);
1202
+ return this.pairingCodeValue;
1203
+ }
1204
+ onPairApproved(approval) {
1205
+ const pending = this.pendingPairing;
1206
+ if (!pending || pending.sessionId !== approval.sessionId || this.stateValue !== "pairing") return;
1207
+ if (approval.controllerInstanceId !== this.controllerInstance) {
1208
+ this.reportError(controllerError("CONTROLLER_NOT_PAIRED", "The approval came from a different controller instance"));
1209
+ return;
1210
+ }
1211
+ this.pendingApproval = approval;
1212
+ this.mergeOrigins(approval.excludedOrigins);
1213
+ this.setState("awaiting-confirmation");
1214
+ this.confirmApprovedPairing(approval).catch((error) => this.reportError(error));
1215
+ }
1216
+ async confirmApprovedPairing(approval) {
1217
+ let accepted = false;
1218
+ let timer;
1219
+ try {
1220
+ accepted = await Promise.race([this.options.confirmPairing({
1221
+ ...approval,
1222
+ controllerName: this.controllerName
1223
+ }), new Promise((_, reject) => {
1224
+ timer = setTimeout(() => reject(controllerError("CONTROLLER_PAIRING_REJECTED", "Device-side pairing confirmation timed out")), CONTROLLER_PROTOCOL_LIMITS.deviceConfirmTimeoutMs);
1225
+ })]);
1226
+ } catch (error) {
1227
+ this.reportError(error);
1228
+ } finally {
1229
+ if (timer) clearTimeout(timer);
1230
+ }
1231
+ if (!this.pendingPairing || this.pendingPairing.sessionId !== approval.sessionId) return;
1232
+ let signature;
1233
+ if (accepted) signature = await signDeviceText(this.keyPair, [
1234
+ "webskill-controller/0.1",
1235
+ "pair.confirm",
1236
+ approval.sessionId,
1237
+ approval.approvalNonce
1238
+ ].join("\n"));
1239
+ const result = await this.transport.request("webskill/pair.confirm", {
1240
+ sessionId: approval.sessionId,
1241
+ accepted,
1242
+ ...signature === void 0 ? {} : { signature }
1243
+ });
1244
+ if (!accepted) {
1245
+ this.pendingPairing = void 0;
1246
+ this.pendingApproval = void 0;
1247
+ this.pairingCodeValue = void 0;
1248
+ this.setState("unpaired");
1249
+ return;
1250
+ }
1251
+ if (!result.grant || !result.expiresAt) throw controllerError("CONTROLLER_PAIRING_REJECTED", "The controller did not return a device grant");
1252
+ const record = {
1253
+ controllerUrl: this.options.url,
1254
+ controllerInstanceId: approval.controllerInstanceId,
1255
+ controllerName: this.controllerName,
1256
+ principalDisplayName: approval.principal.displayName,
1257
+ sessionId: approval.sessionId,
1258
+ grant: result.grant,
1259
+ grantExpiresAt: result.expiresAt,
1260
+ declaredOrigins: this.excludedOrigins,
1261
+ pairedAt: new Date(this.now()).toISOString()
1262
+ };
1263
+ this.pairing = record;
1264
+ await this.options.store.savePairing(record);
1265
+ await this.startWriteEpoch(record.sessionId);
1266
+ this.pendingPairing = void 0;
1267
+ this.pendingApproval = void 0;
1268
+ this.pairingCodeValue = void 0;
1269
+ this.clearPairingTimer();
1270
+ this.audit({
1271
+ type: "controller.pairing",
1272
+ data: {
1273
+ phase: "confirmed",
1274
+ sessionId: approval.sessionId,
1275
+ principal: approval.principal.displayName
1276
+ }
1277
+ });
1278
+ await this.becomeReady();
1279
+ }
1280
+ onPairRejected(rejected) {
1281
+ if (this.pendingPairing?.sessionId !== rejected.sessionId) return;
1282
+ this.pendingPairing = void 0;
1283
+ this.pendingApproval = void 0;
1284
+ this.pairingCodeValue = void 0;
1285
+ this.clearPairingTimer();
1286
+ this.setState("unpaired");
1287
+ this.reportError(controllerError("CONTROLLER_PAIRING_REJECTED", rejected.code));
1288
+ this.audit({
1289
+ type: "controller.pairing",
1290
+ data: {
1291
+ phase: "rejected",
1292
+ sessionId: rejected.sessionId
1293
+ }
1294
+ });
1295
+ }
1296
+ mergeOrigins(origins) {
1297
+ const previous = this.excludedOrigins;
1298
+ for (const origin of origins) this.excluded.add(origin);
1299
+ const next = this.excludedOrigins;
1300
+ if (next.length !== previous.length) {
1301
+ this.options.onExcludedOriginsChange?.(next);
1302
+ this.audit({
1303
+ type: "controller.origins",
1304
+ data: {
1305
+ effective: next,
1306
+ removedFromDeclaration: previous.filter((origin) => !origins.includes(origin))
1307
+ }
1308
+ });
1309
+ if (this.pairing) {
1310
+ this.pairing = {
1311
+ ...this.pairing,
1312
+ declaredOrigins: next
1313
+ };
1314
+ this.options.store.savePairing(this.pairing).catch((error) => this.reportError(error));
1315
+ }
1316
+ }
1317
+ }
1318
+ onControlRequest(method, _params) {
1319
+ if (method === "webskill/ping") return {};
1320
+ if (method === "webskill/bye") {
1321
+ this.transport?.close();
1322
+ return {};
1323
+ }
1324
+ throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Unsupported controller request: ${method}`);
1325
+ }
1326
+ onControlNotification(method, params) {
1327
+ switch (method) {
1328
+ case "webskill/pair.approved":
1329
+ this.onPairApproved(params);
1330
+ return;
1331
+ case "webskill/pair.rejected":
1332
+ this.onPairRejected(params);
1333
+ return;
1334
+ case "webskill/origins":
1335
+ this.mergeOrigins(params.excludedOrigins);
1336
+ return;
1337
+ case "webskill/auth.revoked":
1338
+ this.onRevoked(params.reason);
1339
+ return;
1340
+ case "webskill/bye":
1341
+ this.transport?.close();
1342
+ return;
1343
+ default: this.reportError(controllerError("CONTROLLER_CAPABILITY_MISSING", `Unsupported controller notification: ${method}`));
1344
+ }
1345
+ }
1346
+ async onRevoked(reason) {
1347
+ this.lifecycleGeneration += 1;
1348
+ await this.options.store.savePairing(void 0);
1349
+ this.pairing = void 0;
1350
+ this.writeState = void 0;
1351
+ this.restoredPairing = false;
1352
+ this.writeRecoverable = true;
1353
+ this.writeCache.clear();
1354
+ await this.options.store.saveWriteState?.(void 0);
1355
+ await this.stopMcp();
1356
+ this.setState("unpaired");
1357
+ this.audit({
1358
+ type: "controller.pairing",
1359
+ data: {
1360
+ phase: "unpaired",
1361
+ reason
1362
+ }
1363
+ });
1364
+ this.reportError(controllerError("CONTROLLER_NOT_PAIRED", `The device grant was revoked: ${reason}`));
1365
+ }
1366
+ startHeartbeat() {
1367
+ this.stopHeartbeat();
1368
+ this.heartbeatTimer = setInterval(() => {
1369
+ if (!this.transport?.connected) return;
1370
+ this.transport.request("webskill/ping", {}, CONTROLLER_PROTOCOL_LIMITS.heartbeatTimeoutMs).catch((error) => {
1371
+ this.reportError(error);
1372
+ this.transport?.close(1001, "Controller heartbeat timed out");
1373
+ });
1374
+ }, CONTROLLER_PROTOCOL_LIMITS.heartbeatIntervalMs);
1375
+ }
1376
+ stopHeartbeat() {
1377
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
1378
+ this.heartbeatTimer = void 0;
1379
+ }
1380
+ scheduleRenewal() {
1381
+ if (this.renewTimer) clearTimeout(this.renewTimer);
1382
+ const record = this.pairing;
1383
+ if (!record) return;
1384
+ const remaining = Math.max(1, Date.parse(record.grantExpiresAt) - this.now());
1385
+ this.renewTimer = setTimeout(() => {
1386
+ this.renewGrant().then(() => this.scheduleRenewal()).catch((error) => {
1387
+ this.reportError(error);
1388
+ this.onRevoked("grant-renewal-failed");
1389
+ });
1390
+ }, Math.max(1, Math.floor(remaining * .8)));
1391
+ }
1392
+ onTransportClose() {
1393
+ this.lifecycleGeneration += 1;
1394
+ this.stopHeartbeat();
1395
+ if (this.renewTimer) clearTimeout(this.renewTimer);
1396
+ this.stopMcp();
1397
+ if (this.pendingPairing && (this.stateValue === "pairing" || this.stateValue === "awaiting-confirmation")) {
1398
+ this.pendingPairing = void 0;
1399
+ this.pendingApproval = void 0;
1400
+ this.pairingCodeValue = void 0;
1401
+ this.clearPairingTimer();
1402
+ this.setState("unpaired");
1403
+ }
1404
+ if (!this.intentionalClose && this.pairing) {
1405
+ this.setState("offline");
1406
+ this.scheduleReconnect();
1407
+ }
1408
+ }
1409
+ clearReconnect() {
1410
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
1411
+ this.reconnectTimer = void 0;
1412
+ }
1413
+ scheduleReconnect() {
1414
+ if (this.reconnectTimer || this.intentionalClose || !this.pairing) return;
1415
+ const base = Math.min(1e3 * 2 ** this.reconnectAttempt, CONTROLLER_PROTOCOL_LIMITS.reconnectMaxDelayMs);
1416
+ this.reconnectAttempt += 1;
1417
+ const jitter = base * (Math.random() * .4 - .2);
1418
+ this.reconnectTimer = setTimeout(() => {
1419
+ this.reconnectTimer = void 0;
1420
+ this.connect().catch(() => void 0);
1421
+ }, Math.max(100, Math.floor(base + jitter)));
1422
+ }
1423
+ async disconnect(reason = "User disconnected") {
1424
+ this.lifecycleGeneration += 1;
1425
+ this.intentionalClose = true;
1426
+ this.clearReconnect();
1427
+ this.stopHeartbeat();
1428
+ this.clearPairingTimer();
1429
+ if (this.renewTimer) clearTimeout(this.renewTimer);
1430
+ const transport = this.transport;
1431
+ this.transport = void 0;
1432
+ if (transport?.connected) {
1433
+ await transport.notify("webskill/bye", {
1434
+ code: "disconnect",
1435
+ message: reason
1436
+ }).catch(() => void 0);
1437
+ await transport.close(1e3, reason);
1438
+ }
1439
+ await this.stopMcp();
1440
+ this.setState("idle");
1441
+ }
1442
+ async unpair() {
1443
+ const sessionId = this.pairing?.sessionId ?? this.pendingPairing?.sessionId;
1444
+ this.audit({
1445
+ type: "controller.pairing",
1446
+ data: {
1447
+ phase: "unpaired",
1448
+ ...sessionId === void 0 ? {} : { sessionId }
1449
+ }
1450
+ });
1451
+ await this.disconnect("Device unpaired");
1452
+ await this.options.store.clear();
1453
+ this.pairing = void 0;
1454
+ this.keyPair = void 0;
1455
+ this.writeState = void 0;
1456
+ this.restoredPairing = false;
1457
+ this.writeRecoverable = true;
1458
+ this.writeCache.clear();
1459
+ this.pendingPairing = void 0;
1460
+ this.pendingApproval = void 0;
1461
+ this.pairingCodeValue = void 0;
1462
+ this.excluded = /* @__PURE__ */ new Set([this.controllerOrigin]);
1463
+ this.setState("idle");
1464
+ }
1465
+ setHumanInControl(inControl) {
1466
+ if (this.human === inControl) return;
1467
+ this.lifecycleGeneration += 1;
1468
+ this.human = inControl;
1469
+ this.audit({
1470
+ type: "controller.human-control",
1471
+ data: { inControl }
1472
+ });
1473
+ if (this.transport?.connected) this.transport.notify("webskill/control.human", {
1474
+ inControl,
1475
+ at: new Date(this.now()).toISOString()
1476
+ }).catch((error) => this.reportError(error));
1477
+ }
1478
+ clearPairingTimer() {
1479
+ if (this.pairingTimer) clearTimeout(this.pairingTimer);
1480
+ this.pairingTimer = void 0;
1481
+ }
1482
+ async startMcp() {
1483
+ if (this.mcpServer || this.mcpStart) return await this.mcpStart;
1484
+ this.mcpStart = this.startMcpServer();
1485
+ try {
1486
+ await this.mcpStart;
1487
+ } finally {
1488
+ this.mcpStart = void 0;
1489
+ }
1490
+ }
1491
+ async startMcpServer() {
1492
+ const transport = this.transport;
1493
+ if (!transport) throw controllerError("CONTROLLER_NOT_PAIRED", "The device transport is unavailable");
1494
+ let ServerCtor;
1495
+ let ValidatorCtor;
1496
+ let ListToolsSchema;
1497
+ let CallToolSchema;
1498
+ let McpErrorCtor;
1499
+ try {
1500
+ const [serverModule, validatorModule, typesModule] = await Promise.all([
1501
+ import("@modelcontextprotocol/sdk/server/index.js"),
1502
+ import("@modelcontextprotocol/sdk/validation/cfworker"),
1503
+ import("@modelcontextprotocol/sdk/types.js")
1504
+ ]);
1505
+ ServerCtor = serverModule.Server;
1506
+ ValidatorCtor = validatorModule.CfWorkerJsonSchemaValidator;
1507
+ ListToolsSchema = typesModule.ListToolsRequestSchema;
1508
+ CallToolSchema = typesModule.CallToolRequestSchema;
1509
+ McpErrorCtor = typesModule.McpError;
1510
+ } catch (error) {
1511
+ throw new WebSkillError("MCP_ENDPOINT_UNAVAILABLE", "The MCP SDK is required to start the controller device", error);
1512
+ }
1513
+ const instructions = await controllerInstructions(this.options.tools);
1514
+ const server = new ServerCtor({
1515
+ name: "webskill-extension",
1516
+ version: this.options.hostVersion
1517
+ }, {
1518
+ capabilities: { tools: { listChanged: true } },
1519
+ ...instructions === void 0 ? {} : { instructions },
1520
+ jsonSchemaValidator: new ValidatorCtor()
1521
+ });
1522
+ server.setRequestHandler(ListToolsSchema, async () => ({ tools: await this.listTools() }));
1523
+ server.setRequestHandler(CallToolSchema, async (request) => {
1524
+ try {
1525
+ return await this.onToolCall(request.params);
1526
+ } catch (error) {
1527
+ if (error instanceof ControllerRpcError) {
1528
+ const rpc = toJsonRpcError(error);
1529
+ throw new McpErrorCtor(rpc.code, rpc.message, rpc.data);
1530
+ }
1531
+ throw error;
1532
+ }
1533
+ });
1534
+ await server.connect(transport);
1535
+ if (transport !== this.transport || !transport.connected) {
1536
+ await server.close();
1537
+ throw controllerError("CONTROLLER_MESSAGE_INVALID", "Controller transport changed while starting MCP");
1538
+ }
1539
+ this.mcpServer = server;
1540
+ }
1541
+ async stopMcp() {
1542
+ const server = this.mcpServer;
1543
+ this.mcpServer = void 0;
1544
+ if (server) await server.close().catch(() => void 0);
1545
+ }
1546
+ async listTools() {
1547
+ const tools = await listControllerTools(this.options.tools);
1548
+ this.lastToolNames = tools.map((tool) => tool.name).sort().join("\n");
1549
+ return tools;
1550
+ }
1551
+ readInvocation(params) {
1552
+ const input = params;
1553
+ if (input._meta !== void 0 && (typeof input._meta !== "object" || input._meta === null || Array.isArray(input._meta))) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Tool _meta must be a JSON object");
1554
+ const meta = input._meta ?? {};
1555
+ const id = meta["webskill/invocationId"];
1556
+ if (typeof id !== "string" || !INVOCATION_ID.test(id)) throw controllerError("CONTROLLER_MESSAGE_INVALID", "A valid webskill/invocationId is required for every tool call");
1557
+ const epoch = meta["webskill/deviceEpoch"];
1558
+ if (epoch !== void 0 && (typeof epoch !== "string" || !INVOCATION_ID.test(epoch))) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/deviceEpoch must match the pairing session ID");
1559
+ const sequence = meta["webskill/sequence"];
1560
+ if (sequence !== void 0 && (!Number.isSafeInteger(sequence) || sequence < 1)) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/sequence must be a positive integer");
1561
+ const context = meta["webskill/context"];
1562
+ if (context !== void 0 && (typeof context !== "object" || context === null || Array.isArray(context))) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/context must be a JSON object");
1563
+ if (context !== void 0 && byteLength(JSON.stringify(context)) > CONTROLLER_PROTOCOL_LIMITS.invocationContextMaxBytes) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/context exceeds the protocol size limit");
1564
+ const args = input.arguments ?? {};
1565
+ if (typeof args !== "object" || args === null || Array.isArray(args)) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Tool arguments must be a JSON object");
1566
+ return {
1567
+ id,
1568
+ ...epoch === void 0 ? {} : { epoch },
1569
+ ...sequence === void 0 ? {} : { sequence },
1570
+ ...context === void 0 ? {} : { context },
1571
+ args
1572
+ };
1573
+ }
1574
+ async onToolCall(params) {
1575
+ try {
1576
+ const { id, epoch, sequence, context, args } = this.readInvocation(params);
1577
+ const toolName = params.name;
1578
+ if (typeof toolName !== "string" || !toolName) throw controllerError("CONTROLLER_CAPABILITY_MISSING", "A tool name is required");
1579
+ if (isWriteTool(toolName)) {
1580
+ const result = await this.executeWrite(id, epoch, sequence, toolName, args, context, this.human);
1581
+ try {
1582
+ await this.refreshTools();
1583
+ } catch (error) {
1584
+ this.reportError(error);
1585
+ }
1586
+ return result;
1587
+ }
1588
+ if (this.human && toolName !== "browser_status") return toolResultError(controllerError("CONTROLLER_HUMAN_IN_CONTROL", "The user is in control of the browser"));
1589
+ if (!(await listControllerTools(this.options.tools)).some((tool) => tool.name === toolName)) {
1590
+ await this.audit({
1591
+ type: "controller.call",
1592
+ data: {
1593
+ invocationId: id,
1594
+ tool: toolName,
1595
+ phase: "rejected",
1596
+ code: "CONTROLLER_CAPABILITY_MISSING"
1597
+ }
1598
+ });
1599
+ throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Controller tool is unavailable: ${toolName}`);
1600
+ }
1601
+ await this.audit({
1602
+ type: "controller.call",
1603
+ data: {
1604
+ invocationId: id,
1605
+ tool: toolName,
1606
+ phase: "started",
1607
+ ...context === void 0 ? {} : { context }
1608
+ }
1609
+ });
1610
+ const result = this.fitResult(await this.executeRead(toolName, args));
1611
+ await this.auditCall(id, toolName, result, context);
1612
+ await this.refreshTools();
1613
+ return result;
1614
+ } catch (error) {
1615
+ if (error instanceof ControllerRpcError && ["CONTROLLER_MESSAGE_INVALID", "CONTROLLER_CAPABILITY_MISSING"].includes(error.code)) throw error;
1616
+ this.reportError(error);
1617
+ return toolResultError(error);
1618
+ }
1619
+ }
1620
+ fitResult(result) {
1621
+ if (byteLength(JSON.stringify(result)) <= CONTROLLER_PROTOCOL_LIMITS.maxMessageBytes) return result;
1622
+ return toolResultError(controllerError("CONTROLLER_MESSAGE_INVALID", "Controller tool result exceeds the protocol size limit"));
1623
+ }
1624
+ async executeRead(name, args, invocationId) {
1625
+ if (name === "browser_status") return mapToolResult({
1626
+ ok: true,
1627
+ content: [{
1628
+ type: "json",
1629
+ data: {
1630
+ hostVersion: this.options.hostVersion,
1631
+ protocolVersion: "0.1",
1632
+ humanInControl: this.human,
1633
+ connectionState: this.stateValue,
1634
+ excludedOrigins: this.excludedOrigins,
1635
+ deviceEpoch: this.writeState?.epoch ?? this.pairing?.sessionId ?? "",
1636
+ writeHighWatermark: this.writeState?.highWatermark ?? 0,
1637
+ writeStateRecoverable: this.writeRecoverable
1638
+ }
1639
+ }]
1640
+ });
1641
+ const result = await callControllerTool(this.options.tools, name, args, invocationId);
1642
+ const mapped = mapToolResult(result);
1643
+ if (result.authorization) return {
1644
+ ...mapped,
1645
+ _meta: {
1646
+ ...mapped._meta,
1647
+ "webskill/authorization": result.authorization
1648
+ }
1649
+ };
1650
+ return mapped;
1651
+ }
1652
+ async executeWrite(id, epoch, sequence, name, args, context, rejectNewInvocation = false) {
1653
+ this.pruneWriteCache();
1654
+ if (epoch === void 0 || sequence === void 0) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Write calls require webskill/deviceEpoch and webskill/sequence");
1655
+ const state = this.writeState;
1656
+ const duplicate = (message) => toolResultError(controllerError("CONTROLLER_INVOCATION_DUPLICATE", message));
1657
+ if (this.restoredPairing && state === void 0 || state?.epoch !== epoch) return duplicate("The invocation epoch or recoverable write state is not valid for this pairing");
1658
+ const existing = this.writeCache.get(id);
1659
+ if (existing) {
1660
+ if (existing.epoch !== epoch || existing.sequence !== sequence || existing.tool !== name) return toolResultError(controllerError("CONTROLLER_MESSAGE_INVALID", "The invocation ID was reused with different protocol metadata"));
1661
+ const [digest, originalDigest] = await Promise.all([argsDigest(args), existing.argsDigest]);
1662
+ if (digest !== originalDigest) return toolResultError(controllerError("CONTROLLER_MESSAGE_INVALID", "The invocation ID was reused with different tool arguments"));
1663
+ return await existing.promise;
1664
+ }
1665
+ if (rejectNewInvocation) return toolResultError(controllerError("CONTROLLER_HUMAN_IN_CONTROL", "The user is in control of the browser"));
1666
+ if (sequence <= state.highWatermark) return duplicate("The invocation sequence is outside the recoverable result window; do not retry it without user confirmation");
1667
+ if (sequence > state.highWatermark + CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheEntries) throw controllerError("CONTROLLER_MESSAGE_INVALID", "The invocation sequence exceeds the acceptable forward window");
1668
+ this.writeState = {
1669
+ epoch,
1670
+ highWatermark: sequence
1671
+ };
1672
+ const generation = this.lifecycleGeneration;
1673
+ const execute = async () => {
1674
+ try {
1675
+ await this.persistWriteState();
1676
+ if (generation !== this.lifecycleGeneration || this.human || this.stateValue !== "ready") return toolResultError(controllerError(this.human ? "CONTROLLER_HUMAN_IN_CONTROL" : "CONTROLLER_NOT_PAIRED", "The queued write was cancelled before it could execute"));
1677
+ await this.audit({
1678
+ type: "controller.call",
1679
+ data: {
1680
+ invocationId: id,
1681
+ tool: name,
1682
+ phase: "started",
1683
+ ...context === void 0 ? {} : { context }
1684
+ }
1685
+ });
1686
+ if (!(await listControllerTools(this.options.tools)).some((tool) => tool.name === name)) {
1687
+ await this.audit({
1688
+ type: "controller.call",
1689
+ data: {
1690
+ invocationId: id,
1691
+ tool: name,
1692
+ phase: "rejected",
1693
+ code: "CONTROLLER_CAPABILITY_MISSING"
1694
+ }
1695
+ });
1696
+ throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Controller tool is unavailable: ${name}`);
1697
+ }
1698
+ if (generation !== this.lifecycleGeneration || this.human || this.stateValue !== "ready") return toolResultError(controllerError(this.human ? "CONTROLLER_HUMAN_IN_CONTROL" : "CONTROLLER_NOT_PAIRED", "The queued write was cancelled before it could execute"));
1699
+ const result = this.fitResult(await this.executeRead(name, args, id));
1700
+ try {
1701
+ await this.auditCall(id, name, result, context);
1702
+ } catch (error) {
1703
+ this.reportError(error);
1704
+ return {
1705
+ ...result,
1706
+ _meta: {
1707
+ ...result._meta,
1708
+ "webskill/auditIncomplete": true
1709
+ }
1710
+ };
1711
+ }
1712
+ return result;
1713
+ } catch (error) {
1714
+ if (error instanceof ControllerRpcError && ["CONTROLLER_MESSAGE_INVALID", "CONTROLLER_CAPABILITY_MISSING"].includes(error.code)) throw error;
1715
+ this.reportError(error);
1716
+ return toolResultError(error);
1717
+ }
1718
+ };
1719
+ const queued = this.writeQueue.then(execute, execute);
1720
+ const entry = {
1721
+ epoch,
1722
+ sequence,
1723
+ tool: name,
1724
+ argsDigest: argsDigest(args),
1725
+ promise: queued.then((result) => {
1726
+ entry.result = result;
1727
+ entry.completedAt = this.now();
1728
+ this.pruneWriteCache();
1729
+ return result;
1730
+ }, (error) => {
1731
+ entry.completedAt = this.now();
1732
+ this.pruneWriteCache();
1733
+ throw error;
1734
+ })
1735
+ };
1736
+ this.writeCache.set(id, entry);
1737
+ this.writeQueue = queued.then(() => void 0, () => void 0);
1738
+ return await entry.promise;
1739
+ }
1740
+ pruneWriteCache() {
1741
+ const now = this.now();
1742
+ for (const [key, entry] of this.writeCache) if (entry.completedAt !== void 0 && now - entry.completedAt > CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheTtlMs) this.writeCache.delete(key);
1743
+ if (this.writeCache.size <= CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheEntries) return;
1744
+ for (const [key, entry] of this.writeCache) {
1745
+ if (entry.completedAt === void 0) continue;
1746
+ this.writeCache.delete(key);
1747
+ if (this.writeCache.size <= CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheEntries) return;
1748
+ }
1749
+ }
1750
+ async auditCall(invocationId, tool, result, context) {
1751
+ const failure = result._meta?.["webskill/error"];
1752
+ await this.audit({
1753
+ type: "controller.call",
1754
+ data: {
1755
+ invocationId,
1756
+ tool,
1757
+ ok: result.isError !== true,
1758
+ ...failure?.code === void 0 ? {} : { code: failure.code },
1759
+ ...result._meta?.["webskill/authorization"] === void 0 ? {} : { authorization: result._meta["webskill/authorization"] },
1760
+ ...context === void 0 ? {} : { context }
1761
+ }
1762
+ });
1763
+ }
1764
+ async refreshTools() {
1765
+ if (this.stateValue !== "ready" || !this.transport?.connected) return;
1766
+ const names = (await listControllerTools(this.options.tools)).map((tool) => tool.name).sort().join("\n");
1767
+ if (names === this.lastToolNames) return;
1768
+ this.lastToolNames = names;
1769
+ await this.transport.send({
1770
+ jsonrpc: "2.0",
1771
+ method: "notifications/tools/list_changed"
1772
+ });
1773
+ }
1774
+ };
1775
+ /** @experimental */
1776
+ function createControllerDevice(options) {
1777
+ assertControllerUrl(options.url);
1778
+ return new DeviceSession(options);
1779
+ }
1780
+
1781
+ //#endregion
1782
+ export { createWebSocketControllerTransport as a, ControllerRpcError as c, CONTROLLER_PROTOCOL_VERSION as d, CONTROLLER_TOOL_NAMES as f, randomToken as i, controllerError as l, base64UrlToBytes as n, CONTROLLER_PROTOCOL_SCHEMA as o, instanceFingerprint as r, validateControllerMessage as s, createControllerDevice as t, CONTROLLER_PROTOCOL_LIMITS as u };