@xingular/harness 1.5.0-unstable → 1.6.0-unstable
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/CHANGELOG.md +93 -0
- package/browser-extension/README.md +32 -0
- package/browser-extension/background.js +612 -0
- package/browser-extension/manifest.json +16 -0
- package/browser-extension/overlay.js +97 -0
- package/browser-extension/popup.css +19 -0
- package/browser-extension/popup.html +30 -0
- package/browser-extension/popup.js +101 -0
- package/browser-extension/site-access.js +63 -0
- package/dist/cli.js +1489 -680
- package/dist/sdk/browser/config.d.ts +3 -0
- package/dist/sdk/browser/config.js +2 -1
- package/dist/sdk/browser/extension/bridge.d.ts +55 -0
- package/dist/sdk/browser/extension/bridge.js +336 -0
- package/dist/sdk/browser/extension/session.d.ts +50 -0
- package/dist/sdk/browser/extension/session.js +321 -0
- package/dist/sdk/browser/index.d.ts +7 -2
- package/dist/sdk/browser/index.js +58 -25
- package/dist/sdk/browser/serverMethods.js +4 -1
- package/dist/sdk/browser/tools.d.ts +99 -2
- package/dist/sdk/browser/tools.js +6 -1
- package/dist/sdk/commands/registry.d.ts +5 -0
- package/dist/sdk/commands/registry.js +1 -0
- package/dist/sdk/harness.d.ts +4 -0
- package/dist/sdk/harness.js +1 -1
- package/dist/sdk/pi/retryPolicy.d.ts +19 -0
- package/dist/sdk/pi/retryPolicy.js +11 -0
- package/dist/sdk/prompt/compile.d.ts +0 -2
- package/dist/sdk/prompt/compile.js +2 -6
- package/dist/sdk/prompt/reporting.d.ts +1 -1
- package/dist/sdk/prompt/reporting.js +30 -13
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/remote/webClient.js +6 -2
- package/dist/sdk/sessions/contracts.d.ts +2 -0
- package/dist/sdk/sessions/contracts.js +5 -1
- package/dist/sdk/sessions/planMode.d.ts +16 -6
- package/dist/sdk/sessions/planMode.js +59 -23
- package/dist/sdk/sessions/runner.js +14 -5
- package/dist/sdk/sessions/workSlice.d.ts +15 -1
- package/dist/sdk/sessions/workSlice.js +17 -9
- package/dist/sdk/terminal/controller.d.ts +6 -0
- package/dist/sdk/terminal/controller.js +44 -5
- package/dist/sdk/terminal/opentui/model.js +3 -1
- package/dist/sdk/terminal/piExtension.js +7 -2
- package/dist/sdk/terminal/stallWatch.d.ts +18 -0
- package/dist/sdk/terminal/stallWatch.js +20 -0
- package/dist/sdk/terminal/subagentMonitor.d.ts +2 -0
- package/dist/sdk/terminal/subagentMonitor.js +4 -1
- package/dist/sdk/terminal/terminals.js +35 -4
- package/dist/sdk/terminal/toolName.d.ts +6 -0
- package/dist/sdk/terminal/toolName.js +12 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +14 -0
- package/dist/sdk/terminal/tui/commands.js +4 -1
- package/dist/sdk/terminal/tui/pages/followUpQueuePage.d.ts +34 -0
- package/dist/sdk/terminal/tui/pages/followUpQueuePage.js +130 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +37 -7
- package/dist/sdk/terminal/tui/runtime.js +2 -1
- package/dist/sdk/terminal/tui/settingsModel.js +88 -7
- package/dist/sdk/tools/askUser.js +2 -2
- package/dist/sdk/world/world.d.ts +12 -2
- package/dist/sdk/world/world.js +42 -4
- package/dist/terminal-opentui.js +1456 -756
- package/dist/terminal-pi.js +62 -31
- package/docs/README.md +1 -0
- package/docs/architecture/02-sessions-and-runs.md +43 -25
- package/docs/architecture/04-prompt.md +23 -22
- package/docs/architecture/10-server-protocol.md +23 -3
- package/docs/architecture/11-terminal.md +41 -29
- package/docs/architecture/12-storage.md +2 -1
- package/docs/architecture/13-world-tools.md +35 -22
- package/docs/architecture/14-surfaces-and-parity.md +24 -20
- package/docs/architecture/README.md +4 -1
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +3 -1
- package/docs/decisions/ADR-0027-connected-sketch-display.md +4 -2
- package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +6 -1
- package/docs/decisions/ADR-0077-plan-runs-may-conclude-no-plan-needed.md +120 -0
- package/docs/decisions/ADR-0078-child-aware-stall-watch.md +46 -0
- package/docs/decisions/ADR-0079-phase-led-reporting-guidance.md +72 -0
- package/docs/decisions/ADR-0080-fixed-width-question-sketch-canvas.md +50 -0
- package/docs/decisions/ADR-0081-no-work-slice-budget-by-default.md +36 -0
- package/docs/decisions/ADR-0082-connected-brave-browser.md +72 -0
- package/docs/parity.md +7 -5
- package/docs/releases/1.5.0-unstable.md +12 -3
- package/docs/releases/1.5.1-unstable.md +24 -0
- package/docs/releases/1.6.0-unstable.md +59 -0
- package/docs/study/00-index.md +17 -1
- package/docs/study/STUDY-0007-claude-code-prompt.md +162 -0
- package/docs/study/STUDY-0008-browser-control.md +281 -0
- package/docs/study/STUDY-0009-claude-code-reporting.md +205 -0
- package/docs/study/sources/claude-code.md +51 -39
- package/docs/terminal-tui.md +46 -18
- package/package.json +2 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const hostId = '__xingular_agent_browser_badge__'
|
|
3
|
+
let host = document.getElementById(hostId)
|
|
4
|
+
if (host) return
|
|
5
|
+
|
|
6
|
+
host = document.createElement('div')
|
|
7
|
+
host.id = hostId
|
|
8
|
+
host.setAttribute('role', 'group')
|
|
9
|
+
host.setAttribute('aria-label', 'Xingular Agent browser controls')
|
|
10
|
+
host.style.cssText = 'all:initial;position:fixed;inset:auto 16px 16px auto;z-index:2147483647;pointer-events:auto;'
|
|
11
|
+
const root = host.attachShadow({ mode: 'closed' })
|
|
12
|
+
const style = document.createElement('style')
|
|
13
|
+
style.textContent = `
|
|
14
|
+
:host { all: initial; }
|
|
15
|
+
.badge { display:flex; align-items:center; gap:9px; padding:8px 10px 8px 12px; border:1px solid rgba(255,255,255,.16); border-radius:999px; background:#17201e; color:#f5f4ee; box-shadow:0 8px 24px rgba(0,0,0,.28); font:500 12px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; letter-spacing:0; }
|
|
16
|
+
.dot { width:7px; height:7px; flex:none; border-radius:50%; background:#81d5a8; box-shadow:0 0 0 3px rgba(129,213,168,.15); }
|
|
17
|
+
.badge.paused .dot { background:#e5b960; box-shadow:0 0 0 3px rgba(229,185,96,.15); }
|
|
18
|
+
.label { white-space:nowrap; }
|
|
19
|
+
button { appearance:none; border:0; border-radius:999px; padding:5px 9px; background:#2b3834; color:inherit; font:inherit; cursor:pointer; }
|
|
20
|
+
button:hover { background:#3a4b45; }
|
|
21
|
+
button:focus-visible { outline:2px solid #c5ead7; outline-offset:2px; }
|
|
22
|
+
button.stop { background:#643d3a; }
|
|
23
|
+
button.stop:hover { background:#7a4945; }
|
|
24
|
+
.pulse { position:fixed; width:14px; height:14px; margin:-7px 0 0 -7px; border:2px solid #d8f7e5; border-radius:50%; background:rgba(94,193,139,.22); box-shadow:0 0 0 4px rgba(94,193,139,.2); pointer-events:none; animation:agent-pulse .65s cubic-bezier(.2,.8,.2,1) forwards; }
|
|
25
|
+
@keyframes agent-pulse { from { opacity:.95; transform:scale(.45); } to { opacity:0; transform:scale(2.7); } }
|
|
26
|
+
`
|
|
27
|
+
const badge = document.createElement('div')
|
|
28
|
+
badge.className = 'badge'
|
|
29
|
+
const dot = document.createElement('span')
|
|
30
|
+
dot.className = 'dot'
|
|
31
|
+
const label = document.createElement('span')
|
|
32
|
+
label.className = 'label'
|
|
33
|
+
label.textContent = 'Xingular Agent is on this site'
|
|
34
|
+
const pause = document.createElement('button')
|
|
35
|
+
pause.type = 'button'
|
|
36
|
+
pause.textContent = 'Pause'
|
|
37
|
+
pause.setAttribute('aria-label', 'Pause browser control')
|
|
38
|
+
const stop = document.createElement('button')
|
|
39
|
+
stop.type = 'button'
|
|
40
|
+
stop.className = 'stop'
|
|
41
|
+
stop.textContent = 'Stop'
|
|
42
|
+
stop.setAttribute('aria-label', 'Stop browser control')
|
|
43
|
+
badge.append(dot, label, pause, stop)
|
|
44
|
+
root.append(style, badge)
|
|
45
|
+
document.documentElement.append(host)
|
|
46
|
+
|
|
47
|
+
let paused = false
|
|
48
|
+
const refresh = () => {
|
|
49
|
+
badge.classList.toggle('paused', paused)
|
|
50
|
+
label.textContent = paused ? 'Xingular Agent is paused' : 'Xingular Agent is on this site'
|
|
51
|
+
pause.textContent = paused ? 'Resume' : 'Pause'
|
|
52
|
+
pause.setAttribute('aria-label', paused ? 'Resume browser control' : 'Pause browser control')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ask = action => chrome.runtime.sendMessage({ type: 'control', action })
|
|
56
|
+
pause.addEventListener('click', async event => {
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
event.stopPropagation()
|
|
59
|
+
const result = await ask(paused ? 'resume' : 'pause').catch(() => null)
|
|
60
|
+
if (result?.ok) {
|
|
61
|
+
paused = !paused
|
|
62
|
+
refresh()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
stop.addEventListener('click', async event => {
|
|
66
|
+
event.preventDefault()
|
|
67
|
+
event.stopPropagation()
|
|
68
|
+
await ask('stop').catch(() => null)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
let mutationTimer = null
|
|
72
|
+
const observer = new MutationObserver(records => {
|
|
73
|
+
if (records.every(record => host.contains(record.target))) return
|
|
74
|
+
if (mutationTimer) clearTimeout(mutationTimer)
|
|
75
|
+
mutationTimer = setTimeout(() => chrome.runtime.sendMessage({ type: 'page-changed' }).catch(() => {}), 250)
|
|
76
|
+
})
|
|
77
|
+
const onMessage = message => {
|
|
78
|
+
if (message?.type === 'badge') {
|
|
79
|
+
paused = message.paused === true
|
|
80
|
+
refresh()
|
|
81
|
+
} else if (message?.type === 'pulse') {
|
|
82
|
+
const pulse = document.createElement('span')
|
|
83
|
+
pulse.className = 'pulse'
|
|
84
|
+
pulse.style.left = `${Number(message.x) || 0}px`
|
|
85
|
+
pulse.style.top = `${Number(message.y) || 0}px`
|
|
86
|
+
root.append(pulse)
|
|
87
|
+
setTimeout(() => pulse.remove(), 750)
|
|
88
|
+
} else if (message?.type === 'clear') {
|
|
89
|
+
observer.disconnect()
|
|
90
|
+
if (mutationTimer) clearTimeout(mutationTimer)
|
|
91
|
+
chrome.runtime.onMessage.removeListener(onMessage)
|
|
92
|
+
host.remove()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
chrome.runtime.onMessage.addListener(onMessage)
|
|
96
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['aria-label', 'aria-expanded', 'disabled', 'href', 'role', 'value'] })
|
|
97
|
+
})()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
* { box-sizing: border-box; }
|
|
2
|
+
body { width: 320px; margin: 0; background: #f4f3ec; color: #202823; font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
3
|
+
main { padding: 20px; }
|
|
4
|
+
h1 { margin: 0 0 8px; font-size: 19px; line-height: 1.2; letter-spacing: -.025em; }
|
|
5
|
+
h2 { margin: 0 0 4px; font-size: 14px; font-weight: 650; }
|
|
6
|
+
p { margin: 0 0 12px; color: #46534b; }
|
|
7
|
+
#status { min-height: 22px; color: #28633f; font-weight: 600; }
|
|
8
|
+
section { padding-top: 14px; border-top: 1px solid #d7d9ce; margin-top: 14px; }
|
|
9
|
+
label { display: block; margin-bottom: 5px; font-weight: 600; }
|
|
10
|
+
input { width: 100%; min-height: 38px; margin-bottom: 9px; border: 1px solid #a9b2a8; border-radius: 8px; padding: 8px 10px; color: #202823; background: #fff; font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
11
|
+
button { width: 100%; min-height: 38px; margin-top: 7px; border: 0; border-radius: 8px; padding: 8px 12px; background: #23583a; color: #fff; font: inherit; font-size: 13px; font-weight: 600; line-height: 1.2; cursor: pointer; }
|
|
12
|
+
button:hover { background: #19462d; }
|
|
13
|
+
button:focus-visible, input:focus-visible { outline: 3px solid #91c7a4; outline-offset: 2px; }
|
|
14
|
+
button:disabled { opacity: .55; cursor: default; }
|
|
15
|
+
button.secondary { background: transparent; color: #35473b; border: 1px solid #a9b2a8; }
|
|
16
|
+
button.secondary:hover { background: #e5e8df; }
|
|
17
|
+
.note { margin-top: 16px; font-size: 12px; line-height: 1.45; }
|
|
18
|
+
#error { margin-top: 12px; color: #962f2a; }
|
|
19
|
+
[hidden] { display: none !important; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Xingular Agent</title>
|
|
7
|
+
<link rel="stylesheet" href="popup.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Brave browser access</h1>
|
|
12
|
+
<p id="status" role="status">Checking connection…</p>
|
|
13
|
+
<section id="pairing" hidden>
|
|
14
|
+
<label for="pairing-code">Pairing code</label>
|
|
15
|
+
<input id="pairing-code" autocomplete="off" spellcheck="false" placeholder="38671:ABCDEF012345">
|
|
16
|
+
<button id="connect" type="button">Connect to Harness</button>
|
|
17
|
+
</section>
|
|
18
|
+
<section id="site-access">
|
|
19
|
+
<h2 id="site-name">Current site</h2>
|
|
20
|
+
<p id="site-detail">Only the site you allow is exposed to Xingular Agent.</p>
|
|
21
|
+
<button id="allow" type="button">Allow this site</button>
|
|
22
|
+
<button id="remove" type="button" class="secondary" hidden>Remove site access</button>
|
|
23
|
+
</section>
|
|
24
|
+
<p class="note">Access covers ordinary tabs on this site. Incognito tabs are excluded. The extension does not read history or cookies.</p>
|
|
25
|
+
<p id="error" role="alert" hidden></p>
|
|
26
|
+
</main>
|
|
27
|
+
<script src="site-access.js"></script>
|
|
28
|
+
<script src="popup.js"></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const statusLine = document.getElementById('status')
|
|
2
|
+
const pairing = document.getElementById('pairing')
|
|
3
|
+
const codeInput = document.getElementById('pairing-code')
|
|
4
|
+
const connectButton = document.getElementById('connect')
|
|
5
|
+
const siteName = document.getElementById('site-name')
|
|
6
|
+
const siteDetail = document.getElementById('site-detail')
|
|
7
|
+
const allowButton = document.getElementById('allow')
|
|
8
|
+
const removeButton = document.getElementById('remove')
|
|
9
|
+
const errorLine = document.getElementById('error')
|
|
10
|
+
let currentOrigin = null
|
|
11
|
+
let currentPermission = null
|
|
12
|
+
|
|
13
|
+
function showError(message) {
|
|
14
|
+
errorLine.textContent = message
|
|
15
|
+
errorLine.hidden = !message
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function message(value) {
|
|
19
|
+
const response = await chrome.runtime.sendMessage(value)
|
|
20
|
+
if (!response?.ok) throw new Error(response?.error || 'The extension could not complete that action.')
|
|
21
|
+
return response.value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function currentTab() {
|
|
25
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
26
|
+
return tab || null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function update() {
|
|
30
|
+
showError('')
|
|
31
|
+
const state = await message({ type: 'status' }).catch(() => ({ state: 'waiting', connected: false, paired: false }))
|
|
32
|
+
statusLine.textContent = state.connected ? (state.paused ? 'Connected · control paused' : 'Connected to Harness') : state.paired ? 'Paired · waiting for Harness' : 'Not connected to Harness'
|
|
33
|
+
pairing.hidden = state.paired
|
|
34
|
+
|
|
35
|
+
const tab = await currentTab()
|
|
36
|
+
currentOrigin = tab && !tab.incognito ? XingularSiteAccess.originOf(tab.url || '') : null
|
|
37
|
+
currentPermission = tab && !tab.incognito ? XingularSiteAccess.permissionFor(tab.url || '') : null
|
|
38
|
+
if (!currentOrigin || !currentPermission) {
|
|
39
|
+
siteName.textContent = 'No ordinary website tab selected'
|
|
40
|
+
siteDetail.textContent = tab?.incognito ? 'Incognito is never available to Xingular Agent.' : 'Open an http or https site in a normal Brave tab.'
|
|
41
|
+
allowButton.disabled = true
|
|
42
|
+
removeButton.hidden = true
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
let host = new URL(currentOrigin).host
|
|
46
|
+
try { host = new URL(tab.url).host } catch {}
|
|
47
|
+
siteName.textContent = host
|
|
48
|
+
const allowed = await XingularSiteAccess.allowed(tab.url)
|
|
49
|
+
siteDetail.textContent = allowed ? 'Xingular Agent can access normal tabs on this site.' : 'Allow this exact site before its tabs can be read or controlled.'
|
|
50
|
+
allowButton.hidden = allowed
|
|
51
|
+
allowButton.disabled = !state.connected
|
|
52
|
+
removeButton.hidden = !allowed
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
connectButton.addEventListener('click', async () => {
|
|
56
|
+
showError('')
|
|
57
|
+
const match = /^(\d{1,5}):([0-9a-f]{12})$/i.exec(codeInput.value.trim())
|
|
58
|
+
if (!match) return showError('Enter the port and code shown in Harness Browser settings.')
|
|
59
|
+
connectButton.disabled = true
|
|
60
|
+
try {
|
|
61
|
+
await message({ type: 'pair', port: Number(match[1]), code: match[2] })
|
|
62
|
+
codeInput.value = ''
|
|
63
|
+
await update()
|
|
64
|
+
} catch (error) {
|
|
65
|
+
showError(error.message)
|
|
66
|
+
} finally {
|
|
67
|
+
connectButton.disabled = false
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
allowButton.addEventListener('click', async () => {
|
|
72
|
+
showError('')
|
|
73
|
+
if (!currentOrigin) return
|
|
74
|
+
allowButton.disabled = true
|
|
75
|
+
try {
|
|
76
|
+
const granted = await XingularSiteAccess.grant(currentOrigin)
|
|
77
|
+
if (!granted) throw new Error('Brave did not grant access to this site.')
|
|
78
|
+
await message({ type: 'site-access-changed' })
|
|
79
|
+
await update()
|
|
80
|
+
} catch (error) {
|
|
81
|
+
showError(error.message)
|
|
82
|
+
allowButton.disabled = false
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
removeButton.addEventListener('click', async () => {
|
|
87
|
+
showError('')
|
|
88
|
+
if (!currentOrigin) return
|
|
89
|
+
removeButton.disabled = true
|
|
90
|
+
try {
|
|
91
|
+
await XingularSiteAccess.revoke(currentOrigin)
|
|
92
|
+
await message({ type: 'site-access-changed' })
|
|
93
|
+
await update()
|
|
94
|
+
} catch (error) {
|
|
95
|
+
showError(error.message)
|
|
96
|
+
} finally {
|
|
97
|
+
removeButton.disabled = false
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
void update()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const originOf = address => {
|
|
3
|
+
try {
|
|
4
|
+
const url = new URL(address)
|
|
5
|
+
return ['http:', 'https:'].includes(url.protocol) ? url.origin : null
|
|
6
|
+
} catch {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const permissionFor = address => {
|
|
12
|
+
const origin = originOf(address)
|
|
13
|
+
if (!origin) return null
|
|
14
|
+
const url = new URL(origin)
|
|
15
|
+
// Chrome host permissions do not scope to ports. The stored origin below does.
|
|
16
|
+
return `${url.protocol}//${url.hostname}/*`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const savedOrigins = async api => {
|
|
20
|
+
const saved = await api.storage.local.get('allowedOrigins')
|
|
21
|
+
return Array.isArray(saved.allowedOrigins)
|
|
22
|
+
? [...new Set(saved.allowedOrigins.filter(origin => typeof origin === 'string' && originOf(origin) === origin))]
|
|
23
|
+
: []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const allowed = async (address, api = chrome) => {
|
|
27
|
+
const origin = originOf(address)
|
|
28
|
+
const permission = permissionFor(address)
|
|
29
|
+
if (!origin || !permission || !(await savedOrigins(api)).includes(origin)) return false
|
|
30
|
+
return api.permissions.contains({ origins: [permission] })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const grant = async (address, api = chrome) => {
|
|
34
|
+
const origin = originOf(address)
|
|
35
|
+
const permission = permissionFor(address)
|
|
36
|
+
if (!origin || !permission || !await api.permissions.request({ origins: [permission] })) return false
|
|
37
|
+
const origins = await savedOrigins(api)
|
|
38
|
+
if (!origins.includes(origin)) origins.push(origin)
|
|
39
|
+
await api.storage.local.set({ allowedOrigins: origins })
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const revoke = async (address, api = chrome) => {
|
|
44
|
+
const origin = originOf(address)
|
|
45
|
+
const permission = permissionFor(address)
|
|
46
|
+
if (!origin || !permission) return
|
|
47
|
+
const origins = (await savedOrigins(api)).filter(saved => saved !== origin)
|
|
48
|
+
await api.storage.local.set({ allowedOrigins: origins })
|
|
49
|
+
if (!origins.some(saved => permissionFor(saved) === permission)) {
|
|
50
|
+
await api.permissions.remove({ origins: [permission] }).catch(() => false)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const forgetRemoved = async (permissions, api = chrome) => {
|
|
55
|
+
const removed = new Set(Array.isArray(permissions?.origins) ? permissions.origins : [])
|
|
56
|
+
if (!removed.size) return
|
|
57
|
+
const origins = await savedOrigins(api)
|
|
58
|
+
const remaining = origins.filter(origin => !removed.has(permissionFor(origin)))
|
|
59
|
+
if (remaining.length !== origins.length) await api.storage.local.set({ allowedOrigins: remaining })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
globalThis.XingularSiteAccess = Object.freeze({ originOf, permissionFor, allowed, grant, revoke, forgetRemoved })
|
|
63
|
+
})()
|