gazesight 0.3.5 → 0.4.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/README.md +19 -11
- package/dist/chrome/background.js +1 -1
- package/dist/chrome/manifest.json +2 -2
- package/dist/chrome/options.css +9 -0
- package/dist/chrome/options.html +14 -0
- package/dist/chrome/options.js +13 -13
- package/dist/chrome/popup.css +5 -0
- package/dist/chrome/popup.html +6 -1
- package/dist/chrome/popup.js +1 -1
- package/dist/cli/index.js +1240 -17
- package/dist/firefox/background.js +1 -1
- package/dist/firefox/manifest.json +2 -2
- package/dist/firefox/options.css +9 -0
- package/dist/firefox/options.html +14 -0
- package/dist/firefox/options.js +29 -29
- package/dist/firefox/popup.css +5 -0
- package/dist/firefox/popup.html +6 -1
- package/dist/firefox/popup.js +1 -1
- package/dist/gazesight-firefox.xpi +0 -0
- package/dist/mcp/index.js +243 -9
- package/package.json +1 -1
package/dist/firefox/popup.css
CHANGED
|
@@ -78,6 +78,11 @@ body[data-connected="true"] .connection-dot {
|
|
|
78
78
|
main {
|
|
79
79
|
padding: 13px 14px 0;
|
|
80
80
|
}
|
|
81
|
+
.pairing-connect { margin-bottom: 10px; padding: 14px; border: 1px solid #cfc7ff; border-radius: 10px; background: linear-gradient(135deg, #f2efff, #fff); }
|
|
82
|
+
.pairing-connect strong { font-size: 15px; }
|
|
83
|
+
.pairing-connect p { margin: 6px 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
|
|
84
|
+
.pairing-connect .pairing-code { color: #160653; font: 700 17px/1.3 ui-monospace, monospace; letter-spacing: .08em; }
|
|
85
|
+
.pairing-connect .connect { margin-top: 10px; }
|
|
81
86
|
.status {
|
|
82
87
|
display: grid;
|
|
83
88
|
grid-template-columns: 1fr 1fr;
|
package/dist/firefox/popup.html
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
</header>
|
|
19
19
|
<div class="paper">
|
|
20
20
|
<main>
|
|
21
|
+
<section id="pairing-connect" class="pairing-connect" hidden>
|
|
22
|
+
<strong>Connect your browser</strong>
|
|
23
|
+
<p id="pairing-message">Run <code>gazesight pair</code>, then approve the matching code.</p>
|
|
24
|
+
<p id="pairing-code" class="pairing-code" hidden></p>
|
|
25
|
+
<button id="connect" class="primary connect">Connect to GazeSight</button>
|
|
26
|
+
</section>
|
|
21
27
|
<dl class="status">
|
|
22
28
|
<div class="status-card">
|
|
23
29
|
<span class="icon-disc"
|
|
@@ -188,4 +194,3 @@
|
|
|
188
194
|
<script type="module" src="popup.js"></script>
|
|
189
195
|
</body>
|
|
190
196
|
</html>
|
|
191
|
-
|
package/dist/firefox/popup.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function n(e,t){document.getElementById(e).textContent=t}var
|
|
1
|
+
var r="pendingPairing";function k(){return[...crypto.getRandomValues(new Uint8Array(32))].map(t=>t.toString(16).padStart(2,"0")).join("")}function E(){return location.protocol==="moz-extension:"?"firefox":"chrome"}async function m(e,t,o,i,a=2500){return fetch(`http://127.0.0.1:${e}/gazesight/pairing/${t}`,{method:"POST",headers:{"Content-Type":"application/json",...i?{Authorization:`Bearer ${i}`}:{}},body:JSON.stringify(o),signal:AbortSignal.timeout(a)})}async function w(e,t){let{requestId:o,proof:i,code:a,expiresAt:l,port:b}=t;for(;;){if(Date.now()>=l)return await browser.storage.local.remove(r),e("expired",a),!1;e("waiting_approval",a);let g;try{g=await m(b,"status",{requestId:o,secret:i})}catch{await new Promise(p=>setTimeout(p,800));continue}let d=await g.json();if(d.state==="approved"&&d.credential&&d.pairingId)return await browser.storage.local.set({token:d.credential,pairingId:d.pairingId,port:b,pairingState:"connected"}),await browser.storage.local.remove(r),e("connected"),!0;if(d.state==="rejected")return await browser.storage.local.remove(r),e("reconnect_required",a),!1;if(d.state==="expired"||g.status===410)return await browser.storage.local.remove(r),e("expired",a),!1;await new Promise(p=>setTimeout(p,800))}}async function f(e,t=32147){e("searching");let o=k(),i;try{i=await m(t,"request",{browser:E(),extensionId:browser.runtime.id,secret:o},void 0,6e4)}catch{return e("unavailable"),!1}if(!i.ok)return e("unavailable"),!1;let a=await i.json(),l={requestId:a.requestId,proof:o,code:a.code,expiresAt:a.expiresAt,port:t};return await browser.storage.local.set({[r]:l}),e("pending",a.code),w(e,l)}async function h(e){let o=(await browser.storage.local.get(r))[r];if(o){if(Date.now()>=o.expiresAt){await browser.storage.local.remove(r);return}return w(e,o)}}function n(e,t){document.getElementById(e).textContent=t}var u=!1,y;function v(e,t=!1){clearTimeout(y),n("feedback",e),t&&(y=setTimeout(()=>n("feedback",""),3500))}async function c(e){let t=await browser.runtime.sendMessage({type:"gazesight-ui",action:e});if(!t?.ok)throw new Error(t?.error??"GazeSight unavailable. Open settings to check pairing.");return t.value}async function s(){let e=await c("status");document.body.dataset.connected=String(e.connected),n("connection",e.connected?"Connected":"Disconnected");let t=await browser.storage.local.get(["token","port"]);document.getElementById("pairing-connect").hidden=typeof t.token=="string",document.body.dataset.background=e.backgroundCapture.enabled?"enabled":"disabled",n("background-state",e.backgroundCapture.enabled?"On":"Off"),n("background-toggle",e.backgroundCapture.enabled?"Disable":"Enable"),document.getElementById("background-toggle").setAttribute("aria-pressed",String(e.backgroundCapture.enabled)),n("background-description",e.backgroundCapture.enabled?"GazeSight can capture this target while you work elsewhere.":e.backgroundCapture.permissionGranted?"Background mode is off. Firefox still reports the optional permission; remove it in Add-ons Manager if needed.":e.backgroundMode==="permission_denied"?"Permission was not granted. Standard foreground capture remains available.":"Allow GazeSight to keep capturing the selected development tab while you work in another tab or application."),document.body.dataset.continuous=e.continuousAuthorization.enabled?"enabled":"disabled",n("continuous-state",e.continuousAuthorization.enabled?"On":"Off"),n("continuous-toggle",e.continuousAuthorization.enabled?"Disable":"Enable"),document.getElementById("continuous-toggle").setAttribute("aria-pressed",String(e.continuousAuthorization.enabled)),n("continuous-description",e.continuousAuthorization.enabled?e.continuousAuthorization.authorizedOrigin?`Staying connected to ${e.continuousAuthorization.authorizedOrigin} across reloads, navigation and previews.`:"Keeps GazeSight connected across reloads, navigation and previews once you select a tab.":"Keep GazeSight connected to this development tab across reloads, navigation and responsive previews, so you don't have to reauthorize it every time."),n("pairing",e.paired?"Paired":"Not paired"),n("target",e.targetState==="authorized"?"Authorized":e.targetState==="none"?"None":"Reauthorize"),n("url",e.url??"No target selected"),n("viewport",e.viewport?`${e.viewport.width} \xD7 ${e.viewport.height} CSS \xB7 DPR ${e.viewport.devicePixelRatio}`:"Viewport unavailable"),n("select-label",e.targetState==="reauthorization_required"?"Reauthorize this tab":"Use this tab"),n("last-capture",e.lastCapture?`${e.lastCapture.mode} \xB7 ${e.lastCapture.width} \xD7 ${e.lastCapture.height} \xB7 ${new Date(e.lastCapture.time).toLocaleTimeString()}`:"None yet"),n("last-error",e.lastError?`${e.lastError.code}: ${e.lastError.message}`:"None");for(let o of["fast","stable"])document.getElementById(o).disabled=u||!e.connected||e.targetState!=="authorized";document.getElementById("disconnect").disabled=u||e.targetState==="none"}var I={not_connected:"Not connected.",searching:"Searching for local GazeSight\u2026",pending:"Pairing request pending.",waiting_approval:"Waiting for approval in your terminal\u2026",connected:"Connected.",unavailable:"Connection unavailable. Run gazesight pair and try again.",expired:"Pairing expired. Try again.",reconnect_required:"Reconnect required."};function S(e,t){let o=document.getElementById("pairing-code");n("pairing-message",I[e]),o.hidden=!t,o.textContent=t?`Verification code: ${t}`:""}document.getElementById("connect").addEventListener("click",async()=>{let e=document.getElementById("connect");e.disabled=!0;let t=await browser.storage.local.get(["port"]);await f(S,Number(t.port??32147)),e.disabled=!1,await s()});(async()=>typeof(await browser.storage.local.get(["token"])).token!="string"&&await h(S)&&await s())();for(let e of["select","disconnect","fast","stable","diagnostics"])document.getElementById(e).addEventListener("click",async()=>{if(!u){u=!0,document.querySelectorAll("button").forEach(t=>t.disabled=!0),n("feedback",e==="fast"||e==="stable"?"Capturing\u2026":"Working\u2026");try{if(e==="fast"&&!await browser.permissions.contains({permissions:["downloads"]})){await browser.windows.create({url:browser.runtime.getURL("permission.html?action=download"),type:"popup",focused:!0,width:600,height:700}),window.close();return}let t=await c(e);e==="diagnostics"&&(document.getElementById("diagnostics-panel").hidden=!1,n("diagnostics-output",JSON.stringify(t,null,2))),v(e==="disconnect"?"Target disconnected.":e==="select"?"Target selected.":e==="diagnostics"?"Diagnostics updated.":e==="fast"?`\u2713 Saved to Downloads/${t.filename}`:"Capture completed.",e==="fast")}catch(t){v(t instanceof Error?t.message:"Operation failed.")}finally{u=!1,document.querySelectorAll("button").forEach(t=>t.disabled=!1),await s().catch(t=>n("feedback",String(t)))}}});document.getElementById("copy-url").addEventListener("click",async()=>{let e=document.getElementById("url").textContent;if(!(!e||e==="No target selected"))try{await navigator.clipboard.writeText(e),n("feedback","URL copied.")}catch{n("feedback","Unable to copy URL. Select and copy it manually.")}});document.getElementById("background-toggle").addEventListener("click",async()=>{if((await c("status")).backgroundCapture.enabled){await c("background-disable"),await s();return}await browser.windows.create({url:browser.runtime.getURL("permission.html?action=background"),type:"popup",focused:!0,width:600,height:700}),window.close()});document.getElementById("continuous-toggle").addEventListener("click",async()=>{let e=await c("status");if(e.continuousAuthorization.enabled){await c("continuous-disable"),await s();return}if(e.continuousAuthorization.permissionGranted){await c("continuous-enable"),await s();return}await browser.windows.create({url:browser.runtime.getURL("permission.html?action=continuous"),type:"popup",focused:!0,width:600,height:700}),window.close()});browser.commands.getAll().then(e=>{let t=e.find(o=>o.name==="gazesight-select-target")?.shortcut;t&&n("shortcut",t)}).catch(()=>{});document.getElementById("settings").addEventListener("click",()=>void browser.runtime.openOptionsPage());s().catch(e=>n("feedback",String(e)));var P=setInterval(()=>{u||s().catch(e=>n("feedback",String(e)))},2e3);window.addEventListener("pagehide",()=>clearInterval(P),{once:!0});
|
|
Binary file
|
package/dist/mcp/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
|
7
7
|
import { z as z2 } from "zod";
|
|
8
8
|
|
|
9
9
|
// apps/cli/src/config.ts
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
11
12
|
|
|
12
13
|
// apps/cli/src/config-browser.ts
|
|
13
14
|
import { z } from "zod";
|
|
@@ -18,6 +19,17 @@ var agentGuidanceSchema = z.strictObject({
|
|
|
18
19
|
var localConfigSchema = z.strictObject({
|
|
19
20
|
token: z.string().regex(/^[a-f0-9]{64}$/),
|
|
20
21
|
port: z.number().int().min(1024).max(65535),
|
|
22
|
+
pairings: z.array(
|
|
23
|
+
z.strictObject({
|
|
24
|
+
id: z.string().uuid(),
|
|
25
|
+
browser: z.enum(["firefox", "chrome"]),
|
|
26
|
+
extensionId: z.string().min(1).max(256),
|
|
27
|
+
origin: z.string().min(1).max(300),
|
|
28
|
+
credentialHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
29
|
+
createdAt: z.string().datetime(),
|
|
30
|
+
revokedAt: z.string().datetime().optional()
|
|
31
|
+
})
|
|
32
|
+
).max(32).default([]),
|
|
21
33
|
allowedOrigins: z.array(z.url()).default([]),
|
|
22
34
|
agentGuidance: agentGuidanceSchema.default({
|
|
23
35
|
enabled: true,
|
|
@@ -35,6 +47,14 @@ function readLocalConfig(path) {
|
|
|
35
47
|
);
|
|
36
48
|
}
|
|
37
49
|
}
|
|
50
|
+
function writeLocalConfig(path, value) {
|
|
51
|
+
const config = localConfigSchema.parse(value);
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
54
|
+
writeFileSync(temporary, JSON.stringify(config, null, 2), { mode: 384 });
|
|
55
|
+
renameSync(temporary, path);
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
38
58
|
|
|
39
59
|
// apps/mcp-server/src/config.ts
|
|
40
60
|
function loadConfig(env = process.env) {
|
|
@@ -48,7 +68,17 @@ function loadConfig(env = process.env) {
|
|
|
48
68
|
timeoutMs: z2.coerce.number().int().min(100).max(6e4),
|
|
49
69
|
allowedOrigins: z2.array(z2.url()),
|
|
50
70
|
debugDir: z2.string().optional(),
|
|
51
|
-
agentGuidance: agentGuidanceSchema
|
|
71
|
+
agentGuidance: agentGuidanceSchema,
|
|
72
|
+
pairings: z2.array(z2.object({
|
|
73
|
+
id: z2.string(),
|
|
74
|
+
browser: z2.enum(["firefox", "chrome"]),
|
|
75
|
+
extensionId: z2.string(),
|
|
76
|
+
origin: z2.string(),
|
|
77
|
+
credentialHash: z2.string(),
|
|
78
|
+
createdAt: z2.string(),
|
|
79
|
+
revokedAt: z2.string().optional()
|
|
80
|
+
})).default([]),
|
|
81
|
+
configPath: z2.string().optional()
|
|
52
82
|
}).parse({
|
|
53
83
|
port: env.GAZESIGHT_PORT ?? local?.port ?? 32147,
|
|
54
84
|
token: env.GAZESIGHT_PAIR_TOKEN ?? local?.token,
|
|
@@ -58,12 +88,14 @@ function loadConfig(env = process.env) {
|
|
|
58
88
|
agentGuidance: local?.agentGuidance ?? {
|
|
59
89
|
enabled: true,
|
|
60
90
|
visualFidelity: true
|
|
61
|
-
}
|
|
91
|
+
},
|
|
92
|
+
pairings: local?.pairings ?? [],
|
|
93
|
+
configPath: env.GAZESIGHT_CONFIG_FILE
|
|
62
94
|
});
|
|
63
95
|
}
|
|
64
96
|
|
|
65
97
|
// packages/protocol/src/release.ts
|
|
66
|
-
var VERSION = "0.
|
|
98
|
+
var VERSION = "0.4.0";
|
|
67
99
|
|
|
68
100
|
// apps/mcp-server/src/bridge/websocket-server.ts
|
|
69
101
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -795,6 +827,204 @@ var ActiveTargetRouter = class {
|
|
|
795
827
|
|
|
796
828
|
// apps/mcp-server/src/bridge/websocket-server.ts
|
|
797
829
|
import { createServer } from "node:http";
|
|
830
|
+
|
|
831
|
+
// apps/mcp-server/src/bridge/pairing-service.ts
|
|
832
|
+
import { createHash, randomBytes, randomInt, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
833
|
+
var hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
834
|
+
var equalHash = (value, expected) => {
|
|
835
|
+
const actual = Buffer.from(hash(value));
|
|
836
|
+
const wanted = Buffer.from(expected);
|
|
837
|
+
return actual.length === wanted.length && timingSafeEqual2(actual, wanted);
|
|
838
|
+
};
|
|
839
|
+
var json = (response, status, value) => {
|
|
840
|
+
response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
841
|
+
response.end(JSON.stringify(value));
|
|
842
|
+
};
|
|
843
|
+
async function body(request) {
|
|
844
|
+
const chunks = [];
|
|
845
|
+
let length = 0;
|
|
846
|
+
for await (const chunk of request) {
|
|
847
|
+
length += chunk.length;
|
|
848
|
+
if (length > 4096) throw new Error("body too large");
|
|
849
|
+
chunks.push(chunk);
|
|
850
|
+
}
|
|
851
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
852
|
+
}
|
|
853
|
+
function browserForOrigin(origin) {
|
|
854
|
+
if (/^moz-extension:\/\/[a-f0-9-]+\/?$/i.test(origin)) return "firefox";
|
|
855
|
+
if (/^chrome-extension:\/\/[a-p]{32}\/?$/.test(origin)) return "chrome";
|
|
856
|
+
}
|
|
857
|
+
var PairingService = class {
|
|
858
|
+
constructor(token, configPath, initial) {
|
|
859
|
+
this.token = token;
|
|
860
|
+
this.configPath = configPath;
|
|
861
|
+
this.pairings = initial;
|
|
862
|
+
}
|
|
863
|
+
pending = /* @__PURE__ */ new Map();
|
|
864
|
+
recent = /* @__PURE__ */ new Map();
|
|
865
|
+
pairings;
|
|
866
|
+
authenticate(credential, origin, browser) {
|
|
867
|
+
if (!origin) return false;
|
|
868
|
+
return this.pairings.some((pairing) => !pairing.revokedAt && pairing.browser === browser && pairing.origin === origin && equalHash(credential, pairing.credentialHash));
|
|
869
|
+
}
|
|
870
|
+
listPending() {
|
|
871
|
+
this.expire();
|
|
872
|
+
return [...this.pending.values()].filter((item) => item.state === "pending").map(({ id, browser, extensionId, code, createdAt, expiresAt }) => ({ id, browser, extensionId, code, createdAt, expiresAt }));
|
|
873
|
+
}
|
|
874
|
+
listPairings() {
|
|
875
|
+
return this.pairings.map(({ id, browser, extensionId, origin, createdAt, revokedAt }) => ({ id, browser, extensionId, origin, createdAt, ...revokedAt ? { revokedAt } : {} }));
|
|
876
|
+
}
|
|
877
|
+
deliveryState(id) {
|
|
878
|
+
const item = this.pending.get(id);
|
|
879
|
+
return item ? { state: item.state, redeemed: item.redeemed === true, expiresAt: item.expiresAt } : void 0;
|
|
880
|
+
}
|
|
881
|
+
approve(id, code) {
|
|
882
|
+
this.expire();
|
|
883
|
+
const item = this.pending.get(id);
|
|
884
|
+
if (!item || item.state !== "pending" || item.code !== code) return false;
|
|
885
|
+
const credential = randomBytes(32).toString("hex");
|
|
886
|
+
const pairing = { id: crypto.randomUUID(), browser: item.browser, extensionId: item.extensionId, origin: item.origin, credentialHash: hash(credential), createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
887
|
+
this.pairings.push(pairing);
|
|
888
|
+
item.credential = credential;
|
|
889
|
+
item.state = "approved";
|
|
890
|
+
this.persist();
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
reject(id) {
|
|
894
|
+
const item = this.pending.get(id);
|
|
895
|
+
if (!item || item.state !== "pending") return false;
|
|
896
|
+
item.state = "rejected";
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
revoke(id) {
|
|
900
|
+
const item = this.pairings.find((pairing) => pairing.id === id && !pairing.revokedAt);
|
|
901
|
+
if (!item) return false;
|
|
902
|
+
item.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
903
|
+
this.persist();
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
persist() {
|
|
907
|
+
if (!this.configPath) return;
|
|
908
|
+
const config = readLocalConfig(this.configPath);
|
|
909
|
+
writeLocalConfig(this.configPath, { ...config, pairings: this.pairings });
|
|
910
|
+
}
|
|
911
|
+
expire() {
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
for (const item of this.pending.values()) if (item.expiresAt <= now && item.state === "pending") item.state = "rejected";
|
|
914
|
+
}
|
|
915
|
+
admin(request) {
|
|
916
|
+
return request.headers.origin === void 0 && tokensMatch(request.headers.authorization?.replace(/^Bearer /, "") ?? "", this.token);
|
|
917
|
+
}
|
|
918
|
+
async handle(request, response) {
|
|
919
|
+
const url = request.url;
|
|
920
|
+
if (!url?.startsWith("/gazesight/pairing/")) return false;
|
|
921
|
+
const origin = request.headers.origin;
|
|
922
|
+
if (origin && originAllowed(origin)) response.setHeader("Access-Control-Allow-Origin", origin);
|
|
923
|
+
response.setHeader("Vary", "Origin");
|
|
924
|
+
if (request.method === "OPTIONS" && origin && originAllowed(origin)) {
|
|
925
|
+
response.writeHead(204, { "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "content-type, authorization" }).end();
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
if (url === "/gazesight/pairing/request" && request.method === "POST") {
|
|
930
|
+
const browser = origin ? browserForOrigin(origin) : void 0;
|
|
931
|
+
if (!origin || !browser) {
|
|
932
|
+
json(response, 403, { error: "ineligible_origin" });
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
const now = Date.now();
|
|
936
|
+
const attempts = (this.recent.get(origin) ?? []).filter((time) => now - time < 6e4);
|
|
937
|
+
if (attempts.length >= 5) {
|
|
938
|
+
json(response, 429, { error: "rate_limited" });
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
attempts.push(now);
|
|
942
|
+
this.recent.set(origin, attempts);
|
|
943
|
+
const value = await body(request);
|
|
944
|
+
if (value.browser !== browser || typeof value.extensionId !== "string" || value.extensionId.length > 256 || typeof value.secret !== "string" || !/^[a-f0-9]{64}$/.test(value.secret)) {
|
|
945
|
+
json(response, 400, { error: "invalid_request" });
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
const item = { id: crypto.randomUUID(), browser, extensionId: value.extensionId, origin, secretHash: hash(value.secret), code: String(randomInt(0, 1e6)).padStart(6, "0"), createdAt: now, expiresAt: now + 12e4, attempts: 0, state: "pending" };
|
|
949
|
+
this.pending.set(item.id, item);
|
|
950
|
+
json(response, 201, { requestId: item.id, code: item.code, expiresAt: item.expiresAt });
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
if (url === "/gazesight/pairing/status" && request.method === "POST") {
|
|
954
|
+
const value = await body(request);
|
|
955
|
+
const item = typeof value.requestId === "string" ? this.pending.get(value.requestId) : void 0;
|
|
956
|
+
if (!item || item.origin !== origin || typeof value.secret !== "string") {
|
|
957
|
+
json(response, 404, { state: "unavailable" });
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
item.attempts += 1;
|
|
961
|
+
if (item.attempts > 180 || !equalHash(value.secret, item.secretHash)) {
|
|
962
|
+
json(response, 403, { state: "rejected" });
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
if (Date.now() >= item.expiresAt) {
|
|
966
|
+
json(response, 410, { state: "expired" });
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
if (item.state === "approved" && item.credential && !item.redeemed) {
|
|
970
|
+
const pairing = this.pairings.find((value2) => value2.origin === item.origin && value2.extensionId === item.extensionId && value2.credentialHash === hash(item.credential));
|
|
971
|
+
item.redeemed = true;
|
|
972
|
+
json(response, 200, { state: "approved", credential: item.credential, pairingId: pairing?.id });
|
|
973
|
+
item.credential = void 0;
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
json(response, 200, { state: item.state });
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
if (url === "/gazesight/pairing/revoke-self" && request.method === "POST" && origin) {
|
|
980
|
+
const value = await body(request);
|
|
981
|
+
const id = typeof value.id === "string" ? value.id : "";
|
|
982
|
+
const credential = request.headers.authorization?.replace(/^Bearer /, "") ?? "";
|
|
983
|
+
const pairing = this.pairings.find((item) => item.id === id && item.origin === origin && !item.revokedAt && equalHash(credential, item.credentialHash));
|
|
984
|
+
if (!pairing) {
|
|
985
|
+
json(response, 403, { ok: false });
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
pairing.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
989
|
+
this.persist();
|
|
990
|
+
json(response, 200, { ok: true });
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
if (!this.admin(request)) {
|
|
994
|
+
json(response, 404, { error: "not_found" });
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
if (url === "/gazesight/pairing/delivery" && request.method === "POST") {
|
|
998
|
+
const value = await body(request);
|
|
999
|
+
const delivery = typeof value.id === "string" ? this.deliveryState(value.id) : void 0;
|
|
1000
|
+
json(response, delivery ? 200 : 404, delivery ?? { error: "not_found" });
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
if (url === "/gazesight/pairing/pending" && request.method === "GET") {
|
|
1004
|
+
json(response, 200, { requests: this.listPending() });
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
if (url === "/gazesight/pairing/list" && request.method === "GET") {
|
|
1008
|
+
json(response, 200, { pairings: this.listPairings() });
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
if (request.method === "POST" && ["approve", "reject", "revoke"].some((action) => url === `/gazesight/pairing/${action}`)) {
|
|
1012
|
+
const value = await body(request);
|
|
1013
|
+
const id = typeof value.id === "string" ? value.id : "";
|
|
1014
|
+
const ok = url.endsWith("approve") ? this.approve(id, typeof value.code === "string" ? value.code : "") : url.endsWith("reject") ? this.reject(id) : this.revoke(id);
|
|
1015
|
+
json(response, ok ? 200 : 404, { ok });
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
json(response, 404, { error: "not_found" });
|
|
1019
|
+
return true;
|
|
1020
|
+
} catch {
|
|
1021
|
+
json(response, 400, { error: "invalid_request" });
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// apps/mcp-server/src/bridge/websocket-server.ts
|
|
798
1028
|
var EMPTY_STATE = { selectedTab: null, url: null };
|
|
799
1029
|
function browserFromOrigin(origin) {
|
|
800
1030
|
if (origin === void 0) return void 0;
|
|
@@ -805,10 +1035,12 @@ function browserFromOrigin(origin) {
|
|
|
805
1035
|
var Bridge = class {
|
|
806
1036
|
constructor(config) {
|
|
807
1037
|
this.config = config;
|
|
1038
|
+
this.pairing = new PairingService(config.token, config.configPath, config.pairings ?? []);
|
|
808
1039
|
const verifyClient = (info) => originAllowed(info.origin || void 0);
|
|
809
|
-
this.http = createServer((request, response) => {
|
|
1040
|
+
this.http = createServer(async (request, response) => {
|
|
810
1041
|
response.setHeader("Cache-Control", "no-store");
|
|
811
1042
|
response.setHeader("X-GazeSight-Version", VERSION);
|
|
1043
|
+
if (await this.pairing.handle(request, response)) return;
|
|
812
1044
|
if (request.method !== "GET" || request.url !== "/gazesight/status" || request.headers.origin !== void 0) {
|
|
813
1045
|
response.writeHead(404).end();
|
|
814
1046
|
return;
|
|
@@ -855,14 +1087,14 @@ var Bridge = class {
|
|
|
855
1087
|
const message = JSON.parse(bytes.toString());
|
|
856
1088
|
if (!paired) {
|
|
857
1089
|
const hello = helloSchema.parse(message);
|
|
858
|
-
if (!tokensMatch(hello.token, config.token))
|
|
859
|
-
throw new Error("Pairing rejected");
|
|
860
1090
|
const claimedBrowser = hello.state.browser?.name;
|
|
861
1091
|
if (originBrowser !== void 0 && claimedBrowser !== void 0 && originBrowser !== claimedBrowser)
|
|
862
1092
|
throw new Error("Origin/browser mismatch");
|
|
863
1093
|
const browser = claimedBrowser ?? originBrowser;
|
|
864
1094
|
if (browser === void 0)
|
|
865
1095
|
throw new Error("Unidentifiable browser");
|
|
1096
|
+
if (!tokensMatch(hello.token, config.token) && !this.pairing.authenticate(hello.token, request.headers.origin, browser))
|
|
1097
|
+
throw new Error("Pairing rejected");
|
|
866
1098
|
if (this.registry.isDuplicate(browser))
|
|
867
1099
|
throw new Error("Duplicate connection");
|
|
868
1100
|
connection = this.registry.register(browser, hello.state);
|
|
@@ -905,6 +1137,7 @@ var Bridge = class {
|
|
|
905
1137
|
wiring = /* @__PURE__ */ new Map();
|
|
906
1138
|
server;
|
|
907
1139
|
http;
|
|
1140
|
+
pairing;
|
|
908
1141
|
// Resolves the connection currently backing the global active target, or
|
|
909
1142
|
// undefined if no browser is active (nothing selected yet, or the
|
|
910
1143
|
// previously active browser disconnected — no automatic fallback).
|
|
@@ -968,6 +1201,7 @@ var Bridge = class {
|
|
|
968
1201
|
return {
|
|
969
1202
|
connected: this.connected,
|
|
970
1203
|
paired: this.connected,
|
|
1204
|
+
authorizedPairings: this.pairing.listPairings().filter((item) => !item.revokedAt).length,
|
|
971
1205
|
targetState: this.state.targetState ?? "none",
|
|
972
1206
|
tabId: this.state.selectedTab,
|
|
973
1207
|
bridge: { port: this.port },
|
|
@@ -995,7 +1229,7 @@ var Bridge = class {
|
|
|
995
1229
|
// apps/mcp-server/src/server.ts
|
|
996
1230
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
997
1231
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
998
|
-
import { join } from "node:path";
|
|
1232
|
+
import { join as join2 } from "node:path";
|
|
999
1233
|
|
|
1000
1234
|
// apps/mcp-server/src/tools/capture.ts
|
|
1001
1235
|
function toImage(value) {
|
|
@@ -1226,7 +1460,7 @@ function createServer2(bridge, config) {
|
|
|
1226
1460
|
if (config.debugDir) {
|
|
1227
1461
|
await mkdir(config.debugDir, { recursive: true });
|
|
1228
1462
|
await writeFile(
|
|
1229
|
-
|
|
1463
|
+
join2(config.debugDir, `last-capture.${metadata.format}`),
|
|
1230
1464
|
Buffer.from(image.data, "base64")
|
|
1231
1465
|
);
|
|
1232
1466
|
}
|