@withbuddi/buddi 0.1.0-pre.51 → 0.1.0-pre.52
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/extension/background.js +1604 -847
- package/extension/manifest.json +3 -3
- package/extension/popup.css +91 -3
- package/extension/popup.html +49 -32
- package/extension/popup.js +215 -123
- package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/core/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/core/dist/plugin/index.d.ts +1 -1
- package/node_modules/@buddi/core/dist/plugin/index.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/plugin/index.js.map +1 -1
- package/node_modules/@buddi/core/dist/questions/store.d.ts +3 -1
- package/node_modules/@buddi/core/dist/questions/store.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/questions/store.js +3 -3
- package/node_modules/@buddi/core/dist/questions/store.js.map +1 -1
- package/node_modules/@buddi/core/dist/questions/types.d.ts +46 -1
- package/node_modules/@buddi/core/dist/questions/types.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/questions/types.js +2 -1
- package/node_modules/@buddi/core/dist/questions/types.js.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/approval.d.ts +54 -0
- package/node_modules/@buddi/core/dist/secrets/approval.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/approval.js +58 -1
- package/node_modules/@buddi/core/dist/secrets/approval.js.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/settings.d.ts +178 -0
- package/node_modules/@buddi/core/dist/secrets/settings.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/settings.js +63 -6
- package/node_modules/@buddi/core/dist/secrets/settings.js.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/store.d.ts +31 -0
- package/node_modules/@buddi/core/dist/secrets/store.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/store.js +75 -0
- package/node_modules/@buddi/core/dist/secrets/store.js.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/use.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/secrets/use.js +68 -3
- package/node_modules/@buddi/core/dist/secrets/use.js.map +1 -1
- package/node_modules/@buddi/core/dist/tools.d.ts +22 -0
- package/node_modules/@buddi/core/dist/tools.d.ts.map +1 -1
- package/node_modules/@buddi/core/dist/tools.js.map +1 -1
- package/node_modules/@buddi/core/migrations/080_secret_requests.sql +14 -0
- package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/gateway/dist/chat/session.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/chat/session.js +2 -1
- package/node_modules/@buddi/gateway/dist/chat/session.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/missions/execute.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/missions/execute.js +3 -1
- package/node_modules/@buddi/gateway/dist/missions/execute.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.d.ts +15 -3
- package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.js +34 -7
- package/node_modules/@buddi/gateway/dist/surfaces/browser-cards.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/offered-actions.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/offered-actions.js +8 -2
- package/node_modules/@buddi/gateway/dist/surfaces/offered-actions.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/pending-question.d.ts +3 -1
- package/node_modules/@buddi/gateway/dist/surfaces/pending-question.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/pending-question.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/surfaces/secret-request.d.ts +173 -0
- package/node_modules/@buddi/gateway/dist/surfaces/secret-request.d.ts.map +1 -0
- package/node_modules/@buddi/gateway/dist/surfaces/secret-request.js +332 -0
- package/node_modules/@buddi/gateway/dist/surfaces/secret-request.js.map +1 -0
- package/node_modules/@buddi/gateway/dist/system-context.js +1 -1
- package/node_modules/@buddi/gateway/dist/system-context.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/telegram/main.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/telegram/main.js +14 -3
- package/node_modules/@buddi/gateway/dist/telegram/main.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/telegram/surface.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/telegram/surface.js +4 -1
- package/node_modules/@buddi/gateway/dist/telegram/surface.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/api-routes.js +9 -1
- package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/approval-ask.js +2 -2
- package/node_modules/@buddi/gateway/dist/web/approval-ask.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/browser-capture.d.ts +13 -0
- package/node_modules/@buddi/gateway/dist/web/browser-capture.d.ts.map +1 -0
- package/node_modules/@buddi/gateway/dist/web/browser-capture.js +24 -0
- package/node_modules/@buddi/gateway/dist/web/browser-capture.js.map +1 -0
- package/node_modules/@buddi/gateway/dist/web/chat.d.ts +26 -0
- package/node_modules/@buddi/gateway/dist/web/chat.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/chat.js +87 -2
- package/node_modules/@buddi/gateway/dist/web/chat.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/extension.d.ts +30 -0
- package/node_modules/@buddi/gateway/dist/web/extension.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/extension.js +86 -6
- package/node_modules/@buddi/gateway/dist/web/extension.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/onboarding.js +1 -1
- package/node_modules/@buddi/gateway/dist/web/onboarding.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts +26 -2
- package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/remote-hand.js +89 -28
- package/node_modules/@buddi/gateway/dist/web/remote-hand.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/secret-request.d.ts +54 -0
- package/node_modules/@buddi/gateway/dist/web/secret-request.d.ts.map +1 -0
- package/node_modules/@buddi/gateway/dist/web/secret-request.js +264 -0
- package/node_modules/@buddi/gateway/dist/web/secret-request.js.map +1 -0
- package/node_modules/@buddi/gateway/dist/web/secrets.d.ts +1 -0
- package/node_modules/@buddi/gateway/dist/web/secrets.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/secrets.js +1 -1
- package/node_modules/@buddi/gateway/dist/web/secrets.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/server.js +72 -3
- package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
- package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/runtime/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/runtime/dist/anthropic.d.ts +7 -0
- package/node_modules/@buddi/runtime/dist/anthropic.d.ts.map +1 -1
- package/node_modules/@buddi/runtime/dist/anthropic.js +19 -1
- package/node_modules/@buddi/runtime/dist/anthropic.js.map +1 -1
- package/node_modules/@buddi/tool-artifacts/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-browser/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-browser/dist/controller.d.ts +16 -1
- package/node_modules/@buddi/tool-browser/dist/controller.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/controller.js +59 -5
- package/node_modules/@buddi/tool-browser/dist/controller.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/driver.d.ts +12 -0
- package/node_modules/@buddi/tool-browser/dist/driver.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/driver.js +41 -2
- package/node_modules/@buddi/tool-browser/dist/driver.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/extension.d.ts +49 -9
- package/node_modules/@buddi/tool-browser/dist/extension.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/extension.js +145 -21
- package/node_modules/@buddi/tool-browser/dist/extension.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/index.d.ts +10 -6
- package/node_modules/@buddi/tool-browser/dist/index.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/index.js +22 -8
- package/node_modules/@buddi/tool-browser/dist/index.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/routes.d.ts +10 -1
- package/node_modules/@buddi/tool-browser/dist/routes.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/routes.js +24 -0
- package/node_modules/@buddi/tool-browser/dist/routes.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/service.d.ts +48 -1
- package/node_modules/@buddi/tool-browser/dist/service.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/service.js +114 -11
- package/node_modules/@buddi/tool-browser/dist/service.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/types.d.ts +54 -3
- package/node_modules/@buddi/tool-browser/dist/types.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/types.js +17 -1
- package/node_modules/@buddi/tool-browser/dist/types.js.map +1 -1
- package/node_modules/@buddi/tool-email/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-host/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-mcp/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-memory/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-web/dist/.tsbuildinfo +1 -1
- package/node_modules/imapflow/CHANGELOG.md +15 -0
- package/node_modules/imapflow/dist/cjs/commands/select.d.ts +3 -14
- package/node_modules/imapflow/dist/cjs/commands/select.js +1 -4
- package/node_modules/imapflow/dist/cjs/imap-flow.js +30 -6
- package/node_modules/imapflow/dist/cjs/package-info.d.ts +1 -1
- package/node_modules/imapflow/dist/cjs/package-info.js +1 -1
- package/node_modules/imapflow/dist/cjs/tools.d.ts +10 -0
- package/node_modules/imapflow/dist/cjs/tools.js +29 -0
- package/node_modules/imapflow/dist/cjs/types.d.ts +9 -1
- package/node_modules/imapflow/dist/esm/commands/select.d.ts +3 -14
- package/node_modules/imapflow/dist/esm/commands/select.js +1 -4
- package/node_modules/imapflow/dist/esm/imap-flow.js +31 -7
- package/node_modules/imapflow/dist/esm/package-info.d.ts +1 -1
- package/node_modules/imapflow/dist/esm/package-info.js +1 -1
- package/node_modules/imapflow/dist/esm/tools.d.ts +10 -0
- package/node_modules/imapflow/dist/esm/tools.js +28 -0
- package/node_modules/imapflow/dist/esm/types.d.ts +9 -1
- package/node_modules/imapflow/package.json +2 -2
- package/node_modules/playwright/README.md +4 -4
- package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +1 -1
- package/node_modules/playwright/lib/cli/testActions.js +8 -5
- package/node_modules/playwright/lib/common/index.js +37 -23
- package/node_modules/playwright/lib/common/index.js.txt +8 -9
- package/node_modules/playwright/lib/index.js +16 -4
- package/node_modules/playwright/lib/matchers/expect.js +13 -7
- package/node_modules/playwright/lib/matchers/expect.js.txt +4 -4
- package/node_modules/playwright/lib/program.js +2 -1
- package/node_modules/playwright/lib/runner/index.js +438 -244
- package/node_modules/playwright/lib/runner/index.js.txt +12 -11
- package/node_modules/playwright/lib/transform/babelBundle.js +2612 -2396
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +15 -15
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +14 -11
- package/node_modules/playwright/lib/transform/esmLoader.js +17 -5
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +3 -3
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +17 -18
- package/node_modules/playwright/package.json +2 -7
- package/node_modules/playwright/types/test.d.ts +156 -39
- package/node_modules/playwright-core/browsers.json +9 -9
- package/node_modules/playwright-core/cli.js +1 -0
- package/node_modules/playwright-core/lib/bootstrap.js +61 -0
- package/node_modules/playwright-core/lib/coreBundle.js +6393 -4170
- package/node_modules/playwright-core/lib/server/electron/loader.js +4 -0
- package/node_modules/playwright-core/lib/tools/cli-client/cli.js +2 -1
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +99 -12
- package/node_modules/playwright-core/lib/tools/cli-client/output.js +17 -16
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +8 -4
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +2 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +71 -3
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/pr-attachments.md +60 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +4 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +2 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +1 -1
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +76 -3
- package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +1 -1
- package/node_modules/playwright-core/lib/utilsBundle.js +948 -227
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +6 -6
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +1 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CVQWJZAA.js → codeMirrorModule-DK72OMRi.js} +1 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-Cn0mmDuv.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-BbkfBe3n.js → codeMirrorModule-BEB2e8M3.js} +1 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-D-g2vrvf.js +184 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/{xtermModule-DywYcAf8.js → xtermModule-B794rD2Z.js} +1 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/{defaultSettingsView.Bqk9acqE.css → defaultSettingsView.C8BeHuxd.css} +1 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/{index.B4cLoZK3.js → index.7-c5LSTW.js} +1 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +3 -3
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -3
- package/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.CU5KtEkS.js → uiMode.CYwC_gH1.js} +4 -4
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +3 -3
- package/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright-core/types/protocol.d.ts +468 -337
- package/node_modules/playwright-core/types/types.d.ts +572 -37
- package/package.json +2 -2
- package/packages/cli/dist/.tsbuildinfo +1 -1
- package/packages/core/dist/.tsbuildinfo +1 -1
- package/packages/core/dist/plugin/index.d.ts +1 -1
- package/packages/core/dist/plugin/index.d.ts.map +1 -1
- package/packages/core/dist/plugin/index.js.map +1 -1
- package/packages/core/dist/questions/store.d.ts +3 -1
- package/packages/core/dist/questions/store.d.ts.map +1 -1
- package/packages/core/dist/questions/store.js +3 -3
- package/packages/core/dist/questions/store.js.map +1 -1
- package/packages/core/dist/questions/types.d.ts +46 -1
- package/packages/core/dist/questions/types.d.ts.map +1 -1
- package/packages/core/dist/questions/types.js +2 -1
- package/packages/core/dist/questions/types.js.map +1 -1
- package/packages/core/dist/secrets/approval.d.ts +54 -0
- package/packages/core/dist/secrets/approval.d.ts.map +1 -1
- package/packages/core/dist/secrets/approval.js +58 -1
- package/packages/core/dist/secrets/approval.js.map +1 -1
- package/packages/core/dist/secrets/settings.d.ts +178 -0
- package/packages/core/dist/secrets/settings.d.ts.map +1 -1
- package/packages/core/dist/secrets/settings.js +63 -6
- package/packages/core/dist/secrets/settings.js.map +1 -1
- package/packages/core/dist/secrets/store.d.ts +31 -0
- package/packages/core/dist/secrets/store.d.ts.map +1 -1
- package/packages/core/dist/secrets/store.js +75 -0
- package/packages/core/dist/secrets/store.js.map +1 -1
- package/packages/core/dist/secrets/use.d.ts.map +1 -1
- package/packages/core/dist/secrets/use.js +68 -3
- package/packages/core/dist/secrets/use.js.map +1 -1
- package/packages/core/dist/tools.d.ts +22 -0
- package/packages/core/dist/tools.d.ts.map +1 -1
- package/packages/core/dist/tools.js.map +1 -1
- package/packages/core/migrations/080_secret_requests.sql +14 -0
- package/packages/gateway/dist/.tsbuildinfo +1 -1
- package/packages/gateway/dist/chat/session.d.ts.map +1 -1
- package/packages/gateway/dist/chat/session.js +2 -1
- package/packages/gateway/dist/chat/session.js.map +1 -1
- package/packages/gateway/dist/missions/execute.d.ts.map +1 -1
- package/packages/gateway/dist/missions/execute.js +3 -1
- package/packages/gateway/dist/missions/execute.js.map +1 -1
- package/packages/gateway/dist/surfaces/browser-cards.d.ts +15 -3
- package/packages/gateway/dist/surfaces/browser-cards.d.ts.map +1 -1
- package/packages/gateway/dist/surfaces/browser-cards.js +34 -7
- package/packages/gateway/dist/surfaces/browser-cards.js.map +1 -1
- package/packages/gateway/dist/surfaces/offered-actions.d.ts.map +1 -1
- package/packages/gateway/dist/surfaces/offered-actions.js +8 -2
- package/packages/gateway/dist/surfaces/offered-actions.js.map +1 -1
- package/packages/gateway/dist/surfaces/pending-question.d.ts +3 -1
- package/packages/gateway/dist/surfaces/pending-question.d.ts.map +1 -1
- package/packages/gateway/dist/surfaces/pending-question.js.map +1 -1
- package/packages/gateway/dist/surfaces/secret-request.d.ts +173 -0
- package/packages/gateway/dist/surfaces/secret-request.d.ts.map +1 -0
- package/packages/gateway/dist/surfaces/secret-request.js +332 -0
- package/packages/gateway/dist/surfaces/secret-request.js.map +1 -0
- package/packages/gateway/dist/system-context.js +1 -1
- package/packages/gateway/dist/system-context.js.map +1 -1
- package/packages/gateway/dist/telegram/main.d.ts.map +1 -1
- package/packages/gateway/dist/telegram/main.js +14 -3
- package/packages/gateway/dist/telegram/main.js.map +1 -1
- package/packages/gateway/dist/telegram/surface.d.ts.map +1 -1
- package/packages/gateway/dist/telegram/surface.js +4 -1
- package/packages/gateway/dist/telegram/surface.js.map +1 -1
- package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
- package/packages/gateway/dist/web/api-routes.js +9 -1
- package/packages/gateway/dist/web/api-routes.js.map +1 -1
- package/packages/gateway/dist/web/approval-ask.js +2 -2
- package/packages/gateway/dist/web/approval-ask.js.map +1 -1
- package/packages/gateway/dist/web/browser-capture.d.ts +13 -0
- package/packages/gateway/dist/web/browser-capture.d.ts.map +1 -0
- package/packages/gateway/dist/web/browser-capture.js +24 -0
- package/packages/gateway/dist/web/browser-capture.js.map +1 -0
- package/packages/gateway/dist/web/chat.d.ts +26 -0
- package/packages/gateway/dist/web/chat.d.ts.map +1 -1
- package/packages/gateway/dist/web/chat.js +87 -2
- package/packages/gateway/dist/web/chat.js.map +1 -1
- package/packages/gateway/dist/web/extension.d.ts +30 -0
- package/packages/gateway/dist/web/extension.d.ts.map +1 -1
- package/packages/gateway/dist/web/extension.js +86 -6
- package/packages/gateway/dist/web/extension.js.map +1 -1
- package/packages/gateway/dist/web/onboarding.js +1 -1
- package/packages/gateway/dist/web/onboarding.js.map +1 -1
- package/packages/gateway/dist/web/remote-hand.d.ts +26 -2
- package/packages/gateway/dist/web/remote-hand.d.ts.map +1 -1
- package/packages/gateway/dist/web/remote-hand.js +89 -28
- package/packages/gateway/dist/web/remote-hand.js.map +1 -1
- package/packages/gateway/dist/web/secret-request.d.ts +54 -0
- package/packages/gateway/dist/web/secret-request.d.ts.map +1 -0
- package/packages/gateway/dist/web/secret-request.js +264 -0
- package/packages/gateway/dist/web/secret-request.js.map +1 -0
- package/packages/gateway/dist/web/secrets.d.ts +1 -0
- package/packages/gateway/dist/web/secrets.d.ts.map +1 -1
- package/packages/gateway/dist/web/secrets.js +1 -1
- package/packages/gateway/dist/web/secrets.js.map +1 -1
- package/packages/gateway/dist/web/server.d.ts.map +1 -1
- package/packages/gateway/dist/web/server.js +72 -3
- package/packages/gateway/dist/web/server.js.map +1 -1
- package/packages/install/dist/.tsbuildinfo +1 -1
- package/packages/runtime/dist/.tsbuildinfo +1 -1
- package/packages/runtime/dist/anthropic.d.ts +7 -0
- package/packages/runtime/dist/anthropic.d.ts.map +1 -1
- package/packages/runtime/dist/anthropic.js +19 -1
- package/packages/runtime/dist/anthropic.js.map +1 -1
- package/packages/tools/artifacts/dist/.tsbuildinfo +1 -1
- package/packages/tools/browser/dist/.tsbuildinfo +1 -1
- package/packages/tools/browser/dist/controller.d.ts +16 -1
- package/packages/tools/browser/dist/controller.d.ts.map +1 -1
- package/packages/tools/browser/dist/controller.js +59 -5
- package/packages/tools/browser/dist/controller.js.map +1 -1
- package/packages/tools/browser/dist/driver.d.ts +12 -0
- package/packages/tools/browser/dist/driver.d.ts.map +1 -1
- package/packages/tools/browser/dist/driver.js +41 -2
- package/packages/tools/browser/dist/driver.js.map +1 -1
- package/packages/tools/browser/dist/extension.d.ts +49 -9
- package/packages/tools/browser/dist/extension.d.ts.map +1 -1
- package/packages/tools/browser/dist/extension.js +145 -21
- package/packages/tools/browser/dist/extension.js.map +1 -1
- package/packages/tools/browser/dist/index.d.ts +10 -6
- package/packages/tools/browser/dist/index.d.ts.map +1 -1
- package/packages/tools/browser/dist/index.js +22 -8
- package/packages/tools/browser/dist/index.js.map +1 -1
- package/packages/tools/browser/dist/routes.d.ts +10 -1
- package/packages/tools/browser/dist/routes.d.ts.map +1 -1
- package/packages/tools/browser/dist/routes.js +24 -0
- package/packages/tools/browser/dist/routes.js.map +1 -1
- package/packages/tools/browser/dist/service.d.ts +48 -1
- package/packages/tools/browser/dist/service.d.ts.map +1 -1
- package/packages/tools/browser/dist/service.js +114 -11
- package/packages/tools/browser/dist/service.js.map +1 -1
- package/packages/tools/browser/dist/types.d.ts +54 -3
- package/packages/tools/browser/dist/types.d.ts.map +1 -1
- package/packages/tools/browser/dist/types.js +17 -1
- package/packages/tools/browser/dist/types.js.map +1 -1
- package/packages/tools/email/dist/.tsbuildinfo +1 -1
- package/packages/tools/host/dist/.tsbuildinfo +1 -1
- package/packages/tools/mcp/dist/.tsbuildinfo +1 -1
- package/packages/tools/memory/dist/.tsbuildinfo +1 -1
- package/packages/tools/web/dist/.tsbuildinfo +1 -1
- package/packages/web/dist/assets/index-oxmxR39o.js +155 -0
- package/packages/web/dist/assets/index-rMRjibNI.css +1 -0
- package/packages/web/dist/assets/{lottie_light-D7CS-IeP.js → lottie_light-DROBTM6Y.js} +1 -1
- package/packages/web/dist/build.json +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/precache.json +4 -4
- package/node_modules/playwright/jsx-runtime.js +0 -42
- package/node_modules/playwright/jsx-runtime.mjs +0 -21
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +0 -129
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +0 -182
- package/packages/web/dist/assets/index-BibqUZwt.js +0 -155
- package/packages/web/dist/assets/index-DBVZzTau.css +0 -1
package/extension/background.js
CHANGED
|
@@ -1,6 +1,670 @@
|
|
|
1
|
+
// src/downloads.ts
|
|
2
|
+
var DOWNLOAD_GRACE_MS = 15e3;
|
|
3
|
+
var DOWNLOADS_PERMISSION = "downloads";
|
|
4
|
+
function originOf(url) {
|
|
5
|
+
if (!url) return void 0;
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(url);
|
|
8
|
+
if (parsed.protocol === "blob:") return new URL(parsed.pathname).origin;
|
|
9
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.origin : void 0;
|
|
10
|
+
} catch {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function basename(file) {
|
|
15
|
+
return file.split(/[\\/]/).pop() || "download";
|
|
16
|
+
}
|
|
17
|
+
var AgentDownloads = class {
|
|
18
|
+
#actions = /* @__PURE__ */ new Map();
|
|
19
|
+
#next = 1;
|
|
20
|
+
#claimed = /* @__PURE__ */ new Map();
|
|
21
|
+
#api;
|
|
22
|
+
#send;
|
|
23
|
+
#now;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.#send = options.send;
|
|
26
|
+
this.#now = options.now ?? (() => Date.now());
|
|
27
|
+
}
|
|
28
|
+
/** Chrome's downloads API, once the owner granted it. Listening starts here, once. */
|
|
29
|
+
attach(api) {
|
|
30
|
+
if (!api || this.#api) return;
|
|
31
|
+
this.#api = api;
|
|
32
|
+
api.onCreated.addListener((item) => this.created(item));
|
|
33
|
+
api.onChanged.addListener((delta) => {
|
|
34
|
+
void this.changed(delta).catch(() => void 0);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
get attached() {
|
|
38
|
+
return this.#api !== void 0;
|
|
39
|
+
}
|
|
40
|
+
/** An agent command starts in this session, whose tabs are on these addresses. Returns the command's handle for `end`. */
|
|
41
|
+
begin(session, urls) {
|
|
42
|
+
this.#expire();
|
|
43
|
+
const id = this.#next++;
|
|
44
|
+
const origins = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const url of urls) {
|
|
46
|
+
const origin = originOf(url);
|
|
47
|
+
if (origin) origins.add(origin);
|
|
48
|
+
}
|
|
49
|
+
this.#actions.set(id, { session, origins, running: true, until: 0, at: this.#now() });
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
/** That command ended; its tabs may have moved on to these. Its downloads still count for a few seconds, its own. */
|
|
53
|
+
end(id, urls = []) {
|
|
54
|
+
const action = this.#actions.get(id);
|
|
55
|
+
if (!action) return;
|
|
56
|
+
for (const url of urls) {
|
|
57
|
+
const origin = originOf(url);
|
|
58
|
+
if (origin) action.origins.add(origin);
|
|
59
|
+
}
|
|
60
|
+
action.running = false;
|
|
61
|
+
action.until = this.#now() + DOWNLOAD_GRACE_MS;
|
|
62
|
+
}
|
|
63
|
+
/** Commands whose few seconds are over count for nothing any more. */
|
|
64
|
+
#expire() {
|
|
65
|
+
const now = this.#now();
|
|
66
|
+
for (const [id, action] of this.#actions) if (!action.running && now > action.until) this.#actions.delete(id);
|
|
67
|
+
}
|
|
68
|
+
/** The session ended or the socket went: nothing more is its. */
|
|
69
|
+
forget(session) {
|
|
70
|
+
if (session === void 0) {
|
|
71
|
+
this.#actions.clear();
|
|
72
|
+
this.#claimed.clear();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const [id, action] of this.#actions) if (action.session === session) this.#actions.delete(id);
|
|
76
|
+
for (const [id, owner] of this.#claimed) if (owner === session) this.#claimed.delete(id);
|
|
77
|
+
}
|
|
78
|
+
/** Which session's command this download belongs to, if any. */
|
|
79
|
+
#owner(item) {
|
|
80
|
+
if (item.byExtensionId) return void 0;
|
|
81
|
+
const origin = originOf(item.referrer) ?? originOf(item.finalUrl ?? item.url);
|
|
82
|
+
if (!origin) return void 0;
|
|
83
|
+
this.#expire();
|
|
84
|
+
let best;
|
|
85
|
+
for (const action of this.#actions.values()) {
|
|
86
|
+
if (!action.origins.has(origin)) continue;
|
|
87
|
+
if (!best || action.at > best.at) best = action;
|
|
88
|
+
}
|
|
89
|
+
return best?.session;
|
|
90
|
+
}
|
|
91
|
+
created(item) {
|
|
92
|
+
const session = this.#owner(item);
|
|
93
|
+
if (session) this.#claimed.set(item.id, session);
|
|
94
|
+
}
|
|
95
|
+
async changed(delta) {
|
|
96
|
+
const session = this.#claimed.get(delta.id);
|
|
97
|
+
const state = delta.state?.current;
|
|
98
|
+
if (!session || !state) return;
|
|
99
|
+
if (state === "interrupted") {
|
|
100
|
+
this.#claimed.delete(delta.id);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (state !== "complete") return;
|
|
104
|
+
this.#claimed.delete(delta.id);
|
|
105
|
+
const [item] = await (this.#api?.search({ id: delta.id }) ?? Promise.resolve([]));
|
|
106
|
+
if (!item?.filename) return;
|
|
107
|
+
const url = item.finalUrl || item.url || "";
|
|
108
|
+
const size = item.fileSize !== void 0 && item.fileSize >= 0 ? item.fileSize : item.totalBytes !== void 0 && item.totalBytes >= 0 ? item.totalBytes : void 0;
|
|
109
|
+
this.#send({
|
|
110
|
+
type: "download",
|
|
111
|
+
session,
|
|
112
|
+
path: item.filename,
|
|
113
|
+
filename: basename(item.filename),
|
|
114
|
+
url,
|
|
115
|
+
...item.mime ? { mime: item.mime } : {},
|
|
116
|
+
...size !== void 0 ? { size } : {}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/bar.ts
|
|
122
|
+
var OWNER_WAIT_MS = 3e4;
|
|
123
|
+
var BAR_AFTER_MS = 3e3;
|
|
124
|
+
var BAR_POLL_MS = 500;
|
|
125
|
+
async function waitForOwner(probes, limits = {}) {
|
|
126
|
+
const waitMs = limits.waitMs ?? OWNER_WAIT_MS;
|
|
127
|
+
const barAfter = limits.barAfterMs ?? BAR_AFTER_MS;
|
|
128
|
+
const poll = limits.pollMs ?? BAR_POLL_MS;
|
|
129
|
+
const start = probes.now();
|
|
130
|
+
let shown = false;
|
|
131
|
+
const maxPolls = Math.ceil(waitMs / poll);
|
|
132
|
+
try {
|
|
133
|
+
for (let polls = 0; ; polls++) {
|
|
134
|
+
if (!await probes.watched()) return "left";
|
|
135
|
+
const elapsed = Math.max(probes.now() - start, polls * poll);
|
|
136
|
+
if (!shown && elapsed >= barAfter) {
|
|
137
|
+
shown = true;
|
|
138
|
+
await probes.show().catch(() => void 0);
|
|
139
|
+
}
|
|
140
|
+
if (shown) {
|
|
141
|
+
const choice = await probes.choice().catch(() => void 0);
|
|
142
|
+
if (choice) return choice;
|
|
143
|
+
}
|
|
144
|
+
if (elapsed >= waitMs || polls >= maxPolls) return "timeout";
|
|
145
|
+
await probes.wait(poll);
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
if (shown) await probes.hide().catch(() => void 0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function showBar(agent) {
|
|
152
|
+
const holder = globalThis;
|
|
153
|
+
holder["__buddiBar"] = { choice: void 0 };
|
|
154
|
+
const doc = globalThis.document;
|
|
155
|
+
if (!doc || doc.getElementById("buddi-working-bar")) return;
|
|
156
|
+
const host = doc.createElement("div");
|
|
157
|
+
host.id = "buddi-working-bar";
|
|
158
|
+
host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
|
|
159
|
+
const root = host.attachShadow({ mode: "closed" });
|
|
160
|
+
const bar = doc.createElement("div");
|
|
161
|
+
bar.setAttribute("role", "status");
|
|
162
|
+
bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
|
|
163
|
+
const label = doc.createElement("span");
|
|
164
|
+
label.textContent = `${agent || "buddi"} is working here`;
|
|
165
|
+
label.setAttribute("style", "margin-right:auto;");
|
|
166
|
+
const button = (text2, choice, primary) => {
|
|
167
|
+
const b = doc.createElement("button");
|
|
168
|
+
b.type = "button";
|
|
169
|
+
b.textContent = text2;
|
|
170
|
+
b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
|
|
171
|
+
b.addEventListener("click", () => {
|
|
172
|
+
holder["__buddiBar"] = { choice };
|
|
173
|
+
});
|
|
174
|
+
return b;
|
|
175
|
+
};
|
|
176
|
+
bar.append(label, button("Take over", "takeover", false), button("Let it continue", "continue", true));
|
|
177
|
+
root.append(bar);
|
|
178
|
+
(doc.documentElement ?? doc.body).append(host);
|
|
179
|
+
}
|
|
180
|
+
function readBar() {
|
|
181
|
+
const holder = globalThis;
|
|
182
|
+
const choice = holder["__buddiBar"]?.choice;
|
|
183
|
+
if (choice) holder["__buddiBar"] = { choice: void 0 };
|
|
184
|
+
return choice;
|
|
185
|
+
}
|
|
186
|
+
function hideBar() {
|
|
187
|
+
const doc = globalThis.document;
|
|
188
|
+
doc?.getElementById("buddi-working-bar")?.remove();
|
|
189
|
+
const holder = globalThis;
|
|
190
|
+
delete holder["__buddiBar"];
|
|
191
|
+
}
|
|
192
|
+
var GIVE_BACK_MESSAGE = "buddi-give-back";
|
|
193
|
+
function showHeldBar(session) {
|
|
194
|
+
const doc = globalThis.document;
|
|
195
|
+
if (!doc || doc.getElementById("buddi-held-bar")) return;
|
|
196
|
+
const host = doc.createElement("div");
|
|
197
|
+
host.id = "buddi-held-bar";
|
|
198
|
+
host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
|
|
199
|
+
const root = host.attachShadow({ mode: "closed" });
|
|
200
|
+
const bar = doc.createElement("div");
|
|
201
|
+
bar.setAttribute("role", "status");
|
|
202
|
+
bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
|
|
203
|
+
const label = doc.createElement("span");
|
|
204
|
+
label.textContent = "buddi is waiting";
|
|
205
|
+
label.setAttribute("style", "margin-right:auto;");
|
|
206
|
+
const give = doc.createElement("button");
|
|
207
|
+
give.type = "button";
|
|
208
|
+
give.textContent = "Give it back";
|
|
209
|
+
give.setAttribute("style", "font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;background:#fff;color:#1f2a44;");
|
|
210
|
+
give.addEventListener("click", () => {
|
|
211
|
+
const runtime = globalThis.chrome?.runtime;
|
|
212
|
+
void runtime?.sendMessage({ type: "buddi-give-back", session })?.catch?.(() => void 0);
|
|
213
|
+
host.remove();
|
|
214
|
+
});
|
|
215
|
+
bar.append(label, give);
|
|
216
|
+
root.append(bar);
|
|
217
|
+
(doc.documentElement ?? doc.body).append(host);
|
|
218
|
+
const asking = [];
|
|
219
|
+
const restore = () => {
|
|
220
|
+
for (const button of asking.splice(0)) button.remove();
|
|
221
|
+
label.textContent = "buddi is waiting";
|
|
222
|
+
give.style.display = "";
|
|
223
|
+
};
|
|
224
|
+
globalThis["__buddiHeldAsk"] = (question, choices) => {
|
|
225
|
+
restore();
|
|
226
|
+
label.textContent = question;
|
|
227
|
+
give.style.display = "none";
|
|
228
|
+
for (const choice of choices) {
|
|
229
|
+
const b = doc.createElement("button");
|
|
230
|
+
b.type = "button";
|
|
231
|
+
b.textContent = choice.label;
|
|
232
|
+
b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${choice.primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
|
|
233
|
+
b.addEventListener("click", () => {
|
|
234
|
+
restore();
|
|
235
|
+
choice.pick();
|
|
236
|
+
});
|
|
237
|
+
asking.push(b);
|
|
238
|
+
bar.insertBefore(b, give);
|
|
239
|
+
}
|
|
240
|
+
return restore;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function hideHeldBar() {
|
|
244
|
+
const doc = globalThis.document;
|
|
245
|
+
doc?.getElementById("buddi-held-bar")?.remove();
|
|
246
|
+
delete globalThis["__buddiHeldAsk"];
|
|
247
|
+
}
|
|
248
|
+
var LOGIN_MESSAGE = "buddi-login";
|
|
249
|
+
var LOGIN_PROMPT_MS = 2 * 6e4;
|
|
250
|
+
function watchHeldLogins(session, facts) {
|
|
251
|
+
const holder = globalThis;
|
|
252
|
+
const known = { never: [...facts?.never ?? []], saved: [...facts?.saved ?? []] };
|
|
253
|
+
const existing = holder["__buddiHeldLogins"];
|
|
254
|
+
if (existing) {
|
|
255
|
+
existing.session = session;
|
|
256
|
+
existing.facts = known;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const doc = globalThis.document;
|
|
260
|
+
if (!doc) return;
|
|
261
|
+
const state = { session, facts: known, last: "", lastAt: 0, typedAt: 0, cached: void 0, timer: void 0, drop: void 0 };
|
|
262
|
+
holder["__buddiHeldLogins"] = state;
|
|
263
|
+
const runtime = () => globalThis.chrome?.runtime;
|
|
264
|
+
const call = async (message) => {
|
|
265
|
+
try {
|
|
266
|
+
return await runtime()?.sendMessage(message);
|
|
267
|
+
} catch {
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const site = () => (globalThis.location?.hostname ?? "").toLowerCase().replace(/^www\./, "");
|
|
272
|
+
const listed = (where) => state.facts.never.some((entry) => {
|
|
273
|
+
const bare = entry.toLowerCase().replace(/^www\./, "");
|
|
274
|
+
return where === bare || where.endsWith(`.${bare}`);
|
|
275
|
+
});
|
|
276
|
+
const bar = () => {
|
|
277
|
+
const question = holder["__buddiHeldAsk"];
|
|
278
|
+
return typeof question === "function" ? question : void 0;
|
|
279
|
+
};
|
|
280
|
+
const prompt = (where, update) => {
|
|
281
|
+
const question = bar();
|
|
282
|
+
if (!question) return;
|
|
283
|
+
if (state.drop) clearTimeout(state.drop);
|
|
284
|
+
let answered = false;
|
|
285
|
+
const save = () => {
|
|
286
|
+
if (answered) return;
|
|
287
|
+
answered = true;
|
|
288
|
+
const asking = bar();
|
|
289
|
+
asking?.(update ? "Updating\u2026" : "Saving\u2026", []);
|
|
290
|
+
void call({ type: "buddi-login", session: state.session, decision: "save" }).then((reply) => {
|
|
291
|
+
const answer = reply;
|
|
292
|
+
const now = bar();
|
|
293
|
+
if (!now) return;
|
|
294
|
+
if (answer?.saved === true) {
|
|
295
|
+
const restore2 = now(update ? `Updated the login for ${where}` : `Saved the login for ${where}`, []);
|
|
296
|
+
if (state.drop) clearTimeout(state.drop);
|
|
297
|
+
state.drop = setTimeout(restore2, 2e3);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : "buddi did not answer. Try again.";
|
|
301
|
+
now(reason, [
|
|
302
|
+
{ label: "Try again", primary: true, pick: () => {
|
|
303
|
+
answered = false;
|
|
304
|
+
save();
|
|
305
|
+
} },
|
|
306
|
+
{ label: "Not now", pick: () => {
|
|
307
|
+
void call({ type: "buddi-login", session: state.session, decision: "later" });
|
|
308
|
+
} }
|
|
309
|
+
]);
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
const restore = question(update ? `Update the login for ${where}?` : `Save this login for ${where}?`, [
|
|
313
|
+
{ label: update ? "Update" : "Save", primary: true, pick: save },
|
|
314
|
+
{ label: "Not now", pick: () => {
|
|
315
|
+
answered = true;
|
|
316
|
+
void call({ type: "buddi-login", session: state.session, decision: "later" });
|
|
317
|
+
} },
|
|
318
|
+
{ label: "Never for this site", pick: () => {
|
|
319
|
+
answered = true;
|
|
320
|
+
state.facts.never.push(where);
|
|
321
|
+
void call({ type: "buddi-login", session: state.session, decision: "never" });
|
|
322
|
+
} }
|
|
323
|
+
]);
|
|
324
|
+
state.drop = setTimeout(() => {
|
|
325
|
+
if (!answered) restore();
|
|
326
|
+
}, 2 * 6e4);
|
|
327
|
+
};
|
|
328
|
+
const ask = (found) => {
|
|
329
|
+
const where = site();
|
|
330
|
+
if (where === "" || listed(where) || !bar()) return;
|
|
331
|
+
const update = state.facts.saved.some((login) => login.site === where && login.username === found.username);
|
|
332
|
+
void call({ type: "buddi-login-seen", session: state.session, username: found.username, password: found.password, update }).then((reply) => {
|
|
333
|
+
const asked2 = reply;
|
|
334
|
+
if (holder["__buddiHeldLogins"] !== state || !asked2 || typeof asked2 !== "object") return;
|
|
335
|
+
prompt(where, asked2.update === true);
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
void call({ type: "buddi-login-pending", session: state.session }).then((reply) => {
|
|
339
|
+
const waiting = reply;
|
|
340
|
+
if (holder["__buddiHeldLogins"] !== state || !waiting || typeof waiting.site !== "string") return;
|
|
341
|
+
prompt(waiting.site, waiting.update === true);
|
|
342
|
+
});
|
|
343
|
+
const NOT_SUBMIT = /\b(show|hide|reveal|toggle|eye|forgot|reset|cancel|back|close|sign ?up|register|create)\b/i;
|
|
344
|
+
const USER = /user|email|login|account|ident|phone|mail/i;
|
|
345
|
+
const passwords = (root) => Array.from(root.querySelectorAll('input[type="password" i]'));
|
|
346
|
+
const usernameFor = (password) => {
|
|
347
|
+
const scope = password.form ?? doc;
|
|
348
|
+
const inputs = Array.from(scope.querySelectorAll("input")).filter((input) => ["text", "email", "tel", ""].includes((input.getAttribute("type") ?? "").toLowerCase()) && input.value.trim() !== "");
|
|
349
|
+
const marked = inputs.find((input) => (input.getAttribute("autocomplete") ?? "").toLowerCase().includes("username") || (input.getAttribute("type") ?? "").toLowerCase() === "email");
|
|
350
|
+
if (marked) return marked.value.trim();
|
|
351
|
+
const before = inputs.filter((input) => (input.compareDocumentPosition(password) & 4) !== 0);
|
|
352
|
+
const pick = before[before.length - 1] ?? inputs.find((input) => USER.test(`${input.name} ${input.id} ${input.getAttribute("autocomplete") ?? ""}`));
|
|
353
|
+
return pick ? pick.value.trim() : "";
|
|
354
|
+
};
|
|
355
|
+
const read = (scope) => {
|
|
356
|
+
const field = passwords(scope ?? doc).find((input) => input.value !== "");
|
|
357
|
+
return field ? { username: usernameFor(field), password: field.value } : void 0;
|
|
358
|
+
};
|
|
359
|
+
const send = (found) => {
|
|
360
|
+
if (holder["__buddiHeldLogins"] !== state) return;
|
|
361
|
+
if (!found || found.password === "") return;
|
|
362
|
+
const key = `${found.username}\0${found.password}`;
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
if (key === state.last && now - state.lastAt < 5e3) return;
|
|
365
|
+
state.last = key;
|
|
366
|
+
state.lastAt = now;
|
|
367
|
+
state.cached = void 0;
|
|
368
|
+
ask(found);
|
|
369
|
+
};
|
|
370
|
+
doc.addEventListener("submit", (event) => {
|
|
371
|
+
send(read(event.target) ?? state.cached);
|
|
372
|
+
}, true);
|
|
373
|
+
doc.addEventListener("keydown", (event) => {
|
|
374
|
+
if (event.key !== "Enter") return;
|
|
375
|
+
const target = event.target;
|
|
376
|
+
if (!target || target.tagName !== "INPUT") return;
|
|
377
|
+
send(read(target.form ?? doc));
|
|
378
|
+
}, true);
|
|
379
|
+
doc.addEventListener("click", (event) => {
|
|
380
|
+
const target = event.target;
|
|
381
|
+
const button = target?.closest?.('button, input[type="submit" i], input[type="image" i], input[type="button" i], [role="button"]');
|
|
382
|
+
if (!button) return;
|
|
383
|
+
const words = `${button.textContent ?? ""} ${button.getAttribute("aria-label") ?? ""} ${button.getAttribute("value") ?? ""} ${button.getAttribute("title") ?? ""}`;
|
|
384
|
+
if (NOT_SUBMIT.test(words)) return;
|
|
385
|
+
send(read(button.form ?? doc));
|
|
386
|
+
}, true);
|
|
387
|
+
doc.addEventListener("input", (event) => {
|
|
388
|
+
const target = event.target;
|
|
389
|
+
if ((target?.getAttribute?.("type") ?? "").toLowerCase() !== "password") return;
|
|
390
|
+
state.typedAt = Date.now();
|
|
391
|
+
state.cached = read(target.form ?? doc);
|
|
392
|
+
}, true);
|
|
393
|
+
const Observer = globalThis.PerformanceObserver;
|
|
394
|
+
if (typeof Observer === "function") {
|
|
395
|
+
try {
|
|
396
|
+
new Observer((list) => {
|
|
397
|
+
if (!state.cached || Date.now() - state.typedAt > 1e4) return;
|
|
398
|
+
const fetched = list.getEntries().some((entry) => ["fetch", "xmlhttprequest"].includes(entry.initiatorType));
|
|
399
|
+
if (!fetched) return;
|
|
400
|
+
if (state.timer) clearTimeout(state.timer);
|
|
401
|
+
state.timer = setTimeout(() => {
|
|
402
|
+
const still = passwords(doc).some((input) => input.isConnected && input.value !== "" && input.getClientRects().length > 0);
|
|
403
|
+
if (!still) send(state.cached);
|
|
404
|
+
}, 1500);
|
|
405
|
+
}).observe({ type: "resource", buffered: false });
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
var LOGIN_SEEN_MESSAGE = "buddi-login-seen";
|
|
411
|
+
var LOGIN_PENDING_MESSAGE = "buddi-login-pending";
|
|
412
|
+
function heldLoginRequest(message, sender, heldTab) {
|
|
413
|
+
const request = message;
|
|
414
|
+
if (!request || typeof request.session !== "string" || request.session === "") return void 0;
|
|
415
|
+
if (heldTab === void 0 || sender?.tab?.id !== heldTab) return void 0;
|
|
416
|
+
const session = request.session;
|
|
417
|
+
const url = sender.url ?? "";
|
|
418
|
+
if (request.type === LOGIN_SEEN_MESSAGE) {
|
|
419
|
+
if (typeof request.password !== "string" || request.password === "" || request.password.length > 1024) return void 0;
|
|
420
|
+
const username = typeof request.username === "string" ? request.username.slice(0, 200) : "";
|
|
421
|
+
return { kind: "seen", session, tabId: heldTab, url, username, password: request.password, update: request.update === true };
|
|
422
|
+
}
|
|
423
|
+
if (request.type === LOGIN_PENDING_MESSAGE) return { kind: "pending", session, tabId: heldTab, url };
|
|
424
|
+
if (request.type === LOGIN_MESSAGE && (request.decision === "save" || request.decision === "later" || request.decision === "never")) {
|
|
425
|
+
return { kind: "answer", session, tabId: heldTab, decision: request.decision };
|
|
426
|
+
}
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/pairings.ts
|
|
431
|
+
var COLOURS = ["blue", "purple", "green", "orange", "pink", "cyan", "red", "yellow", "grey"];
|
|
432
|
+
var DEFAULT_GATEWAY = "http://127.0.0.1:4317";
|
|
433
|
+
var PAIRINGS_KEY = "pairings";
|
|
434
|
+
var LEGACY_GATEWAY = "gateway";
|
|
435
|
+
var LEGACY_TOKEN = "token";
|
|
436
|
+
var LOOPBACK = ["127.0.0.1", "localhost", "[::1]", "::1"];
|
|
437
|
+
function originOf2(address) {
|
|
438
|
+
let parsed;
|
|
439
|
+
try {
|
|
440
|
+
parsed = new URL(address.trim());
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
445
|
+
if (!LOOPBACK.includes(parsed.hostname)) return null;
|
|
446
|
+
return parsed.origin;
|
|
447
|
+
}
|
|
448
|
+
function portOf(url) {
|
|
449
|
+
return url.port || (url.protocol === "https:" ? "443" : "80");
|
|
450
|
+
}
|
|
451
|
+
function sameBuddi(a, b) {
|
|
452
|
+
let x, y;
|
|
453
|
+
try {
|
|
454
|
+
x = new URL(a);
|
|
455
|
+
y = new URL(b);
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
if (!LOOPBACK.includes(x.hostname) || !LOOPBACK.includes(y.hostname)) return false;
|
|
460
|
+
return x.protocol === y.protocol && portOf(x) === portOf(y);
|
|
461
|
+
}
|
|
462
|
+
function hostOf(origin) {
|
|
463
|
+
try {
|
|
464
|
+
return new URL(origin).host;
|
|
465
|
+
} catch {
|
|
466
|
+
return origin;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function displayName(entry, all) {
|
|
470
|
+
const own = entry.name.trim();
|
|
471
|
+
if (!own) return hostOf(entry.origin);
|
|
472
|
+
const twin = all.some((other) => other.id !== entry.id && other.name.trim().toLowerCase() === own.toLowerCase());
|
|
473
|
+
if (!twin) return own;
|
|
474
|
+
try {
|
|
475
|
+
return `${own} :${portOf(new URL(entry.origin))}`;
|
|
476
|
+
} catch {
|
|
477
|
+
return own;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function nextColour(all) {
|
|
481
|
+
const taken = new Set(all.map((entry) => entry.colour));
|
|
482
|
+
return COLOURS.find((colour) => !taken.has(colour)) ?? COLOURS[all.length % COLOURS.length];
|
|
483
|
+
}
|
|
484
|
+
function freshId() {
|
|
485
|
+
const bytes = new Uint8Array(8);
|
|
486
|
+
crypto.getRandomValues(bytes);
|
|
487
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
488
|
+
}
|
|
489
|
+
function readEntry(raw) {
|
|
490
|
+
if (!raw || typeof raw !== "object") return null;
|
|
491
|
+
const value = raw;
|
|
492
|
+
const origin = typeof value["origin"] === "string" ? originOf2(value["origin"]) : null;
|
|
493
|
+
if (!origin || typeof value["id"] !== "string" || value["id"] === "") return null;
|
|
494
|
+
const colour = COLOURS.includes(String(value["colour"])) ? value["colour"] : "grey";
|
|
495
|
+
const token = typeof value["token"] === "string" && value["token"] !== "" ? value["token"] : void 0;
|
|
496
|
+
return {
|
|
497
|
+
id: value["id"],
|
|
498
|
+
origin,
|
|
499
|
+
...token ? { token } : {},
|
|
500
|
+
name: typeof value["name"] === "string" ? value["name"].slice(0, 60) : "",
|
|
501
|
+
colour,
|
|
502
|
+
enabled: value["enabled"] !== false
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
var PairingStore = class {
|
|
506
|
+
#storage;
|
|
507
|
+
#tail = Promise.resolve();
|
|
508
|
+
#newId;
|
|
509
|
+
constructor(storage, options = {}) {
|
|
510
|
+
this.#storage = storage;
|
|
511
|
+
this.#newId = options.id ?? freshId;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Every entry, after the one-time move from the single pairing.
|
|
515
|
+
*
|
|
516
|
+
* An extension that held `gateway` and `token` keeps exactly that buddi, as
|
|
517
|
+
* the first entry, switched on. One that held nothing gets the default
|
|
518
|
+
* address, switched on and unpaired, so a fresh install still knocks on the
|
|
519
|
+
* buddi at 4317 by itself and the dashboard in this browser can pair it
|
|
520
|
+
* without the owner typing anything.
|
|
521
|
+
*/
|
|
522
|
+
list() {
|
|
523
|
+
return this.#queue(async () => this.#load());
|
|
524
|
+
}
|
|
525
|
+
async #load() {
|
|
526
|
+
const stored = await this.#storage.get([PAIRINGS_KEY, LEGACY_GATEWAY, LEGACY_TOKEN]);
|
|
527
|
+
const raw = stored[PAIRINGS_KEY];
|
|
528
|
+
if (Array.isArray(raw)) return raw.map(readEntry).filter((entry) => entry !== null);
|
|
529
|
+
const address = typeof stored[LEGACY_GATEWAY] === "string" ? originOf2(stored[LEGACY_GATEWAY]) : null;
|
|
530
|
+
const token = typeof stored[LEGACY_TOKEN] === "string" && stored[LEGACY_TOKEN] !== "" ? stored[LEGACY_TOKEN] : void 0;
|
|
531
|
+
const first = { id: this.#newId(), origin: address ?? DEFAULT_GATEWAY, ...token ? { token } : {}, name: "", colour: COLOURS[0], enabled: true };
|
|
532
|
+
await this.#storage.set({ [PAIRINGS_KEY]: [first] });
|
|
533
|
+
await this.#storage.remove([LEGACY_GATEWAY, LEGACY_TOKEN]);
|
|
534
|
+
return [first];
|
|
535
|
+
}
|
|
536
|
+
async #save(entries2) {
|
|
537
|
+
await this.#storage.set({ [PAIRINGS_KEY]: entries2 });
|
|
538
|
+
}
|
|
539
|
+
#queue(work) {
|
|
540
|
+
const next = this.#tail.then(work, work);
|
|
541
|
+
this.#tail = next.catch(() => void 0);
|
|
542
|
+
return next;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Add a buddi by its address, or hand back the entry that already has it
|
|
546
|
+
* (switched on again). Throws the sentence the popup shows for an address
|
|
547
|
+
* that is not on this machine.
|
|
548
|
+
*/
|
|
549
|
+
add(address) {
|
|
550
|
+
return this.#queue(async () => {
|
|
551
|
+
const origin = originOf2(address);
|
|
552
|
+
if (!origin) throw new Error(/^\s*https?:\/\//i.test(address) ? "A buddi address has to be on this machine." : "A buddi address starts with http.");
|
|
553
|
+
const entries2 = await this.#load();
|
|
554
|
+
const existing = entries2.find((entry2) => sameBuddi(entry2.origin, origin));
|
|
555
|
+
if (existing) {
|
|
556
|
+
existing.enabled = true;
|
|
557
|
+
await this.#save(entries2);
|
|
558
|
+
return existing;
|
|
559
|
+
}
|
|
560
|
+
const entry = { id: this.#newId(), origin, name: "", colour: nextColour(entries2), enabled: true };
|
|
561
|
+
entries2.push(entry);
|
|
562
|
+
await this.#save(entries2);
|
|
563
|
+
return entry;
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/** Change one entry in place; nothing happens to an entry that is gone. */
|
|
567
|
+
update(id, patch) {
|
|
568
|
+
return this.#queue(async () => {
|
|
569
|
+
const entries2 = await this.#load();
|
|
570
|
+
const entry = entries2.find((candidate) => candidate.id === id);
|
|
571
|
+
if (!entry) return void 0;
|
|
572
|
+
if ("token" in patch) {
|
|
573
|
+
if (patch.token) entry.token = patch.token;
|
|
574
|
+
else delete entry.token;
|
|
575
|
+
}
|
|
576
|
+
if (patch.name !== void 0) entry.name = patch.name.slice(0, 60);
|
|
577
|
+
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
578
|
+
await this.#save(entries2);
|
|
579
|
+
return entry;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/** Forget a buddi: its entry and its token go; its tabs stay, because they are the owner's. */
|
|
583
|
+
remove(id) {
|
|
584
|
+
return this.#queue(async () => {
|
|
585
|
+
const entries2 = await this.#load();
|
|
586
|
+
await this.#save(entries2.filter((entry) => entry.id !== id));
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/** The token seam `Protocol` reads and writes, for one entry. */
|
|
590
|
+
tokens(id) {
|
|
591
|
+
return {
|
|
592
|
+
read: async () => (await this.list()).find((entry) => entry.id === id)?.token ?? null,
|
|
593
|
+
write: async (token) => {
|
|
594
|
+
await this.update(id, { token });
|
|
595
|
+
},
|
|
596
|
+
clear: async () => {
|
|
597
|
+
await this.update(id, { token: void 0 });
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
var GroupRegistry = class {
|
|
603
|
+
#owners = /* @__PURE__ */ new Map();
|
|
604
|
+
/** Record that `instance` made (or re-joined) this group. */
|
|
605
|
+
claim(groupId, instance) {
|
|
606
|
+
if (groupId >= 0) this.#owners.set(groupId, instance);
|
|
607
|
+
}
|
|
608
|
+
/** Whose group this is, or undefined for a group no buddi made (the owner's own). */
|
|
609
|
+
ownerOf(groupId) {
|
|
610
|
+
return groupId === void 0 || groupId < 0 ? void 0 : this.#owners.get(groupId);
|
|
611
|
+
}
|
|
612
|
+
/** May `instance` act in a tab that sits in this group? Only in its own, or in no group at all. */
|
|
613
|
+
allows(groupId, instance) {
|
|
614
|
+
const owner = this.ownerOf(groupId);
|
|
615
|
+
return owner === void 0 || owner === instance;
|
|
616
|
+
}
|
|
617
|
+
/** A buddi was removed: its groups belong to nobody now (the tabs are the owner's). */
|
|
618
|
+
release(instance) {
|
|
619
|
+
for (const [groupId, owner] of this.#owners) if (owner === instance) this.#owners.delete(groupId);
|
|
620
|
+
}
|
|
621
|
+
groupsOf(instance) {
|
|
622
|
+
return [...this.#owners].filter(([, owner]) => owner === instance).map(([groupId]) => groupId);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/external.ts
|
|
627
|
+
function isLoopbackOrigin(origin) {
|
|
628
|
+
if (!origin) return false;
|
|
629
|
+
let parsed;
|
|
630
|
+
try {
|
|
631
|
+
parsed = new URL(origin);
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
636
|
+
return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname);
|
|
637
|
+
}
|
|
638
|
+
function named(state) {
|
|
639
|
+
return state.connection === "paired" ? "paired" : state.connection === "pairing" ? "pairing" : "disconnected";
|
|
640
|
+
}
|
|
641
|
+
function describe(state, gateway, version2) {
|
|
642
|
+
const status = { installed: true, version: version2, state: named(state), gateway };
|
|
643
|
+
if (status.state === "pairing" && state.code) status.code = state.code;
|
|
644
|
+
return status;
|
|
645
|
+
}
|
|
646
|
+
function describeFor(origin, known, version2) {
|
|
647
|
+
const mine = known.find((entry) => sameBuddi(entry.origin, origin));
|
|
648
|
+
const status = mine ? describe(mine.state, mine.origin, version2) : describe({ connection: "offline", code: null, installation: null, error: null }, known[0]?.origin ?? origin, version2);
|
|
649
|
+
status.pairings = known.map((entry) => ({ origin: entry.origin, name: entry.name, state: named(entry.state), enabled: entry.enabled }));
|
|
650
|
+
return status;
|
|
651
|
+
}
|
|
652
|
+
function handleExternal(message, context, respond) {
|
|
653
|
+
if (!isLoopbackOrigin(context.origin)) return false;
|
|
654
|
+
const request = message;
|
|
655
|
+
if (!request || request.type !== "buddi.status") return false;
|
|
656
|
+
const origin = context.origin;
|
|
657
|
+
void context.known().then((known) => {
|
|
658
|
+
if (!known.some((entry) => sameBuddi(entry.origin, origin))) context.unknown?.(origin);
|
|
659
|
+
respond(describeFor(origin, known, context.version));
|
|
660
|
+
});
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
1
664
|
// src/protocol.ts
|
|
2
665
|
var COMMAND_NAMES = ["navigate", "observe", "click", "fill", "select", "press", "scroll", "tab", "close", "fieldInfo", "secretFill"];
|
|
3
|
-
var HAND_COMMANDS = ["screencast.start", "screencast.stop", "input", "hold", "unhold"];
|
|
666
|
+
var HAND_COMMANDS = ["screencast.start", "screencast.stop", "input", "hold", "unhold", "capture", "copy"];
|
|
667
|
+
var EXTENSION_FEATURES = ["live"];
|
|
4
668
|
var PreconditionError = class extends Error {
|
|
5
669
|
};
|
|
6
670
|
var OpenedError = class extends Error {
|
|
@@ -33,6 +697,7 @@ var Cancellation = class {
|
|
|
33
697
|
};
|
|
34
698
|
var TOKEN_KEY = "token";
|
|
35
699
|
var BAD_PROOF = "That buddi could not prove it is the one this browser was paired with.";
|
|
700
|
+
var PAIR_AGAIN = "pair-again";
|
|
36
701
|
var encoder = new TextEncoder();
|
|
37
702
|
var hex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
38
703
|
async function sha256Hex(text2) {
|
|
@@ -85,24 +750,48 @@ var Protocol = class {
|
|
|
85
750
|
const stored = await this.#token();
|
|
86
751
|
this.#nonce = nonce();
|
|
87
752
|
this.#authenticated = false;
|
|
88
|
-
this.#set({ connection: "connecting", code: null, error: null });
|
|
89
|
-
this.#options.send({ type: "hello", extension: this.#options.version, nonce: this.#nonce, paired: stored !== null });
|
|
753
|
+
this.#set({ connection: "connecting", code: null, error: null, features: [], refused: false });
|
|
754
|
+
this.#options.send({ type: "hello", extension: this.#options.version, nonce: this.#nonce, paired: stored !== null, features: [...EXTENSION_FEATURES] });
|
|
90
755
|
}
|
|
91
|
-
closed(reason) {
|
|
756
|
+
closed(reason, options = {}) {
|
|
92
757
|
this.#authenticated = false;
|
|
93
758
|
for (const cancel of this.#running.values()) cancel.cancel();
|
|
94
759
|
this.#running.clear();
|
|
95
760
|
this.#options.onReset?.();
|
|
96
|
-
this.#set({ connection: "offline", code: null, error: reason ?? null });
|
|
761
|
+
this.#set({ connection: "offline", code: null, error: reason ?? null, refused: options.refused === true });
|
|
762
|
+
}
|
|
763
|
+
/** The token this browser held is no use at that buddi: forget it, so the next hello asks for a code. */
|
|
764
|
+
async dropToken() {
|
|
765
|
+
if (this.#options.tokens) await this.#options.tokens.clear();
|
|
766
|
+
else await this.#options.chrome?.storage.local.remove([TOKEN_KEY]);
|
|
97
767
|
}
|
|
98
768
|
async #token() {
|
|
99
|
-
|
|
769
|
+
if (this.#options.tokens) return this.#options.tokens.read();
|
|
770
|
+
const stored = await this.#options.chrome?.storage.local.get([TOKEN_KEY]) ?? {};
|
|
100
771
|
const token = stored[TOKEN_KEY];
|
|
101
772
|
return typeof token === "string" && token.length > 0 ? token : null;
|
|
102
773
|
}
|
|
774
|
+
async #keep(token) {
|
|
775
|
+
if (this.#options.tokens) {
|
|
776
|
+
await this.#options.tokens.write(token);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
await this.#options.chrome?.storage.local.set({ [TOKEN_KEY]: token });
|
|
780
|
+
}
|
|
781
|
+
/** What a handshake frame says about the buddi: its name and what it reads. */
|
|
782
|
+
#about(message) {
|
|
783
|
+
const patch = {};
|
|
784
|
+
const name = message["name"];
|
|
785
|
+
if (typeof name === "string" && name.trim() !== "") patch.name = name.trim().slice(0, 60);
|
|
786
|
+
const features = message["features"];
|
|
787
|
+
patch.features = Array.isArray(features) ? features.filter((item) => typeof item === "string" && item.length <= 40).slice(0, 20) : [];
|
|
788
|
+
patch.older = !patch.features.includes("frames.binary");
|
|
789
|
+
return patch;
|
|
790
|
+
}
|
|
103
791
|
/** Drops the token and asks for a new socket, which starts the pairing dance again. */
|
|
104
792
|
async forget() {
|
|
105
|
-
await this.#options.
|
|
793
|
+
if (this.#options.tokens) await this.#options.tokens.clear();
|
|
794
|
+
else await this.#options.chrome?.storage.local.remove([TOKEN_KEY]);
|
|
106
795
|
this.#authenticated = false;
|
|
107
796
|
this.#set({ connection: "offline", code: null, installation: null, error: null });
|
|
108
797
|
this.#options.disconnect?.("The owner forgot this buddi.");
|
|
@@ -137,10 +826,11 @@ var Protocol = class {
|
|
|
137
826
|
connection: "paired",
|
|
138
827
|
code: null,
|
|
139
828
|
error: null,
|
|
829
|
+
...this.#about(message),
|
|
140
830
|
installation: typeof installation === "string" ? installation : this.#state.installation
|
|
141
831
|
});
|
|
142
832
|
const token = message["token"];
|
|
143
|
-
if (typeof token === "string" && token.length > 0) await this.#
|
|
833
|
+
if (typeof token === "string" && token.length > 0) await this.#keep(token);
|
|
144
834
|
return;
|
|
145
835
|
}
|
|
146
836
|
case "rehello":
|
|
@@ -169,398 +859,90 @@ var Protocol = class {
|
|
|
169
859
|
default:
|
|
170
860
|
return;
|
|
171
861
|
}
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* The gateway signed our nonce. Check it before the token leaves this browser.
|
|
175
|
-
*
|
|
176
|
-
* The key is the token's hash, which is all the gateway keeps, so a gateway
|
|
177
|
-
* that cannot produce this signature is not the one this browser paired with
|
|
178
|
-
* and never gets to see the token.
|
|
179
|
-
*/
|
|
180
|
-
async #challenge(message) {
|
|
181
|
-
const token = await this.#token();
|
|
182
|
-
const claimed = typeof message["proof"] === "string" ? message["proof"] : "";
|
|
183
|
-
if (!token) {
|
|
184
|
-
this.#options.disconnect?.("This browser has no token for that buddi.");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const expected = await proofFor(await sha256Hex(token), this.#nonce);
|
|
188
|
-
if (!sameProof(expected, claimed)) {
|
|
189
|
-
this.#authenticated = false;
|
|
190
|
-
this.#set({ connection: "offline", code: null, error: BAD_PROOF });
|
|
191
|
-
this.#options.disconnect?.(BAD_PROOF);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const installation = message["installation"];
|
|
195
|
-
if (typeof installation === "string") this.#set({ installation });
|
|
196
|
-
this.#options.send({ type: "auth", token });
|
|
197
|
-
}
|
|
198
|
-
async #command(message) {
|
|
199
|
-
const id = typeof message["id"] === "string" ? message["id"] : null;
|
|
200
|
-
if (!id) return;
|
|
201
|
-
if (!this.#authenticated) {
|
|
202
|
-
this.#options.send({ type: "result", id, ok: false, error: "This browser is not paired with you.", precondition: true });
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const name = message["name"];
|
|
206
|
-
const session = message["session"];
|
|
207
|
-
if (typeof name !== "string" || !isCommandName(name)) {
|
|
208
|
-
this.#options.send({ type: "result", id, ok: false, error: `This buddi extension does not know the command ${String(name)}.`, precondition: true });
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (typeof session !== "string" || session.length === 0) {
|
|
212
|
-
this.#options.send({ type: "result", id, ok: false, error: "The command arrived without a session.", precondition: true });
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const args = message["args"];
|
|
216
|
-
const command = {
|
|
217
|
-
id,
|
|
218
|
-
name,
|
|
219
|
-
session,
|
|
220
|
-
args: args && typeof args === "object" ? args : {},
|
|
221
|
-
owner: message["owner"] === true
|
|
222
|
-
};
|
|
223
|
-
const cancel = new Cancellation();
|
|
224
|
-
this.#running.set(id, cancel);
|
|
225
|
-
try {
|
|
226
|
-
const result = await this.#options.execute(command, cancel);
|
|
227
|
-
cancel.check();
|
|
228
|
-
this.#options.send({ type: "result", id, ok: true, observation: result.observation ?? null, screenshot: result.screenshot ?? null });
|
|
229
|
-
} catch (error) {
|
|
230
|
-
if (error instanceof CancelledError) {
|
|
231
|
-
this.#options.send({ type: "result", id, ok: false, error: "cancelled", precondition: !cancel.dispatched });
|
|
232
|
-
} else {
|
|
233
|
-
this.#options.send({
|
|
234
|
-
type: "result",
|
|
235
|
-
id,
|
|
236
|
-
ok: false,
|
|
237
|
-
error: sentence(error),
|
|
238
|
-
precondition: error instanceof PreconditionError,
|
|
239
|
-
...error instanceof OpenedError ? { page: error.page } : {}
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
} finally {
|
|
243
|
-
this.#running.delete(id);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
function isCommandName(name) {
|
|
248
|
-
return name === "screenshot" || COMMAND_NAMES.includes(name) || HAND_COMMANDS.includes(name);
|
|
249
|
-
}
|
|
250
|
-
function sentence(error) {
|
|
251
|
-
const text2 = error instanceof Error ? error.message : String(error);
|
|
252
|
-
const trimmed = text2.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
253
|
-
if (!trimmed) return "The browser extension could not run that command.";
|
|
254
|
-
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// src/bar.ts
|
|
258
|
-
var OWNER_WAIT_MS = 3e4;
|
|
259
|
-
var BAR_AFTER_MS = 3e3;
|
|
260
|
-
var BAR_POLL_MS = 500;
|
|
261
|
-
async function waitForOwner(probes, limits = {}) {
|
|
262
|
-
const waitMs = limits.waitMs ?? OWNER_WAIT_MS;
|
|
263
|
-
const barAfter = limits.barAfterMs ?? BAR_AFTER_MS;
|
|
264
|
-
const poll = limits.pollMs ?? BAR_POLL_MS;
|
|
265
|
-
const start = probes.now();
|
|
266
|
-
let shown = false;
|
|
267
|
-
const maxPolls = Math.ceil(waitMs / poll);
|
|
268
|
-
try {
|
|
269
|
-
for (let polls = 0; ; polls++) {
|
|
270
|
-
if (!await probes.watched()) return "left";
|
|
271
|
-
const elapsed = Math.max(probes.now() - start, polls * poll);
|
|
272
|
-
if (!shown && elapsed >= barAfter) {
|
|
273
|
-
shown = true;
|
|
274
|
-
await probes.show().catch(() => void 0);
|
|
275
|
-
}
|
|
276
|
-
if (shown) {
|
|
277
|
-
const choice = await probes.choice().catch(() => void 0);
|
|
278
|
-
if (choice) return choice;
|
|
279
|
-
}
|
|
280
|
-
if (elapsed >= waitMs || polls >= maxPolls) return "timeout";
|
|
281
|
-
await probes.wait(poll);
|
|
282
|
-
}
|
|
283
|
-
} finally {
|
|
284
|
-
if (shown) await probes.hide().catch(() => void 0);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function showBar(agent) {
|
|
288
|
-
const holder = globalThis;
|
|
289
|
-
holder["__buddiBar"] = { choice: void 0 };
|
|
290
|
-
const doc = globalThis.document;
|
|
291
|
-
if (!doc || doc.getElementById("buddi-working-bar")) return;
|
|
292
|
-
const host = doc.createElement("div");
|
|
293
|
-
host.id = "buddi-working-bar";
|
|
294
|
-
host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
|
|
295
|
-
const root = host.attachShadow({ mode: "closed" });
|
|
296
|
-
const bar = doc.createElement("div");
|
|
297
|
-
bar.setAttribute("role", "status");
|
|
298
|
-
bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
|
|
299
|
-
const label = doc.createElement("span");
|
|
300
|
-
label.textContent = `${agent || "buddi"} is working here`;
|
|
301
|
-
label.setAttribute("style", "margin-right:auto;");
|
|
302
|
-
const button = (text2, choice, primary) => {
|
|
303
|
-
const b = doc.createElement("button");
|
|
304
|
-
b.type = "button";
|
|
305
|
-
b.textContent = text2;
|
|
306
|
-
b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
|
|
307
|
-
b.addEventListener("click", () => {
|
|
308
|
-
holder["__buddiBar"] = { choice };
|
|
309
|
-
});
|
|
310
|
-
return b;
|
|
311
|
-
};
|
|
312
|
-
bar.append(label, button("Take over", "takeover", false), button("Let it continue", "continue", true));
|
|
313
|
-
root.append(bar);
|
|
314
|
-
(doc.documentElement ?? doc.body).append(host);
|
|
315
|
-
}
|
|
316
|
-
function readBar() {
|
|
317
|
-
const holder = globalThis;
|
|
318
|
-
const choice = holder["__buddiBar"]?.choice;
|
|
319
|
-
if (choice) holder["__buddiBar"] = { choice: void 0 };
|
|
320
|
-
return choice;
|
|
321
|
-
}
|
|
322
|
-
function hideBar() {
|
|
323
|
-
const doc = globalThis.document;
|
|
324
|
-
doc?.getElementById("buddi-working-bar")?.remove();
|
|
325
|
-
const holder = globalThis;
|
|
326
|
-
delete holder["__buddiBar"];
|
|
327
|
-
}
|
|
328
|
-
var GIVE_BACK_MESSAGE = "buddi-give-back";
|
|
329
|
-
function showHeldBar(session) {
|
|
330
|
-
const doc = globalThis.document;
|
|
331
|
-
if (!doc || doc.getElementById("buddi-held-bar")) return;
|
|
332
|
-
const host = doc.createElement("div");
|
|
333
|
-
host.id = "buddi-held-bar";
|
|
334
|
-
host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
|
|
335
|
-
const root = host.attachShadow({ mode: "closed" });
|
|
336
|
-
const bar = doc.createElement("div");
|
|
337
|
-
bar.setAttribute("role", "status");
|
|
338
|
-
bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
|
|
339
|
-
const label = doc.createElement("span");
|
|
340
|
-
label.textContent = "buddi is waiting";
|
|
341
|
-
label.setAttribute("style", "margin-right:auto;");
|
|
342
|
-
const give = doc.createElement("button");
|
|
343
|
-
give.type = "button";
|
|
344
|
-
give.textContent = "Give it back";
|
|
345
|
-
give.setAttribute("style", "font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;background:#fff;color:#1f2a44;");
|
|
346
|
-
give.addEventListener("click", () => {
|
|
347
|
-
const runtime = globalThis.chrome?.runtime;
|
|
348
|
-
void runtime?.sendMessage({ type: "buddi-give-back", session })?.catch?.(() => void 0);
|
|
349
|
-
host.remove();
|
|
350
|
-
});
|
|
351
|
-
bar.append(label, give);
|
|
352
|
-
root.append(bar);
|
|
353
|
-
(doc.documentElement ?? doc.body).append(host);
|
|
354
|
-
const asking = [];
|
|
355
|
-
const restore = () => {
|
|
356
|
-
for (const button of asking.splice(0)) button.remove();
|
|
357
|
-
label.textContent = "buddi is waiting";
|
|
358
|
-
give.style.display = "";
|
|
359
|
-
};
|
|
360
|
-
globalThis["__buddiHeldAsk"] = (question, choices) => {
|
|
361
|
-
restore();
|
|
362
|
-
label.textContent = question;
|
|
363
|
-
give.style.display = "none";
|
|
364
|
-
for (const choice of choices) {
|
|
365
|
-
const b = doc.createElement("button");
|
|
366
|
-
b.type = "button";
|
|
367
|
-
b.textContent = choice.label;
|
|
368
|
-
b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${choice.primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
|
|
369
|
-
b.addEventListener("click", () => {
|
|
370
|
-
restore();
|
|
371
|
-
choice.pick();
|
|
372
|
-
});
|
|
373
|
-
asking.push(b);
|
|
374
|
-
bar.insertBefore(b, give);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* The gateway signed our nonce. Check it before the token leaves this browser.
|
|
865
|
+
*
|
|
866
|
+
* The key is the token's hash, which is all the gateway keeps, so a gateway
|
|
867
|
+
* that cannot produce this signature is not the one this browser paired with
|
|
868
|
+
* and never gets to see the token.
|
|
869
|
+
*/
|
|
870
|
+
async #challenge(message) {
|
|
871
|
+
const token = await this.#token();
|
|
872
|
+
const claimed = typeof message["proof"] === "string" ? message["proof"] : "";
|
|
873
|
+
if (!token) {
|
|
874
|
+
this.#options.disconnect?.("This browser has no token for that buddi.");
|
|
875
|
+
return;
|
|
375
876
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const holder = globalThis;
|
|
388
|
-
const known = { never: [...facts?.never ?? []], saved: [...facts?.saved ?? []] };
|
|
389
|
-
const existing = holder["__buddiHeldLogins"];
|
|
390
|
-
if (existing) {
|
|
391
|
-
existing.session = session;
|
|
392
|
-
existing.facts = known;
|
|
393
|
-
return;
|
|
877
|
+
const expected = await proofFor(await sha256Hex(token), this.#nonce);
|
|
878
|
+
if (!sameProof(expected, claimed)) {
|
|
879
|
+
this.#authenticated = false;
|
|
880
|
+
await this.dropToken();
|
|
881
|
+
this.#set({ connection: "offline", code: null, error: BAD_PROOF });
|
|
882
|
+
this.#options.disconnect?.(PAIR_AGAIN);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const installation = message["installation"];
|
|
886
|
+
this.#set({ ...this.#about(message), ...typeof installation === "string" ? { installation } : {} });
|
|
887
|
+
this.#options.send({ type: "auth", token });
|
|
394
888
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
return await runtime()?.sendMessage(message);
|
|
403
|
-
} catch {
|
|
404
|
-
return void 0;
|
|
889
|
+
async #command(message) {
|
|
890
|
+
const id = typeof message["id"] === "string" ? message["id"] : null;
|
|
891
|
+
if (!id) return;
|
|
892
|
+
if (!this.#authenticated) {
|
|
893
|
+
this.#options.send({ type: "result", id, ok: false, error: "This browser is not paired with you.", precondition: true });
|
|
894
|
+
return;
|
|
405
895
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
answered = true;
|
|
424
|
-
const asking = bar();
|
|
425
|
-
asking?.(update ? "Updating\u2026" : "Saving\u2026", []);
|
|
426
|
-
void call({ type: "buddi-login", session: state.session, decision: "save" }).then((reply) => {
|
|
427
|
-
const answer = reply;
|
|
428
|
-
const now = bar();
|
|
429
|
-
if (!now) return;
|
|
430
|
-
if (answer?.saved === true) {
|
|
431
|
-
const restore2 = now(update ? `Updated the login for ${where}` : `Saved the login for ${where}`, []);
|
|
432
|
-
if (state.drop) clearTimeout(state.drop);
|
|
433
|
-
state.drop = setTimeout(restore2, 2e3);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : "buddi did not answer. Try again.";
|
|
437
|
-
now(reason, [
|
|
438
|
-
{ label: "Try again", primary: true, pick: () => {
|
|
439
|
-
answered = false;
|
|
440
|
-
save();
|
|
441
|
-
} },
|
|
442
|
-
{ label: "Not now", pick: () => {
|
|
443
|
-
void call({ type: "buddi-login", session: state.session, decision: "later" });
|
|
444
|
-
} }
|
|
445
|
-
]);
|
|
446
|
-
});
|
|
896
|
+
const name = message["name"];
|
|
897
|
+
const session = message["session"];
|
|
898
|
+
if (typeof name !== "string" || !isCommandName(name)) {
|
|
899
|
+
this.#options.send({ type: "result", id, ok: false, error: `This buddi extension does not know the command ${String(name)}.`, precondition: true });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (typeof session !== "string" || session.length === 0) {
|
|
903
|
+
this.#options.send({ type: "result", id, ok: false, error: "The command arrived without a session.", precondition: true });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const args = message["args"];
|
|
907
|
+
const command = {
|
|
908
|
+
id,
|
|
909
|
+
name,
|
|
910
|
+
session,
|
|
911
|
+
args: args && typeof args === "object" ? args : {},
|
|
912
|
+
owner: message["owner"] === true
|
|
447
913
|
};
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
{ label: "Not now", pick: () => {
|
|
451
|
-
answered = true;
|
|
452
|
-
void call({ type: "buddi-login", session: state.session, decision: "later" });
|
|
453
|
-
} },
|
|
454
|
-
{ label: "Never for this site", pick: () => {
|
|
455
|
-
answered = true;
|
|
456
|
-
state.facts.never.push(where);
|
|
457
|
-
void call({ type: "buddi-login", session: state.session, decision: "never" });
|
|
458
|
-
} }
|
|
459
|
-
]);
|
|
460
|
-
state.drop = setTimeout(() => {
|
|
461
|
-
if (!answered) restore();
|
|
462
|
-
}, 2 * 6e4);
|
|
463
|
-
};
|
|
464
|
-
const ask = (found) => {
|
|
465
|
-
const where = site();
|
|
466
|
-
if (where === "" || listed(where) || !bar()) return;
|
|
467
|
-
const update = state.facts.saved.some((login) => login.site === where && login.username === found.username);
|
|
468
|
-
void call({ type: "buddi-login-seen", session: state.session, username: found.username, password: found.password, update }).then((reply) => {
|
|
469
|
-
const asked = reply;
|
|
470
|
-
if (holder["__buddiHeldLogins"] !== state || !asked || typeof asked !== "object") return;
|
|
471
|
-
prompt(where, asked.update === true);
|
|
472
|
-
});
|
|
473
|
-
};
|
|
474
|
-
void call({ type: "buddi-login-pending", session: state.session }).then((reply) => {
|
|
475
|
-
const waiting = reply;
|
|
476
|
-
if (holder["__buddiHeldLogins"] !== state || !waiting || typeof waiting.site !== "string") return;
|
|
477
|
-
prompt(waiting.site, waiting.update === true);
|
|
478
|
-
});
|
|
479
|
-
const NOT_SUBMIT = /\b(show|hide|reveal|toggle|eye|forgot|reset|cancel|back|close|sign ?up|register|create)\b/i;
|
|
480
|
-
const USER = /user|email|login|account|ident|phone|mail/i;
|
|
481
|
-
const passwords = (root) => Array.from(root.querySelectorAll('input[type="password" i]'));
|
|
482
|
-
const usernameFor = (password) => {
|
|
483
|
-
const scope = password.form ?? doc;
|
|
484
|
-
const inputs = Array.from(scope.querySelectorAll("input")).filter((input) => ["text", "email", "tel", ""].includes((input.getAttribute("type") ?? "").toLowerCase()) && input.value.trim() !== "");
|
|
485
|
-
const marked = inputs.find((input) => (input.getAttribute("autocomplete") ?? "").toLowerCase().includes("username") || (input.getAttribute("type") ?? "").toLowerCase() === "email");
|
|
486
|
-
if (marked) return marked.value.trim();
|
|
487
|
-
const before = inputs.filter((input) => (input.compareDocumentPosition(password) & 4) !== 0);
|
|
488
|
-
const pick = before[before.length - 1] ?? inputs.find((input) => USER.test(`${input.name} ${input.id} ${input.getAttribute("autocomplete") ?? ""}`));
|
|
489
|
-
return pick ? pick.value.trim() : "";
|
|
490
|
-
};
|
|
491
|
-
const read = (scope) => {
|
|
492
|
-
const field = passwords(scope ?? doc).find((input) => input.value !== "");
|
|
493
|
-
return field ? { username: usernameFor(field), password: field.value } : void 0;
|
|
494
|
-
};
|
|
495
|
-
const send2 = (found) => {
|
|
496
|
-
if (holder["__buddiHeldLogins"] !== state) return;
|
|
497
|
-
if (!found || found.password === "") return;
|
|
498
|
-
const key = `${found.username}\0${found.password}`;
|
|
499
|
-
const now = Date.now();
|
|
500
|
-
if (key === state.last && now - state.lastAt < 5e3) return;
|
|
501
|
-
state.last = key;
|
|
502
|
-
state.lastAt = now;
|
|
503
|
-
state.cached = void 0;
|
|
504
|
-
ask(found);
|
|
505
|
-
};
|
|
506
|
-
doc.addEventListener("submit", (event) => {
|
|
507
|
-
send2(read(event.target) ?? state.cached);
|
|
508
|
-
}, true);
|
|
509
|
-
doc.addEventListener("keydown", (event) => {
|
|
510
|
-
if (event.key !== "Enter") return;
|
|
511
|
-
const target = event.target;
|
|
512
|
-
if (!target || target.tagName !== "INPUT") return;
|
|
513
|
-
send2(read(target.form ?? doc));
|
|
514
|
-
}, true);
|
|
515
|
-
doc.addEventListener("click", (event) => {
|
|
516
|
-
const target = event.target;
|
|
517
|
-
const button = target?.closest?.('button, input[type="submit" i], input[type="image" i], input[type="button" i], [role="button"]');
|
|
518
|
-
if (!button) return;
|
|
519
|
-
const words = `${button.textContent ?? ""} ${button.getAttribute("aria-label") ?? ""} ${button.getAttribute("value") ?? ""} ${button.getAttribute("title") ?? ""}`;
|
|
520
|
-
if (NOT_SUBMIT.test(words)) return;
|
|
521
|
-
send2(read(button.form ?? doc));
|
|
522
|
-
}, true);
|
|
523
|
-
doc.addEventListener("input", (event) => {
|
|
524
|
-
const target = event.target;
|
|
525
|
-
if ((target?.getAttribute?.("type") ?? "").toLowerCase() !== "password") return;
|
|
526
|
-
state.typedAt = Date.now();
|
|
527
|
-
state.cached = read(target.form ?? doc);
|
|
528
|
-
}, true);
|
|
529
|
-
const Observer = globalThis.PerformanceObserver;
|
|
530
|
-
if (typeof Observer === "function") {
|
|
914
|
+
const cancel = new Cancellation();
|
|
915
|
+
this.#running.set(id, cancel);
|
|
531
916
|
try {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
917
|
+
const result = await this.#options.execute(command, cancel);
|
|
918
|
+
cancel.check();
|
|
919
|
+
this.#options.send({ type: "result", id, ok: true, observation: result.observation ?? null, screenshot: result.screenshot ?? null });
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (error instanceof CancelledError) {
|
|
922
|
+
this.#options.send({ type: "result", id, ok: false, error: "cancelled", precondition: !cancel.dispatched });
|
|
923
|
+
} else {
|
|
924
|
+
this.#options.send({
|
|
925
|
+
type: "result",
|
|
926
|
+
id,
|
|
927
|
+
ok: false,
|
|
928
|
+
error: sentence(error),
|
|
929
|
+
precondition: error instanceof PreconditionError,
|
|
930
|
+
...error instanceof OpenedError ? { page: error.page } : {}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
} finally {
|
|
934
|
+
this.#running.delete(id);
|
|
543
935
|
}
|
|
544
936
|
}
|
|
937
|
+
};
|
|
938
|
+
function isCommandName(name) {
|
|
939
|
+
return name === "screenshot" || COMMAND_NAMES.includes(name) || HAND_COMMANDS.includes(name);
|
|
545
940
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (heldTab === void 0 || sender?.tab?.id !== heldTab) return void 0;
|
|
552
|
-
const session = request.session;
|
|
553
|
-
const url = sender.url ?? "";
|
|
554
|
-
if (request.type === LOGIN_SEEN_MESSAGE) {
|
|
555
|
-
if (typeof request.password !== "string" || request.password === "" || request.password.length > 1024) return void 0;
|
|
556
|
-
const username = typeof request.username === "string" ? request.username.slice(0, 200) : "";
|
|
557
|
-
return { kind: "seen", session, tabId: heldTab, url, username, password: request.password, update: request.update === true };
|
|
558
|
-
}
|
|
559
|
-
if (request.type === LOGIN_PENDING_MESSAGE) return { kind: "pending", session, tabId: heldTab, url };
|
|
560
|
-
if (request.type === LOGIN_MESSAGE && (request.decision === "save" || request.decision === "later" || request.decision === "never")) {
|
|
561
|
-
return { kind: "answer", session, tabId: heldTab, decision: request.decision };
|
|
562
|
-
}
|
|
563
|
-
return void 0;
|
|
941
|
+
function sentence(error) {
|
|
942
|
+
const text2 = error instanceof Error ? error.message : String(error);
|
|
943
|
+
const trimmed = text2.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
944
|
+
if (!trimmed) return "The browser extension could not run that command.";
|
|
945
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
564
946
|
}
|
|
565
947
|
|
|
566
948
|
// src/held-logins.ts
|
|
@@ -588,8 +970,8 @@ function webOrigin(url) {
|
|
|
588
970
|
}
|
|
589
971
|
}
|
|
590
972
|
var HeldLogins = class {
|
|
591
|
-
constructor(
|
|
592
|
-
this.send =
|
|
973
|
+
constructor(send, options = {}) {
|
|
974
|
+
this.send = send;
|
|
593
975
|
this.options = options;
|
|
594
976
|
}
|
|
595
977
|
#byTab = /* @__PURE__ */ new Map();
|
|
@@ -602,11 +984,11 @@ var HeldLogins = class {
|
|
|
602
984
|
#ask(tabId, frame, ms) {
|
|
603
985
|
const id = this.#id();
|
|
604
986
|
return new Promise((resolve) => {
|
|
605
|
-
const
|
|
987
|
+
const timer = setTimeout(() => {
|
|
606
988
|
this.#waiting.delete(id);
|
|
607
989
|
resolve(void 0);
|
|
608
990
|
}, ms);
|
|
609
|
-
this.#waiting.set(id, { tabId, resolve, timer
|
|
991
|
+
this.#waiting.set(id, { tabId, resolve, timer });
|
|
610
992
|
this.send({ ...frame, id });
|
|
611
993
|
});
|
|
612
994
|
}
|
|
@@ -733,6 +1115,9 @@ var SETTLE_MS = 600;
|
|
|
733
1115
|
var SETTLE_STEP_MS = 50;
|
|
734
1116
|
var OBSERVE_RETRY_MS = [500, 1e3, 2e3];
|
|
735
1117
|
var MIN_FRAME_MS = 100;
|
|
1118
|
+
var WATCH_FRAME_MS = 500;
|
|
1119
|
+
var MAX_FRAME_MS = 2e3;
|
|
1120
|
+
var MAX_COPY = 4e3;
|
|
736
1121
|
var MAX_CAST_SIDE = 4096;
|
|
737
1122
|
var MAX_COORDINATE = 2e4;
|
|
738
1123
|
var MAX_DELTA = 1e4;
|
|
@@ -757,13 +1142,13 @@ function num(value) {
|
|
|
757
1142
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
758
1143
|
}
|
|
759
1144
|
function side(value, fallback) {
|
|
760
|
-
const
|
|
761
|
-
return Math.min(MAX_CAST_SIDE, Math.max(1, Math.round(
|
|
1145
|
+
const asked2 = num(value) ?? fallback;
|
|
1146
|
+
return Math.min(MAX_CAST_SIDE, Math.max(1, Math.round(asked2)));
|
|
762
1147
|
}
|
|
763
1148
|
function clamp(value, limit, fallback = 0) {
|
|
764
|
-
const
|
|
765
|
-
if (
|
|
766
|
-
return Math.min(limit, Math.max(-limit,
|
|
1149
|
+
const asked2 = num(value);
|
|
1150
|
+
if (asked2 === void 0) return fallback;
|
|
1151
|
+
return Math.min(limit, Math.max(-limit, asked2));
|
|
767
1152
|
}
|
|
768
1153
|
function metadataOf(value) {
|
|
769
1154
|
const source = value ?? {};
|
|
@@ -813,8 +1198,8 @@ var VIRTUAL_KEYS = {
|
|
|
813
1198
|
F12: 123
|
|
814
1199
|
};
|
|
815
1200
|
function virtualKey(key) {
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
1201
|
+
const named2 = VIRTUAL_KEYS[key];
|
|
1202
|
+
if (named2 !== void 0) return named2;
|
|
818
1203
|
return key.length === 1 ? key.toUpperCase().charCodeAt(0) : 0;
|
|
819
1204
|
}
|
|
820
1205
|
function text(value, limit) {
|
|
@@ -935,8 +1320,17 @@ var BrowserCommands = class {
|
|
|
935
1320
|
#casts = /* @__PURE__ */ new Map();
|
|
936
1321
|
#castsByTab = /* @__PURE__ */ new Map();
|
|
937
1322
|
#ownerWaitMs;
|
|
1323
|
+
/** Which buddi these commands belong to, and the registry that says whose each group is. */
|
|
1324
|
+
#instance;
|
|
1325
|
+
#groups;
|
|
1326
|
+
#groupTitle;
|
|
1327
|
+
#groupColour;
|
|
938
1328
|
constructor(chrome2, options = {}) {
|
|
939
1329
|
this.#chrome = chrome2;
|
|
1330
|
+
this.#instance = options.instance ?? "buddi";
|
|
1331
|
+
this.#groups = options.groups;
|
|
1332
|
+
this.#groupTitle = options.groupTitle ?? (() => GROUP_TITLE);
|
|
1333
|
+
this.#groupColour = options.groupColour ?? (() => void 0);
|
|
940
1334
|
this.downloads = options.downloads;
|
|
941
1335
|
this.logins = new HeldLogins(options.onLogin ?? (() => void 0));
|
|
942
1336
|
chrome2.tabs.onRemoved?.addListener((tabId) => this.logins.clearTab(tabId));
|
|
@@ -949,11 +1343,17 @@ var BrowserCommands = class {
|
|
|
949
1343
|
chrome2.debugger.onDetach.addListener((source) => {
|
|
950
1344
|
const tabId = source.tabId;
|
|
951
1345
|
if (tabId === void 0) return;
|
|
1346
|
+
if (!this.#attached.has(tabId) && !this.#castsByTab.has(tabId)) return;
|
|
952
1347
|
this.#attached.delete(tabId);
|
|
953
1348
|
const cast = this.#castsByTab.get(tabId);
|
|
954
|
-
if (cast)
|
|
1349
|
+
if (cast) {
|
|
1350
|
+
this.#forgetCast(cast);
|
|
1351
|
+
this.#onEvent({ type: "event", name: "castended", session: cast.session });
|
|
1352
|
+
}
|
|
955
1353
|
});
|
|
956
|
-
chrome2.tabs.onUpdated?.addListener((tabId, change) => {
|
|
1354
|
+
chrome2.tabs.onUpdated?.addListener((tabId, change, tab) => {
|
|
1355
|
+
const cast = this.#castsByTab.get(tabId);
|
|
1356
|
+
if (cast && tab?.url) cast.url = tab.url;
|
|
957
1357
|
if (change.status !== "complete" || ![...this.#held.values()].includes(tabId)) return;
|
|
958
1358
|
const session = [...this.#held].find(([, held]) => held === tabId)?.[0];
|
|
959
1359
|
if (session) void this.#drawHeld(tabId, session);
|
|
@@ -963,6 +1363,26 @@ var BrowserCommands = class {
|
|
|
963
1363
|
}
|
|
964
1364
|
/** Downloads an agent's command starts, noticed while it runs (and a few seconds after). */
|
|
965
1365
|
downloads;
|
|
1366
|
+
/** How many tabs this buddi is working in right now, for the popup. */
|
|
1367
|
+
async tabCount() {
|
|
1368
|
+
let count = 0;
|
|
1369
|
+
for (const session of this.#sessions.values()) {
|
|
1370
|
+
for (const tabId of session.tabs.values()) {
|
|
1371
|
+
const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
|
|
1372
|
+
if (tab && !this.#taken(session, tab)) count += 1;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return count;
|
|
1376
|
+
}
|
|
1377
|
+
/** A buddi's name changed (its handshake said one): its groups take it. */
|
|
1378
|
+
async retitle() {
|
|
1379
|
+
const title = this.#groupTitle();
|
|
1380
|
+
const color = this.#groupColour();
|
|
1381
|
+
for (const session of this.#sessions.values()) {
|
|
1382
|
+
if (session.groupId < 0) continue;
|
|
1383
|
+
await this.#chrome.tabGroups.update(session.groupId, { title, ...color ? { color } : {} }).catch(() => void 0);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
966
1386
|
/** The addresses of a session's tabs, for telling its downloads from the owner's. */
|
|
967
1387
|
async #tabUrls(name) {
|
|
968
1388
|
const session = this.#sessions.get(name);
|
|
@@ -1022,6 +1442,10 @@ var BrowserCommands = class {
|
|
|
1022
1442
|
return this.#hold(command, cancel);
|
|
1023
1443
|
case "unhold":
|
|
1024
1444
|
return this.#unhold(command.session);
|
|
1445
|
+
case "capture":
|
|
1446
|
+
return this.#capture(command.session, cancel);
|
|
1447
|
+
case "copy":
|
|
1448
|
+
return this.#copy(command.session);
|
|
1025
1449
|
case "close":
|
|
1026
1450
|
return this.#close(command, cancel);
|
|
1027
1451
|
default:
|
|
@@ -1090,8 +1514,8 @@ var BrowserCommands = class {
|
|
|
1090
1514
|
const session = message?.session;
|
|
1091
1515
|
const request = heldLoginRequest(message, sender, typeof session === "string" ? this.#held.get(session) : void 0);
|
|
1092
1516
|
if (!request) return void 0;
|
|
1093
|
-
if (request.kind === "seen") return this.logins.capture(request.tabId, request.session, request.url, request).then((
|
|
1094
|
-
if (request.kind === "pending") return this.logins.pending(request.tabId, request.session, request.url).then((
|
|
1517
|
+
if (request.kind === "seen") return this.logins.capture(request.tabId, request.session, request.url, request).then((asked2) => asked2 ?? null);
|
|
1518
|
+
if (request.kind === "pending") return this.logins.pending(request.tabId, request.session, request.url).then((asked2) => asked2 ?? null);
|
|
1095
1519
|
if (request.decision === "save") return this.logins.save(request.tabId, request.session);
|
|
1096
1520
|
if (request.decision === "never") return Promise.resolve({ ok: this.logins.never(request.tabId, request.session) });
|
|
1097
1521
|
this.logins.dismiss(request.tabId, request.session);
|
|
@@ -1162,6 +1586,7 @@ var BrowserCommands = class {
|
|
|
1162
1586
|
/** The owner dragged this tab out of the session's group. A tab that never made it into the group cannot have been. */
|
|
1163
1587
|
#taken(session, tab) {
|
|
1164
1588
|
if (tab.id !== void 0 && session.ungrouped.has(tab.id)) return false;
|
|
1589
|
+
if (this.#groups && !this.#groups.allows(tab.groupId, this.#instance)) return true;
|
|
1165
1590
|
return session.groupId >= 0 && tab.groupId !== session.groupId;
|
|
1166
1591
|
}
|
|
1167
1592
|
#activeKey(session) {
|
|
@@ -1238,8 +1663,8 @@ var BrowserCommands = class {
|
|
|
1238
1663
|
if (normal(found)) return found.id;
|
|
1239
1664
|
}
|
|
1240
1665
|
}
|
|
1241
|
-
const
|
|
1242
|
-
if (normal(
|
|
1666
|
+
const last = await windows.getLastFocused?.({ windowTypes: ["normal"] }).catch(() => void 0);
|
|
1667
|
+
if (normal(last)) return last.id;
|
|
1243
1668
|
const all = await windows.getAll?.({ windowTypes: ["normal"] }).catch(() => void 0);
|
|
1244
1669
|
return all?.find(normal)?.id ?? null;
|
|
1245
1670
|
}
|
|
@@ -1277,9 +1702,11 @@ var BrowserCommands = class {
|
|
|
1277
1702
|
if (groupable) {
|
|
1278
1703
|
try {
|
|
1279
1704
|
const groupId = await this.#chrome.tabs.group(session.groupId >= 0 ? { tabIds: [tabId], groupId: session.groupId } : { tabIds: [tabId] });
|
|
1705
|
+
this.#groups?.claim(groupId, this.#instance);
|
|
1280
1706
|
if (session.groupId !== groupId) {
|
|
1281
1707
|
session.groupId = groupId;
|
|
1282
|
-
|
|
1708
|
+
const color = this.#groupColour();
|
|
1709
|
+
await this.#chrome.tabGroups.update(groupId, { title: this.#groupTitle(), collapsed: false, ...color ? { color } : {} }).catch(() => void 0);
|
|
1283
1710
|
}
|
|
1284
1711
|
} catch {
|
|
1285
1712
|
session.ungrouped.add(tabId);
|
|
@@ -1358,6 +1785,7 @@ var BrowserCommands = class {
|
|
|
1358
1785
|
this.#refs.delete(command.session);
|
|
1359
1786
|
cancel.check();
|
|
1360
1787
|
const note = await this.#reuseOrOpen(session, url, cancel);
|
|
1788
|
+
await this.#followWatch(command.session);
|
|
1361
1789
|
return note ? { observation: { note } } : {};
|
|
1362
1790
|
}
|
|
1363
1791
|
async #tab(command) {
|
|
@@ -1368,11 +1796,13 @@ var BrowserCommands = class {
|
|
|
1368
1796
|
throw error instanceof PreconditionError ? error : new PreconditionError("No such tab in this conversation.");
|
|
1369
1797
|
});
|
|
1370
1798
|
session.active = wanted;
|
|
1799
|
+
await this.#followWatch(command.session);
|
|
1371
1800
|
return {};
|
|
1372
1801
|
}
|
|
1373
1802
|
/** Closes this session's own tabs, and only the ones still in its group. */
|
|
1374
1803
|
async #close(command, cancel) {
|
|
1375
1804
|
this.#inView.delete(command.session);
|
|
1805
|
+
await this.#stopScreencast(command.session).catch(() => void 0);
|
|
1376
1806
|
const session = this.#sessions.get(command.session);
|
|
1377
1807
|
if (!session) return {};
|
|
1378
1808
|
const ids = [];
|
|
@@ -1425,7 +1855,7 @@ var BrowserCommands = class {
|
|
|
1425
1855
|
* with growing waits, before telling the model the page is not answering.
|
|
1426
1856
|
*/
|
|
1427
1857
|
async #readFrames(tabId, cancel) {
|
|
1428
|
-
for (let
|
|
1858
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1429
1859
|
await this.#inject(tabId);
|
|
1430
1860
|
const frames = await this.#chrome.scripting.executeScript({
|
|
1431
1861
|
target: { tabId, allFrames: true },
|
|
@@ -1433,7 +1863,7 @@ var BrowserCommands = class {
|
|
|
1433
1863
|
}).catch(() => []);
|
|
1434
1864
|
const top = frames.find((frame) => frame.frameId === 0);
|
|
1435
1865
|
if (top?.result) return { frames, top: { frameId: top.frameId, result: top.result } };
|
|
1436
|
-
const wait = OBSERVE_RETRY_MS[
|
|
1866
|
+
const wait = OBSERVE_RETRY_MS[attempt];
|
|
1437
1867
|
if (wait === void 0) break;
|
|
1438
1868
|
await this.#wait(wait);
|
|
1439
1869
|
cancel.check();
|
|
@@ -1620,36 +2050,53 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1620
2050
|
await this.#releaseDebugger(tabId);
|
|
1621
2051
|
}
|
|
1622
2052
|
}
|
|
1623
|
-
/* ---- the owner's own hand
|
|
2053
|
+
/* ---- the picture out, and the owner's own hand in ---- */
|
|
1624
2054
|
/**
|
|
1625
|
-
* Start painting this session's tab to
|
|
2055
|
+
* Start painting this session's tab to buddi.
|
|
1626
2056
|
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
2057
|
+
* Two kinds, one per session at a time. `watch: true` is the Canvas's
|
|
2058
|
+
* picture while the agent works: it follows the session's active tab, sends
|
|
2059
|
+
* at most two frames a second, takes no input, and asking for it again with
|
|
2060
|
+
* the same bounds on the same tab changes nothing. Without it, this is the
|
|
2061
|
+
* owner's take-over: pinned to the tab it starts on, ten frames a second,
|
|
2062
|
+
* and the only cast input reaches. Starting either replaces whatever cast
|
|
2063
|
+
* the session had, which is also what a re-tune and a reconnecting
|
|
2064
|
+
* dashboard do.
|
|
2065
|
+
*
|
|
2066
|
+
* The debugger is claimed for the cast's whole life rather than per frame:
|
|
2067
|
+
* frames arrive as events, not as answers, and a detach between them would
|
|
2068
|
+
* end the stream. And focus is emulated on the tab first: a tab in the
|
|
2069
|
+
* background, or in a window that is minimised or behind another, is
|
|
2070
|
+
* "hidden" to Chrome, and a hidden page paints nothing at all for a
|
|
2071
|
+
* screencast. With focus emulated the page renders as if it were in front
|
|
2072
|
+
* (it is told it is visible), and stays where it is.
|
|
1631
2073
|
*/
|
|
1632
2074
|
async #startScreencast(command, cancel) {
|
|
1633
2075
|
const session = await this.#session(command.session);
|
|
1634
2076
|
const tab = await this.#ownTab(session, this.#activeKey(session));
|
|
1635
2077
|
this.#liveUrl(tab);
|
|
1636
2078
|
const tabId = tab.id;
|
|
2079
|
+
const mode = command.args["watch"] === true ? "watch" : "hand";
|
|
2080
|
+
const params = {
|
|
2081
|
+
// Smaller and cheaper than a screenshot on purpose: this is a picture
|
|
2082
|
+
// for a phone on someone else's network, not evidence for a model.
|
|
2083
|
+
quality: Math.min(90, Math.max(20, Math.trunc(num(command.args["quality"]) ?? 50))),
|
|
2084
|
+
maxWidth: side(command.args["maxWidth"], 960),
|
|
2085
|
+
maxHeight: side(command.args["maxHeight"], 600),
|
|
2086
|
+
everyNthFrame: Math.min(10, Math.max(1, Math.trunc(num(command.args["everyNthFrame"]) ?? 1)))
|
|
2087
|
+
};
|
|
2088
|
+
const interval = Math.min(MAX_FRAME_MS, Math.max(MIN_FRAME_MS, Math.trunc(num(command.args["interval"]) ?? (mode === "watch" ? WATCH_FRAME_MS : MIN_FRAME_MS))));
|
|
2089
|
+
const running = this.#casts.get(command.session);
|
|
2090
|
+
if (running && !running.stopped && mode === "watch" && running.mode === "watch" && running.tabId === tabId && running.interval === interval && JSON.stringify(running.params) === JSON.stringify(params)) return {};
|
|
1637
2091
|
cancel.check();
|
|
1638
|
-
await this.#stopScreencast(command.session);
|
|
1639
2092
|
await this.#claimDebugger(tabId);
|
|
1640
|
-
|
|
2093
|
+
await this.#stopScreencast(command.session);
|
|
2094
|
+
const cast = { session: command.session, tabId, mode, interval, params, url: tab.url, lastSentAt: 0, stopped: false };
|
|
1641
2095
|
this.#casts.set(command.session, cast);
|
|
1642
2096
|
this.#castsByTab.set(tabId, cast);
|
|
1643
2097
|
try {
|
|
1644
|
-
await this.#send(tabId, "
|
|
1645
|
-
|
|
1646
|
-
// Smaller and cheaper than a screenshot on purpose: this is a picture
|
|
1647
|
-
// for a phone on someone else's network, not evidence for a model.
|
|
1648
|
-
quality: Math.min(90, Math.max(20, Math.trunc(num(command.args["quality"]) ?? 50))),
|
|
1649
|
-
maxWidth: side(command.args["maxWidth"], 960),
|
|
1650
|
-
maxHeight: side(command.args["maxHeight"], 600),
|
|
1651
|
-
everyNthFrame: Math.min(10, Math.max(1, Math.trunc(num(command.args["everyNthFrame"]) ?? 1)))
|
|
1652
|
-
});
|
|
2098
|
+
await this.#send(tabId, "Emulation.setFocusEmulationEnabled", { enabled: true }).catch(() => void 0);
|
|
2099
|
+
await this.#send(tabId, "Page.startScreencast", { format: "jpeg", ...params });
|
|
1653
2100
|
cancel.check();
|
|
1654
2101
|
} catch (error) {
|
|
1655
2102
|
await this.#stopScreencast(command.session);
|
|
@@ -1657,12 +2104,27 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1657
2104
|
}
|
|
1658
2105
|
return {};
|
|
1659
2106
|
}
|
|
2107
|
+
/**
|
|
2108
|
+
* The agent moved to another tab, or opened one: a watching picture moves
|
|
2109
|
+
* with it. The owner's own take-over never moves; that is their tab.
|
|
2110
|
+
*/
|
|
2111
|
+
async #followWatch(sessionName) {
|
|
2112
|
+
const cast = this.#casts.get(sessionName);
|
|
2113
|
+
if (!cast || cast.mode !== "watch") return;
|
|
2114
|
+
const session = this.#sessions.get(sessionName);
|
|
2115
|
+
const key = session?.active;
|
|
2116
|
+
const tabId = key ? session?.tabs.get(key) : void 0;
|
|
2117
|
+
if (tabId === void 0 || tabId === cast.tabId) return;
|
|
2118
|
+
const { params, interval } = cast;
|
|
2119
|
+
await this.#startScreencast({ id: "follow", name: "screencast.start", session: sessionName, args: { ...params, interval, watch: true } }, new Cancellation()).catch(() => void 0);
|
|
2120
|
+
}
|
|
1660
2121
|
/** Idempotent: a stop for a session with no screencast is an answer, not a failure. */
|
|
1661
2122
|
async #stopScreencast(session) {
|
|
1662
2123
|
const cast = this.#casts.get(session);
|
|
1663
2124
|
if (!cast) return;
|
|
1664
2125
|
this.#forgetCast(cast);
|
|
1665
2126
|
await this.#send(cast.tabId, "Page.stopScreencast").catch(() => void 0);
|
|
2127
|
+
await this.#send(cast.tabId, "Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => void 0);
|
|
1666
2128
|
await this.#releaseDebugger(cast.tabId);
|
|
1667
2129
|
}
|
|
1668
2130
|
/** Drop every trace of a screencast without touching the debugger, for when Chrome already has. */
|
|
@@ -1687,7 +2149,8 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1687
2149
|
this.#offer(cast, { data, metadata: metadataOf(message["metadata"]), sessionId });
|
|
1688
2150
|
}
|
|
1689
2151
|
/**
|
|
1690
|
-
* One painted frame, acked at once and sent at most
|
|
2152
|
+
* One painted frame, acked at once and sent at most once per interval (ten
|
|
2153
|
+
* a second for the owner's hand, two for the Canvas's picture).
|
|
1691
2154
|
*
|
|
1692
2155
|
* The ack comes first, always, and before the throttle rather than after it.
|
|
1693
2156
|
* Chrome paints nothing more until the frame it sent is acknowledged, so a
|
|
@@ -1702,7 +2165,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1702
2165
|
*/
|
|
1703
2166
|
#offer(cast, frame) {
|
|
1704
2167
|
this.#ack(cast, frame.sessionId);
|
|
1705
|
-
const wait = cast.lastSentAt +
|
|
2168
|
+
const wait = cast.lastSentAt + cast.interval - this.#now();
|
|
1706
2169
|
if (wait <= 0) {
|
|
1707
2170
|
this.#emit(cast, frame);
|
|
1708
2171
|
return;
|
|
@@ -1718,7 +2181,8 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1718
2181
|
}
|
|
1719
2182
|
#emit(cast, frame) {
|
|
1720
2183
|
cast.lastSentAt = this.#now();
|
|
1721
|
-
|
|
2184
|
+
const url = cast.url && /^https?:/i.test(cast.url) ? cast.url.slice(0, 2048) : void 0;
|
|
2185
|
+
this.#onFrame({ type: "frame", session: cast.session, data: frame.data, metadata: frame.metadata, sessionId: frame.sessionId, ...url ? { url } : {} });
|
|
1722
2186
|
}
|
|
1723
2187
|
#ack(cast, sessionId) {
|
|
1724
2188
|
void this.#send(cast.tabId, "Page.screencastFrameAck", { sessionId }).catch(() => void 0);
|
|
@@ -1726,14 +2190,15 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1726
2190
|
/**
|
|
1727
2191
|
* A pointer or a key from the owner's hand.
|
|
1728
2192
|
*
|
|
1729
|
-
* Only while
|
|
1730
|
-
* painting it is an agent reaching for coordinates, which this
|
|
1731
|
-
* not do
|
|
1732
|
-
*
|
|
2193
|
+
* Only while the owner's own screencast is running for that session: input
|
|
2194
|
+
* with nothing painting it is an agent reaching for coordinates, which this
|
|
2195
|
+
* backend does not do, and the Canvas's watching picture is not a hand. The
|
|
2196
|
+
* tab is the screencast's, not the session's active one, so a click cannot
|
|
2197
|
+
* land somewhere the owner is not looking at.
|
|
1733
2198
|
*/
|
|
1734
2199
|
async #input(command, cancel) {
|
|
1735
2200
|
const cast = this.#casts.get(command.session);
|
|
1736
|
-
if (!cast) throw new PreconditionError(NO_CAST);
|
|
2201
|
+
if (!cast || cast.mode !== "hand") throw new PreconditionError(NO_CAST);
|
|
1737
2202
|
const session = await this.#session(command.session);
|
|
1738
2203
|
const tab = await this.#chrome.tabs.get(cast.tabId).catch(() => void 0);
|
|
1739
2204
|
if (!tab) {
|
|
@@ -1745,12 +2210,89 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1745
2210
|
throw new PreconditionError(TAKEN);
|
|
1746
2211
|
}
|
|
1747
2212
|
await this.#background(tab, command);
|
|
2213
|
+
if (str(command.args, "kind") === "nav") {
|
|
2214
|
+
cancel.check();
|
|
2215
|
+
cancel.dispatch();
|
|
2216
|
+
await this.#nav(cast.tabId, command.args);
|
|
2217
|
+
return {};
|
|
2218
|
+
}
|
|
1748
2219
|
const event = inputEvent(command.args);
|
|
1749
2220
|
cancel.check();
|
|
1750
2221
|
cancel.dispatch();
|
|
1751
2222
|
await this.#send(cast.tabId, event.method, event.params);
|
|
1752
2223
|
return {};
|
|
1753
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* The window's own buttons while the owner holds the page from the
|
|
2227
|
+
* dashboard: back, forward, reload, or an address they typed (http and
|
|
2228
|
+
* https only; buddi checked it against the allowed sites before it came).
|
|
2229
|
+
*/
|
|
2230
|
+
async #nav(tabId, args) {
|
|
2231
|
+
const action = str(args, "action");
|
|
2232
|
+
if (action === "reload") {
|
|
2233
|
+
await this.#send(tabId, "Page.reload", {});
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (action === "back" || action === "forward") {
|
|
2237
|
+
const history = await this.#send(tabId, "Page.getNavigationHistory");
|
|
2238
|
+
const index = (history?.currentIndex ?? 0) + (action === "back" ? -1 : 1);
|
|
2239
|
+
const entry = history?.entries?.[index];
|
|
2240
|
+
if (!entry) return;
|
|
2241
|
+
await this.#send(tabId, "Page.navigateToHistoryEntry", { entryId: entry.id });
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (action === "navigate") {
|
|
2245
|
+
const url = checkUrl(str(args, "url") ?? "");
|
|
2246
|
+
await this.#send(tabId, "Page.navigate", { url });
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
throw new PreconditionError("That is not a button this window has.");
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* The owner's Cmd/Ctrl+C on the page they hold: the page's selection, or
|
|
2253
|
+
* the selected part of the focused field, read with `Runtime.evaluate` in
|
|
2254
|
+
* the page's own world. A password field is never read; its selection is
|
|
2255
|
+
* nothing. The text goes back to buddi as the answer and is kept by nothing.
|
|
2256
|
+
*/
|
|
2257
|
+
async #copy(session) {
|
|
2258
|
+
const cast = this.#casts.get(session);
|
|
2259
|
+
if (!cast || cast.mode !== "hand") throw new PreconditionError(NO_CAST);
|
|
2260
|
+
const answer = await this.#send(cast.tabId, "Runtime.evaluate", { expression: SELECTION_SOURCE, returnByValue: true }).catch(() => void 0);
|
|
2261
|
+
const value = answer?.result?.value;
|
|
2262
|
+
const text2 = typeof value === "string" ? value.slice(0, MAX_COPY) : "";
|
|
2263
|
+
return { observation: { copied: text2 } };
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Capture, for the owner's Files: the tab's viewport as a PNG, at its own
|
|
2267
|
+
* size, with every password field painted over first and uncovered after.
|
|
2268
|
+
* Whoever holds the page; not one of the agent's steps, and nothing about it
|
|
2269
|
+
* is evidence.
|
|
2270
|
+
*/
|
|
2271
|
+
async #capture(sessionName, cancel) {
|
|
2272
|
+
const session = await this.#session(sessionName);
|
|
2273
|
+
const cast = this.#casts.get(sessionName);
|
|
2274
|
+
const tabId = cast?.tabId ?? (await this.#ownTab(session, this.#activeKey(session))).id;
|
|
2275
|
+
const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
|
|
2276
|
+
if (!tab) throw new PreconditionError("That tab is gone.");
|
|
2277
|
+
this.#liveUrl(tab);
|
|
2278
|
+
cancel.check();
|
|
2279
|
+
await this.#chrome.scripting.executeScript({ target: { tabId, allFrames: true }, func: maskPasswords, args: [true] }).catch(() => void 0);
|
|
2280
|
+
try {
|
|
2281
|
+
const png = await this.#withDebugger(tabId, async (send) => {
|
|
2282
|
+
await send("Emulation.setFocusEmulationEnabled", { enabled: true }).catch(() => void 0);
|
|
2283
|
+
try {
|
|
2284
|
+
const shot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
|
|
2285
|
+
return shot?.data ?? null;
|
|
2286
|
+
} finally {
|
|
2287
|
+
if (!this.#castsByTab.has(tabId)) await send("Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => void 0);
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
if (!png) throw new Error("Chrome did not hand over a picture of that tab.");
|
|
2291
|
+
return { screenshot: png, observation: { url: tab.url ?? "", title: tab.title ?? "" } };
|
|
2292
|
+
} finally {
|
|
2293
|
+
await this.#chrome.scripting.executeScript({ target: { tabId, allFrames: true }, func: maskPasswords, args: [false] }).catch(() => void 0);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
1754
2296
|
async #click(command, cancel) {
|
|
1755
2297
|
const { ref, tab } = await this.#aim(command, cancel);
|
|
1756
2298
|
const { point } = await this.#locate(ref);
|
|
@@ -1760,11 +2302,11 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1760
2302
|
await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: clickRef, args: [ref.local] });
|
|
1761
2303
|
return {};
|
|
1762
2304
|
}
|
|
1763
|
-
await this.#withDebugger(ref.tabId, async (
|
|
2305
|
+
await this.#withDebugger(ref.tabId, async (send) => {
|
|
1764
2306
|
const base = { x: point.x, y: point.y, button: "left", clickCount: 1, buttons: 1 };
|
|
1765
|
-
await
|
|
1766
|
-
await
|
|
1767
|
-
await
|
|
2307
|
+
await send("Input.dispatchMouseEvent", { ...base, type: "mouseMoved", buttons: 0 });
|
|
2308
|
+
await send("Input.dispatchMouseEvent", { ...base, type: "mousePressed" });
|
|
2309
|
+
await send("Input.dispatchMouseEvent", { ...base, type: "mouseReleased" });
|
|
1768
2310
|
});
|
|
1769
2311
|
await this.#settle(ref.tabId, tab);
|
|
1770
2312
|
return {};
|
|
@@ -1779,7 +2321,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1779
2321
|
await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: typeRef, args: [ref.local, value] });
|
|
1780
2322
|
return {};
|
|
1781
2323
|
}
|
|
1782
|
-
await this.#withDebugger(ref.tabId, (
|
|
2324
|
+
await this.#withDebugger(ref.tabId, (send) => send("Input.insertText", { text: value }));
|
|
1783
2325
|
return {};
|
|
1784
2326
|
}
|
|
1785
2327
|
/* ---- the owner's secret: one read to aim it, one write that delivers it ---- */
|
|
@@ -1841,7 +2383,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1841
2383
|
await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: typeRef, args: [ref.local, value] });
|
|
1842
2384
|
return {};
|
|
1843
2385
|
}
|
|
1844
|
-
await this.#withDebugger(ref.tabId, (
|
|
2386
|
+
await this.#withDebugger(ref.tabId, (send) => send("Input.insertText", { text: value }));
|
|
1845
2387
|
return {};
|
|
1846
2388
|
}
|
|
1847
2389
|
async #press(command, cancel) {
|
|
@@ -1856,10 +2398,10 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1856
2398
|
await this.#chrome.scripting.executeScript({ target: { tabId: ref.tabId, frameIds: [ref.frameId] }, func: pressRef, args: [ref.local, descriptor.key, descriptor.code, descriptor.keyCode] });
|
|
1857
2399
|
return {};
|
|
1858
2400
|
}
|
|
1859
|
-
await this.#withDebugger(ref.tabId, async (
|
|
2401
|
+
await this.#withDebugger(ref.tabId, async (send) => {
|
|
1860
2402
|
const common = { key: descriptor.key, code: descriptor.code, windowsVirtualKeyCode: descriptor.keyCode, nativeVirtualKeyCode: descriptor.keyCode };
|
|
1861
|
-
await
|
|
1862
|
-
await
|
|
2403
|
+
await send("Input.dispatchKeyEvent", { ...common, type: descriptor.text ? "keyDown" : "rawKeyDown", ...descriptor.text ? { text: descriptor.text } : {} });
|
|
2404
|
+
await send("Input.dispatchKeyEvent", { ...common, type: "keyUp" });
|
|
1863
2405
|
});
|
|
1864
2406
|
await this.#settle(ref.tabId, tab);
|
|
1865
2407
|
return {};
|
|
@@ -1873,264 +2415,173 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
|
|
|
1873
2415
|
func: chooseRef,
|
|
1874
2416
|
args: [ref.local, value]
|
|
1875
2417
|
});
|
|
1876
|
-
if (!frame?.result?.ok) throw new PreconditionError(frame?.result?.reason ?? "That option is not in the list.");
|
|
1877
|
-
return {};
|
|
1878
|
-
}
|
|
1879
|
-
/**
|
|
1880
|
-
* Scroll names no element, so what it checks instead is the observation: the
|
|
1881
|
-
* page the model is reading has to be the page still in the tab.
|
|
1882
|
-
*/
|
|
1883
|
-
async #scroll(command, cancel) {
|
|
1884
|
-
const session = await this.#session(command.session);
|
|
1885
|
-
const observed = session.observed;
|
|
1886
|
-
if (!observed || observed.generation !== session.generation) throw new PreconditionError("Stale page observation. Observe again before scrolling.");
|
|
1887
|
-
const tab = await this.#ownTab(session, observed.tabKey);
|
|
1888
|
-
if (this.#liveUrl(tab) !== observed.url) throw new PreconditionError(MOVED);
|
|
1889
|
-
await this.#background(tab, command);
|
|
1890
|
-
const tabId = tab.id;
|
|
1891
|
-
const direction = str(command.args, "direction") === "up" ? "up" : "down";
|
|
1892
|
-
cancel.check();
|
|
1893
|
-
await this.#inject(tabId);
|
|
1894
|
-
cancel.check();
|
|
1895
|
-
cancel.dispatch();
|
|
1896
|
-
await this.#chrome.scripting.executeScript({ target: { tabId }, func: scrollPage, args: [direction] });
|
|
1897
|
-
return {};
|
|
1898
|
-
}
|
|
1899
|
-
async #screenshot(sessionId, cancel) {
|
|
1900
|
-
const session = await this.#session(sessionId);
|
|
1901
|
-
const tab = await this.#ownTab(session, this.#activeKey(session));
|
|
1902
|
-
this.#liveUrl(tab);
|
|
1903
|
-
const tabId = tab.id;
|
|
1904
|
-
cancel.check();
|
|
1905
|
-
return this.#withDebugger(tabId, async (
|
|
1906
|
-
const shot = await
|
|
1907
|
-
return shot?.data ?? null;
|
|
1908
|
-
}).catch(() => null);
|
|
1909
|
-
}
|
|
1910
|
-
};
|
|
1911
|
-
function readReadyState() {
|
|
1912
|
-
return document.readyState;
|
|
1913
|
-
}
|
|
1914
|
-
function readObservation() {
|
|
1915
|
-
const api = globalThis["__buddiBrowser"];
|
|
1916
|
-
return api ? api.observe() : null;
|
|
1917
|
-
}
|
|
1918
|
-
function locateRef(ref, options) {
|
|
1919
|
-
const api = globalThis["__buddiBrowser"];
|
|
1920
|
-
return api ? api.locate(ref, options) : { ok: false, reason: "The page was reloaded. Observe again." };
|
|
1921
|
-
}
|
|
1922
|
-
function chooseRef(ref, value) {
|
|
1923
|
-
const api = globalThis["__buddiBrowser"];
|
|
1924
|
-
return api ? api.choose(ref, value) : { ok: false, reason: "The page was reloaded. Observe again." };
|
|
1925
|
-
}
|
|
1926
|
-
function scrollPage(direction) {
|
|
1927
|
-
const api = globalThis["__buddiBrowser"];
|
|
1928
|
-
api?.scroll(direction);
|
|
1929
|
-
}
|
|
1930
|
-
function clickRef(ref) {
|
|
1931
|
-
const api = globalThis["__buddiBrowser"];
|
|
1932
|
-
api?.locate(ref, {});
|
|
1933
|
-
const holder = globalThis["__buddiBrowserRegistry"];
|
|
1934
|
-
const element = holder?.refs.get(ref)?.deref();
|
|
1935
|
-
element?.click();
|
|
1936
|
-
}
|
|
1937
|
-
function typeRef(ref, value) {
|
|
1938
|
-
const holder = globalThis["__buddiBrowserRegistry"];
|
|
1939
|
-
const element = holder?.refs.get(ref)?.deref();
|
|
1940
|
-
if (!element) return;
|
|
1941
|
-
element.value = value;
|
|
1942
|
-
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1943
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1944
|
-
}
|
|
1945
|
-
function readFieldRef(ref) {
|
|
1946
|
-
const api = globalThis["__buddiBrowser"];
|
|
1947
|
-
const located = api?.locate(ref, {});
|
|
1948
|
-
if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
|
|
1949
|
-
return { ok: true, origin: location.origin, password: located.password === true, name: located.name ?? "" };
|
|
1950
|
-
}
|
|
1951
|
-
function prepareSecretRef(ref, expectedOrigin) {
|
|
1952
|
-
const api = globalThis["__buddiBrowser"];
|
|
1953
|
-
const located = api?.locate(ref, {});
|
|
1954
|
-
if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
|
|
1955
|
-
const origin = location.origin;
|
|
1956
|
-
if (origin !== expectedOrigin) return { ok: false, reason: "The page moved since the owner approved this fill. Nothing was entered." };
|
|
1957
|
-
const holder = globalThis["__buddiBrowserRegistry"];
|
|
1958
|
-
const element = holder?.refs.get(ref)?.deref();
|
|
1959
|
-
if (!element || !element.isConnected) return { ok: false, reason: "The referenced element changed or disappeared." };
|
|
1960
|
-
if ("focus" in element) element.focus();
|
|
1961
|
-
const input = element;
|
|
1962
|
-
if ("value" in input) {
|
|
1963
|
-
input.value = "";
|
|
1964
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1965
|
-
}
|
|
1966
|
-
return { ok: true, origin, password: located.password === true };
|
|
1967
|
-
}
|
|
1968
|
-
function pressRef(ref, key, code, keyCode) {
|
|
1969
|
-
const holder = globalThis["__buddiBrowserRegistry"];
|
|
1970
|
-
const element = holder?.refs.get(ref)?.deref();
|
|
1971
|
-
if (!element) return;
|
|
1972
|
-
for (const type of ["keydown", "keypress", "keyup"]) {
|
|
1973
|
-
element.dispatchEvent(new KeyboardEvent(type, { key, code, keyCode, bubbles: true, cancelable: true }));
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
function heldLoginFacts(raw) {
|
|
1977
|
-
if (!raw || typeof raw !== "object") return void 0;
|
|
1978
|
-
const { never, saved } = raw;
|
|
1979
|
-
const sites = Array.isArray(never) ? never.filter((site) => typeof site === "string" && site.length <= 253).slice(0, 500) : [];
|
|
1980
|
-
const kept = Array.isArray(saved) ? saved.filter((login) => typeof login?.site === "string" && typeof login?.username === "string").slice(0, 500).map(({ site, username }) => ({ site, username })) : [];
|
|
1981
|
-
return { never: sites, saved: kept };
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
// src/downloads.ts
|
|
1985
|
-
var DOWNLOAD_GRACE_MS = 15e3;
|
|
1986
|
-
var DOWNLOADS_PERMISSION = "downloads";
|
|
1987
|
-
function originOf(url) {
|
|
1988
|
-
if (!url) return void 0;
|
|
1989
|
-
try {
|
|
1990
|
-
const parsed = new URL(url);
|
|
1991
|
-
if (parsed.protocol === "blob:") return new URL(parsed.pathname).origin;
|
|
1992
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.origin : void 0;
|
|
1993
|
-
} catch {
|
|
1994
|
-
return void 0;
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
function basename(file) {
|
|
1998
|
-
return file.split(/[\\/]/).pop() || "download";
|
|
1999
|
-
}
|
|
2000
|
-
var AgentDownloads = class {
|
|
2001
|
-
#actions = /* @__PURE__ */ new Map();
|
|
2002
|
-
#next = 1;
|
|
2003
|
-
#claimed = /* @__PURE__ */ new Map();
|
|
2004
|
-
#api;
|
|
2005
|
-
#send;
|
|
2006
|
-
#now;
|
|
2007
|
-
constructor(options) {
|
|
2008
|
-
this.#send = options.send;
|
|
2009
|
-
this.#now = options.now ?? (() => Date.now());
|
|
2010
|
-
}
|
|
2011
|
-
/** Chrome's downloads API, once the owner granted it. Listening starts here, once. */
|
|
2012
|
-
attach(api) {
|
|
2013
|
-
if (!api || this.#api) return;
|
|
2014
|
-
this.#api = api;
|
|
2015
|
-
api.onCreated.addListener((item) => this.created(item));
|
|
2016
|
-
api.onChanged.addListener((delta) => {
|
|
2017
|
-
void this.changed(delta).catch(() => void 0);
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
get attached() {
|
|
2021
|
-
return this.#api !== void 0;
|
|
2022
|
-
}
|
|
2023
|
-
/** An agent command starts in this session, whose tabs are on these addresses. Returns the command's handle for `end`. */
|
|
2024
|
-
begin(session, urls) {
|
|
2025
|
-
this.#expire();
|
|
2026
|
-
const id = this.#next++;
|
|
2027
|
-
const origins = /* @__PURE__ */ new Set();
|
|
2028
|
-
for (const url of urls) {
|
|
2029
|
-
const origin = originOf(url);
|
|
2030
|
-
if (origin) origins.add(origin);
|
|
2031
|
-
}
|
|
2032
|
-
this.#actions.set(id, { session, origins, running: true, until: 0, at: this.#now() });
|
|
2033
|
-
return id;
|
|
2034
|
-
}
|
|
2035
|
-
/** That command ended; its tabs may have moved on to these. Its downloads still count for a few seconds, its own. */
|
|
2036
|
-
end(id, urls = []) {
|
|
2037
|
-
const action = this.#actions.get(id);
|
|
2038
|
-
if (!action) return;
|
|
2039
|
-
for (const url of urls) {
|
|
2040
|
-
const origin = originOf(url);
|
|
2041
|
-
if (origin) action.origins.add(origin);
|
|
2042
|
-
}
|
|
2043
|
-
action.running = false;
|
|
2044
|
-
action.until = this.#now() + DOWNLOAD_GRACE_MS;
|
|
2045
|
-
}
|
|
2046
|
-
/** Commands whose few seconds are over count for nothing any more. */
|
|
2047
|
-
#expire() {
|
|
2048
|
-
const now = this.#now();
|
|
2049
|
-
for (const [id, action] of this.#actions) if (!action.running && now > action.until) this.#actions.delete(id);
|
|
2050
|
-
}
|
|
2051
|
-
/** The session ended or the socket went: nothing more is its. */
|
|
2052
|
-
forget(session) {
|
|
2053
|
-
if (session === void 0) {
|
|
2054
|
-
this.#actions.clear();
|
|
2055
|
-
this.#claimed.clear();
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
for (const [id, action] of this.#actions) if (action.session === session) this.#actions.delete(id);
|
|
2059
|
-
for (const [id, owner] of this.#claimed) if (owner === session) this.#claimed.delete(id);
|
|
2060
|
-
}
|
|
2061
|
-
/** Which session's command this download belongs to, if any. */
|
|
2062
|
-
#owner(item) {
|
|
2063
|
-
if (item.byExtensionId) return void 0;
|
|
2064
|
-
const origin = originOf(item.referrer) ?? originOf(item.finalUrl ?? item.url);
|
|
2065
|
-
if (!origin) return void 0;
|
|
2066
|
-
this.#expire();
|
|
2067
|
-
let best;
|
|
2068
|
-
for (const action of this.#actions.values()) {
|
|
2069
|
-
if (!action.origins.has(origin)) continue;
|
|
2070
|
-
if (!best || action.at > best.at) best = action;
|
|
2071
|
-
}
|
|
2072
|
-
return best?.session;
|
|
2073
|
-
}
|
|
2074
|
-
created(item) {
|
|
2075
|
-
const session = this.#owner(item);
|
|
2076
|
-
if (session) this.#claimed.set(item.id, session);
|
|
2077
|
-
}
|
|
2078
|
-
async changed(delta) {
|
|
2079
|
-
const session = this.#claimed.get(delta.id);
|
|
2080
|
-
const state = delta.state?.current;
|
|
2081
|
-
if (!session || !state) return;
|
|
2082
|
-
if (state === "interrupted") {
|
|
2083
|
-
this.#claimed.delete(delta.id);
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
if (state !== "complete") return;
|
|
2087
|
-
this.#claimed.delete(delta.id);
|
|
2088
|
-
const [item] = await (this.#api?.search({ id: delta.id }) ?? Promise.resolve([]));
|
|
2089
|
-
if (!item?.filename) return;
|
|
2090
|
-
const url = item.finalUrl || item.url || "";
|
|
2091
|
-
const size = item.fileSize !== void 0 && item.fileSize >= 0 ? item.fileSize : item.totalBytes !== void 0 && item.totalBytes >= 0 ? item.totalBytes : void 0;
|
|
2092
|
-
this.#send({
|
|
2093
|
-
type: "download",
|
|
2094
|
-
session,
|
|
2095
|
-
path: item.filename,
|
|
2096
|
-
filename: basename(item.filename),
|
|
2097
|
-
url,
|
|
2098
|
-
...item.mime ? { mime: item.mime } : {},
|
|
2099
|
-
...size !== void 0 ? { size } : {}
|
|
2100
|
-
});
|
|
2418
|
+
if (!frame?.result?.ok) throw new PreconditionError(frame?.result?.reason ?? "That option is not in the list.");
|
|
2419
|
+
return {};
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Scroll names no element, so what it checks instead is the observation: the
|
|
2423
|
+
* page the model is reading has to be the page still in the tab.
|
|
2424
|
+
*/
|
|
2425
|
+
async #scroll(command, cancel) {
|
|
2426
|
+
const session = await this.#session(command.session);
|
|
2427
|
+
const observed = session.observed;
|
|
2428
|
+
if (!observed || observed.generation !== session.generation) throw new PreconditionError("Stale page observation. Observe again before scrolling.");
|
|
2429
|
+
const tab = await this.#ownTab(session, observed.tabKey);
|
|
2430
|
+
if (this.#liveUrl(tab) !== observed.url) throw new PreconditionError(MOVED);
|
|
2431
|
+
await this.#background(tab, command);
|
|
2432
|
+
const tabId = tab.id;
|
|
2433
|
+
const direction = str(command.args, "direction") === "up" ? "up" : "down";
|
|
2434
|
+
cancel.check();
|
|
2435
|
+
await this.#inject(tabId);
|
|
2436
|
+
cancel.check();
|
|
2437
|
+
cancel.dispatch();
|
|
2438
|
+
await this.#chrome.scripting.executeScript({ target: { tabId }, func: scrollPage, args: [direction] });
|
|
2439
|
+
return {};
|
|
2440
|
+
}
|
|
2441
|
+
async #screenshot(sessionId, cancel) {
|
|
2442
|
+
const session = await this.#session(sessionId);
|
|
2443
|
+
const tab = await this.#ownTab(session, this.#activeKey(session));
|
|
2444
|
+
this.#liveUrl(tab);
|
|
2445
|
+
const tabId = tab.id;
|
|
2446
|
+
cancel.check();
|
|
2447
|
+
return this.#withDebugger(tabId, async (send) => {
|
|
2448
|
+
const shot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
|
|
2449
|
+
return shot?.data ?? null;
|
|
2450
|
+
}).catch(() => null);
|
|
2101
2451
|
}
|
|
2102
2452
|
};
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2453
|
+
var SELECTION_SOURCE = `(() => {
|
|
2454
|
+
const active = document.activeElement;
|
|
2455
|
+
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) {
|
|
2456
|
+
if (active.tagName === 'INPUT' && String(active.type).toLowerCase() === 'password') return '';
|
|
2457
|
+
const start = active.selectionStart, end = active.selectionEnd;
|
|
2458
|
+
if (typeof start === 'number' && typeof end === 'number' && end > start) return String(active.value).slice(start, end).slice(0, ${MAX_COPY});
|
|
2459
|
+
}
|
|
2460
|
+
const selection = window.getSelection();
|
|
2461
|
+
return selection ? String(selection).slice(0, ${MAX_COPY}) : '';
|
|
2462
|
+
})()`;
|
|
2463
|
+
function maskPasswords(on) {
|
|
2464
|
+
const id = "buddi-capture-mask";
|
|
2465
|
+
document.getElementById(id)?.remove();
|
|
2466
|
+
if (!on) return;
|
|
2467
|
+
const style = document.createElement("style");
|
|
2468
|
+
style.id = id;
|
|
2469
|
+
style.textContent = "input[type=password]{background:#c3d0e3 !important;color:transparent !important;-webkit-text-fill-color:transparent !important;text-shadow:none !important;caret-color:transparent !important;}";
|
|
2470
|
+
(document.head ?? document.documentElement).appendChild(style);
|
|
2471
|
+
}
|
|
2472
|
+
function readReadyState() {
|
|
2473
|
+
return document.readyState;
|
|
2474
|
+
}
|
|
2475
|
+
function readObservation() {
|
|
2476
|
+
const api = globalThis["__buddiBrowser"];
|
|
2477
|
+
return api ? api.observe() : null;
|
|
2478
|
+
}
|
|
2479
|
+
function locateRef(ref, options) {
|
|
2480
|
+
const api = globalThis["__buddiBrowser"];
|
|
2481
|
+
return api ? api.locate(ref, options) : { ok: false, reason: "The page was reloaded. Observe again." };
|
|
2482
|
+
}
|
|
2483
|
+
function chooseRef(ref, value) {
|
|
2484
|
+
const api = globalThis["__buddiBrowser"];
|
|
2485
|
+
return api ? api.choose(ref, value) : { ok: false, reason: "The page was reloaded. Observe again." };
|
|
2486
|
+
}
|
|
2487
|
+
function scrollPage(direction) {
|
|
2488
|
+
const api = globalThis["__buddiBrowser"];
|
|
2489
|
+
api?.scroll(direction);
|
|
2490
|
+
}
|
|
2491
|
+
function clickRef(ref) {
|
|
2492
|
+
const api = globalThis["__buddiBrowser"];
|
|
2493
|
+
api?.locate(ref, {});
|
|
2494
|
+
const holder = globalThis["__buddiBrowserRegistry"];
|
|
2495
|
+
const element = holder?.refs.get(ref)?.deref();
|
|
2496
|
+
element?.click();
|
|
2497
|
+
}
|
|
2498
|
+
function typeRef(ref, value) {
|
|
2499
|
+
const holder = globalThis["__buddiBrowserRegistry"];
|
|
2500
|
+
const element = holder?.refs.get(ref)?.deref();
|
|
2501
|
+
if (!element) return;
|
|
2502
|
+
element.value = value;
|
|
2503
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2504
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2505
|
+
}
|
|
2506
|
+
function readFieldRef(ref) {
|
|
2507
|
+
const api = globalThis["__buddiBrowser"];
|
|
2508
|
+
const located = api?.locate(ref, {});
|
|
2509
|
+
if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
|
|
2510
|
+
return { ok: true, origin: location.origin, password: located.password === true, name: located.name ?? "" };
|
|
2511
|
+
}
|
|
2512
|
+
function prepareSecretRef(ref, expectedOrigin) {
|
|
2513
|
+
const api = globalThis["__buddiBrowser"];
|
|
2514
|
+
const located = api?.locate(ref, {});
|
|
2515
|
+
if (!located?.ok) return { ok: false, reason: located?.reason ?? "The referenced element changed or disappeared." };
|
|
2516
|
+
const origin = location.origin;
|
|
2517
|
+
if (origin !== expectedOrigin) return { ok: false, reason: "The page moved since the owner approved this fill. Nothing was entered." };
|
|
2518
|
+
const holder = globalThis["__buddiBrowserRegistry"];
|
|
2519
|
+
const element = holder?.refs.get(ref)?.deref();
|
|
2520
|
+
if (!element || !element.isConnected) return { ok: false, reason: "The referenced element changed or disappeared." };
|
|
2521
|
+
if ("focus" in element) element.focus();
|
|
2522
|
+
const input = element;
|
|
2523
|
+
if ("value" in input) {
|
|
2524
|
+
input.value = "";
|
|
2525
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2112
2526
|
}
|
|
2113
|
-
|
|
2114
|
-
return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname);
|
|
2527
|
+
return { ok: true, origin, password: located.password === true };
|
|
2115
2528
|
}
|
|
2116
|
-
function
|
|
2117
|
-
const
|
|
2118
|
-
const
|
|
2119
|
-
if (
|
|
2120
|
-
|
|
2529
|
+
function pressRef(ref, key, code, keyCode) {
|
|
2530
|
+
const holder = globalThis["__buddiBrowserRegistry"];
|
|
2531
|
+
const element = holder?.refs.get(ref)?.deref();
|
|
2532
|
+
if (!element) return;
|
|
2533
|
+
for (const type of ["keydown", "keypress", "keyup"]) {
|
|
2534
|
+
element.dispatchEvent(new KeyboardEvent(type, { key, code, keyCode, bubbles: true, cancelable: true }));
|
|
2535
|
+
}
|
|
2121
2536
|
}
|
|
2122
|
-
function
|
|
2123
|
-
if (!
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
return
|
|
2537
|
+
function heldLoginFacts(raw) {
|
|
2538
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2539
|
+
const { never, saved } = raw;
|
|
2540
|
+
const sites = Array.isArray(never) ? never.filter((site) => typeof site === "string" && site.length <= 253).slice(0, 500) : [];
|
|
2541
|
+
const kept = Array.isArray(saved) ? saved.filter((login) => typeof login?.site === "string" && typeof login?.username === "string").slice(0, 500).map(({ site, username }) => ({ site, username })) : [];
|
|
2542
|
+
return { never: sites, saved: kept };
|
|
2128
2543
|
}
|
|
2129
2544
|
|
|
2130
|
-
// src/
|
|
2131
|
-
var
|
|
2132
|
-
var
|
|
2133
|
-
var
|
|
2545
|
+
// src/frames.ts
|
|
2546
|
+
var BINARY_FRAMES = "frames.binary";
|
|
2547
|
+
var VERSION = 1;
|
|
2548
|
+
var MAX_HEADER = 8 * 1024;
|
|
2549
|
+
var encoder2 = new TextEncoder();
|
|
2550
|
+
var decoder = new TextDecoder();
|
|
2551
|
+
function base64Bytes(data) {
|
|
2552
|
+
const binary = atob(data);
|
|
2553
|
+
const bytes = new Uint8Array(binary.length);
|
|
2554
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
2555
|
+
return bytes;
|
|
2556
|
+
}
|
|
2557
|
+
function packFrame(header, jpeg) {
|
|
2558
|
+
const head = encoder2.encode(JSON.stringify(header));
|
|
2559
|
+
if (head.length > MAX_HEADER) throw new Error("A frame header that long is not a frame header.");
|
|
2560
|
+
const out = new Uint8Array(3 + head.length + jpeg.length);
|
|
2561
|
+
out[0] = VERSION;
|
|
2562
|
+
out[1] = head.length >> 8 & 255;
|
|
2563
|
+
out[2] = head.length & 255;
|
|
2564
|
+
out.set(head, 3);
|
|
2565
|
+
out.set(jpeg, 3 + head.length);
|
|
2566
|
+
return out;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// src/link.ts
|
|
2570
|
+
var BACKOFF_MS = [1e3, 2e3, 5e3, 1e4, 2e4, 3e4];
|
|
2571
|
+
var REPAIR_GAP_MS = 3e4;
|
|
2572
|
+
var NO_ANSWER2 = "buddi did not answer at this address. Is it running?";
|
|
2573
|
+
var OTHER_BROWSER = "This buddi is paired with another copy of the extension (the Chrome Web Store one, or another unpacked one). In its Settings \u2192 Browser & apps choose Pair again, then press Try again here.";
|
|
2574
|
+
var TURNED_AWAY = "buddi is running at this address but turned this browser away. Update buddi, then press Try again.";
|
|
2575
|
+
function readClose(code, reason) {
|
|
2576
|
+
const said = reason.trim();
|
|
2577
|
+
if (/paired with another browser/i.test(said)) return { kind: "refused", message: OTHER_BROWSER };
|
|
2578
|
+
if (/token is not this buddi/i.test(said)) return { kind: "repair", message: "This buddi no longer knows this browser. Asking it for a new code." };
|
|
2579
|
+
if (/forgot its browser/i.test(said)) return { kind: "repair", message: "This buddi forgot this browser. Asking it for a new code." };
|
|
2580
|
+
if (code === 1008 && !/too many pairing requests|one hello per connection/i.test(said)) {
|
|
2581
|
+
return { kind: "refused", message: said ? `This buddi turned this browser away: ${said}. Press Try again.` : TURNED_AWAY };
|
|
2582
|
+
}
|
|
2583
|
+
return { kind: "retry", message: said ? `buddi closed the connection (${said}). Trying again shortly.` : "buddi closed the connection. Trying again shortly." };
|
|
2584
|
+
}
|
|
2134
2585
|
var KNOCK_MS = 3e3;
|
|
2135
2586
|
var SILENCE = 7e4;
|
|
2136
2587
|
var KEEPALIVE_MS = 2e4;
|
|
@@ -2148,136 +2599,321 @@ function safely(work) {
|
|
|
2148
2599
|
console.warn(LOST);
|
|
2149
2600
|
}
|
|
2150
2601
|
}
|
|
2151
|
-
var socket;
|
|
2152
2602
|
var DURABLE = /* @__PURE__ */ new Set(["hello", "auth", "result"]);
|
|
2153
|
-
var pending = [];
|
|
2154
2603
|
var frameType = (frame) => frame && typeof frame === "object" ? String(frame.type ?? "") : "";
|
|
2155
|
-
var
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2604
|
+
var Link = class {
|
|
2605
|
+
id;
|
|
2606
|
+
origin;
|
|
2607
|
+
commands;
|
|
2608
|
+
protocol;
|
|
2609
|
+
downloads;
|
|
2610
|
+
#options;
|
|
2611
|
+
#socket;
|
|
2612
|
+
/** Text frames waiting for the socket that is still connecting; discarded if it never opens. */
|
|
2613
|
+
#pending = [];
|
|
2614
|
+
#attempt = 0;
|
|
2615
|
+
#timer;
|
|
2616
|
+
#silence;
|
|
2617
|
+
#keepalive;
|
|
2618
|
+
#last = { connection: "offline", code: null, installation: null, error: null };
|
|
2619
|
+
/** The tail of the frame queue: every frame waits for the one before it. */
|
|
2620
|
+
#incoming = Promise.resolve();
|
|
2621
|
+
/** Switched off or removed: no reconnects, nothing sent. */
|
|
2622
|
+
#disposed = false;
|
|
2623
|
+
/** Turned away by the buddi: no reconnects until the owner presses Try again (or pairs again). */
|
|
2624
|
+
#halted = false;
|
|
2625
|
+
/** Why this side closed the socket, when it did. */
|
|
2626
|
+
#closing;
|
|
2627
|
+
#lastRepair = -Infinity;
|
|
2628
|
+
constructor(options) {
|
|
2629
|
+
this.#options = options;
|
|
2630
|
+
this.id = options.id;
|
|
2631
|
+
this.origin = options.origin;
|
|
2632
|
+
this.downloads = new AgentDownloads({ send: (frame) => this.send(frame) });
|
|
2633
|
+
this.commands = new BrowserCommands(options.chrome, {
|
|
2634
|
+
// Screencast frames are not answers to anything: they arrive while the
|
|
2635
|
+
// agent or the owner is driving and go straight out, outside the
|
|
2636
|
+
// command/result pairing.
|
|
2637
|
+
onFrame: (frame) => this.sendFrame(frame),
|
|
2638
|
+
onEvent: (event) => this.send(event),
|
|
2639
|
+
onLogin: (frame) => this.send(frame),
|
|
2640
|
+
downloads: this.downloads,
|
|
2641
|
+
instance: options.id,
|
|
2642
|
+
groups: options.groups,
|
|
2643
|
+
groupTitle: options.title,
|
|
2644
|
+
groupColour: options.colour
|
|
2645
|
+
});
|
|
2646
|
+
this.protocol = new Protocol({
|
|
2647
|
+
tokens: options.store.tokens(options.id),
|
|
2648
|
+
version: options.version,
|
|
2649
|
+
send: (frame) => this.send(frame),
|
|
2650
|
+
execute: (command, cancel) => this.commands.run(command, cancel),
|
|
2651
|
+
// The socket ended: sessions and refs go with it. The tabs do not.
|
|
2652
|
+
onReset: () => this.commands.reset(),
|
|
2653
|
+
onLoginAck: (id, answer) => this.commands.logins.ack(id, answer),
|
|
2654
|
+
onState: (state) => {
|
|
2655
|
+
if (state.connection === "paired") this.#attempt = 0;
|
|
2656
|
+
const named2 = state.name && state.name !== this.#last.name ? state.name : null;
|
|
2657
|
+
this.#last = state;
|
|
2658
|
+
if (named2) {
|
|
2659
|
+
this.#options.onName?.(this.id, named2);
|
|
2660
|
+
safely(() => this.commands.retitle());
|
|
2661
|
+
}
|
|
2662
|
+
this.#options.onState?.(this.id, state);
|
|
2663
|
+
},
|
|
2664
|
+
disconnect: (reason) => {
|
|
2665
|
+
this.#closing = reason;
|
|
2666
|
+
this.#socket?.close();
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
state() {
|
|
2671
|
+
return { ...this.#last };
|
|
2672
|
+
}
|
|
2673
|
+
attachDownloads(api) {
|
|
2674
|
+
if (api) this.downloads.attach(api);
|
|
2675
|
+
}
|
|
2676
|
+
get #WebSocket() {
|
|
2677
|
+
return this.#options.WebSocket ?? WebSocket;
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* One way out for everything this link says, so a frame and a result travel
|
|
2681
|
+
* the same socket.
|
|
2682
|
+
*
|
|
2683
|
+
* A socket that is still CONNECTING throws on `send`, and the worker reconnects
|
|
2684
|
+
* often enough — an alarm, a pong, a screencast that outlived the last socket —
|
|
2685
|
+
* that something always arrives early. So this is the only place that decides:
|
|
2686
|
+
* send it, queue it, or let it go.
|
|
2687
|
+
*/
|
|
2688
|
+
send(frame) {
|
|
2689
|
+
const live = this.#socket;
|
|
2690
|
+
const text2 = JSON.stringify(frame);
|
|
2691
|
+
const Socket = this.#WebSocket;
|
|
2692
|
+
if (live && live.readyState === Socket.OPEN) {
|
|
2693
|
+
try {
|
|
2694
|
+
live.send(text2);
|
|
2695
|
+
} catch {
|
|
2696
|
+
}
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
if (live && live.readyState === Socket.CONNECTING && DURABLE.has(frameType(frame))) this.#pending.push(text2);
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* A screencast frame: as bytes to a gateway that said it reads them (the
|
|
2703
|
+
* picture, without base64 inside JSON), as the old JSON frame otherwise.
|
|
2704
|
+
* Never queued: a frame that missed its socket is stale.
|
|
2705
|
+
*/
|
|
2706
|
+
sendFrame(frame) {
|
|
2707
|
+
const live = this.#socket;
|
|
2708
|
+
if (!live || live.readyState !== this.#WebSocket.OPEN) return;
|
|
2709
|
+
if (!(this.#last.features ?? []).includes(BINARY_FRAMES)) {
|
|
2710
|
+
this.send(frame);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2159
2713
|
try {
|
|
2160
|
-
|
|
2714
|
+
const bytes = packFrame({ session: frame.session, ...frame.metadata, ...frame.url ? { url: frame.url } : {} }, base64Bytes(frame.data));
|
|
2715
|
+
live.send(bytes);
|
|
2161
2716
|
} catch {
|
|
2162
2717
|
}
|
|
2163
|
-
return;
|
|
2164
2718
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2719
|
+
/** Everything that waited for this socket, in the order it was said. */
|
|
2720
|
+
#flush(open) {
|
|
2721
|
+
const queued = this.#pending;
|
|
2722
|
+
this.#pending = [];
|
|
2723
|
+
for (const text2 of queued) {
|
|
2724
|
+
if (open.readyState !== this.#WebSocket.OPEN) return;
|
|
2725
|
+
try {
|
|
2726
|
+
open.send(text2);
|
|
2727
|
+
} catch {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
#quiet() {
|
|
2733
|
+
if (this.#silence) clearTimeout(this.#silence);
|
|
2734
|
+
this.#silence = setTimeout(() => this.#socket?.close(), SILENCE);
|
|
2735
|
+
}
|
|
2736
|
+
/** Connect now, unless a socket is already open or opening. */
|
|
2737
|
+
async connect() {
|
|
2738
|
+
if (this.#disposed || this.#halted) return;
|
|
2739
|
+
const Socket = this.#WebSocket;
|
|
2740
|
+
const socket = this.#socket;
|
|
2741
|
+
if (socket && (socket.readyState === Socket.OPEN || socket.readyState === Socket.CONNECTING)) return;
|
|
2742
|
+
if (this.#timer) {
|
|
2743
|
+
clearTimeout(this.#timer);
|
|
2744
|
+
this.#timer = void 0;
|
|
2745
|
+
}
|
|
2746
|
+
this.#socket = void 0;
|
|
2747
|
+
this.#pending = [];
|
|
2748
|
+
this.commands.reset();
|
|
2749
|
+
let url;
|
|
2172
2750
|
try {
|
|
2173
|
-
|
|
2174
|
-
} catch {
|
|
2751
|
+
url = socketUrl(this.origin);
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
this.protocol.closed(error instanceof Error ? error.message : String(error));
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
if (this.#attempt > 0 && !await answering(url)) {
|
|
2757
|
+
if (this.#disposed) return;
|
|
2758
|
+
this.protocol.closed(NO_ANSWER2);
|
|
2759
|
+
this.#schedule();
|
|
2175
2760
|
return;
|
|
2176
2761
|
}
|
|
2762
|
+
if (this.#disposed) return;
|
|
2763
|
+
let opened = false;
|
|
2764
|
+
this.#closing = void 0;
|
|
2765
|
+
const opening = new Socket(url);
|
|
2766
|
+
opening.binaryType = "arraybuffer";
|
|
2767
|
+
this.#socket = opening;
|
|
2768
|
+
opening.addEventListener("open", () => {
|
|
2769
|
+
opened = true;
|
|
2770
|
+
this.#quiet();
|
|
2771
|
+
this.#flush(opening);
|
|
2772
|
+
if (this.#keepalive) clearInterval(this.#keepalive);
|
|
2773
|
+
this.#keepalive = setInterval(() => {
|
|
2774
|
+
if (this.#socket === opening) this.send({ type: "keepalive" });
|
|
2775
|
+
}, KEEPALIVE_MS);
|
|
2776
|
+
safely(() => this.protocol.open());
|
|
2777
|
+
});
|
|
2778
|
+
opening.addEventListener("message", (event) => {
|
|
2779
|
+
this.#quiet();
|
|
2780
|
+
const text2 = String(event.data);
|
|
2781
|
+
this.#incoming = this.#incoming.then(() => this.protocol.receive(text2)).catch(() => {
|
|
2782
|
+
console.warn(LOST);
|
|
2783
|
+
});
|
|
2784
|
+
});
|
|
2785
|
+
opening.addEventListener("error", () => {
|
|
2786
|
+
if (this.#socket === opening) this.#pending = [];
|
|
2787
|
+
});
|
|
2788
|
+
opening.addEventListener("close", (event) => {
|
|
2789
|
+
if (this.#socket !== opening) return;
|
|
2790
|
+
this.#socket = void 0;
|
|
2791
|
+
this.#pending = [];
|
|
2792
|
+
if (this.#keepalive) {
|
|
2793
|
+
clearInterval(this.#keepalive);
|
|
2794
|
+
this.#keepalive = void 0;
|
|
2795
|
+
}
|
|
2796
|
+
if (this.#silence) {
|
|
2797
|
+
clearTimeout(this.#silence);
|
|
2798
|
+
this.#silence = void 0;
|
|
2799
|
+
}
|
|
2800
|
+
if (this.#disposed) return;
|
|
2801
|
+
const closing = this.#closing;
|
|
2802
|
+
this.#closing = void 0;
|
|
2803
|
+
const { code = 1005, reason = "" } = event ?? {};
|
|
2804
|
+
safely(() => this.#closed(opened, closing, code, reason));
|
|
2805
|
+
});
|
|
2177
2806
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
}
|
|
2223
|
-
socket = void 0;
|
|
2224
|
-
pending = [];
|
|
2225
|
-
commands.reset();
|
|
2226
|
-
let url;
|
|
2227
|
-
try {
|
|
2228
|
-
url = socketUrl(await gateway());
|
|
2229
|
-
} catch (error) {
|
|
2230
|
-
protocol.closed(error instanceof Error ? error.message : String(error));
|
|
2231
|
-
return;
|
|
2807
|
+
/**
|
|
2808
|
+
* What a closed socket means, said once, and what happens next. A socket
|
|
2809
|
+
* that never opened is a buddi that did not answer, unless something on
|
|
2810
|
+
* that port did answer the knock and turned the socket away. One that
|
|
2811
|
+
* opened was answered: the buddi's own close reason decides.
|
|
2812
|
+
*/
|
|
2813
|
+
async #closed(opened, closing, code, reason) {
|
|
2814
|
+
if (this.#socket) return;
|
|
2815
|
+
if (closing === PAIR_AGAIN) {
|
|
2816
|
+
await this.#repair("That buddi does not know this browser any more. Asking it for a new code.");
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
if (!opened) {
|
|
2820
|
+
const knocked = await answering(socketUrl(this.origin)).catch(() => false);
|
|
2821
|
+
if (this.#disposed || this.#socket) return;
|
|
2822
|
+
if (knocked) {
|
|
2823
|
+
this.#halt(TURNED_AWAY);
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
this.protocol.closed(NO_ANSWER2);
|
|
2827
|
+
this.#schedule();
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const read = readClose(code, reason);
|
|
2831
|
+
if (read.kind === "refused") {
|
|
2832
|
+
this.#halt(read.message);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (read.kind === "repair") {
|
|
2836
|
+
await this.protocol.dropToken().catch(() => void 0);
|
|
2837
|
+
await this.#repair(read.message);
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
this.protocol.closed(read.message);
|
|
2841
|
+
this.#schedule();
|
|
2842
|
+
}
|
|
2843
|
+
/** Stop knocking and say why, until the owner presses Try again. */
|
|
2844
|
+
#halt(message) {
|
|
2845
|
+
this.#halted = true;
|
|
2846
|
+
if (this.#timer) {
|
|
2847
|
+
clearTimeout(this.#timer);
|
|
2848
|
+
this.#timer = void 0;
|
|
2849
|
+
}
|
|
2850
|
+
this.protocol.closed(message, { refused: true });
|
|
2232
2851
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2852
|
+
/** Start over at once with no token, which asks the buddi for a code; at most once in a while. */
|
|
2853
|
+
async #repair(message) {
|
|
2854
|
+
const now = Date.now();
|
|
2855
|
+
this.protocol.closed(message);
|
|
2856
|
+
if (now - this.#lastRepair < REPAIR_GAP_MS) {
|
|
2857
|
+
this.#schedule();
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
this.#lastRepair = now;
|
|
2861
|
+
this.#attempt = 0;
|
|
2862
|
+
await this.connect();
|
|
2237
2863
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
attempt = 0;
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
if (socket === opening) pending = [];
|
|
2259
|
-
});
|
|
2260
|
-
opening.addEventListener("close", () => {
|
|
2261
|
-
if (socket === opening) {
|
|
2262
|
-
socket = void 0;
|
|
2263
|
-
pending = [];
|
|
2264
|
-
if (keepalive) {
|
|
2265
|
-
clearInterval(keepalive);
|
|
2266
|
-
keepalive = void 0;
|
|
2267
|
-
}
|
|
2864
|
+
/** The owner pressed Connect or Try again: start over at once. */
|
|
2865
|
+
async retry() {
|
|
2866
|
+
this.#halted = false;
|
|
2867
|
+
this.#attempt = 0;
|
|
2868
|
+
this.#socket?.close();
|
|
2869
|
+
await this.connect();
|
|
2870
|
+
}
|
|
2871
|
+
/** Drop the token and pair again from a fresh code. */
|
|
2872
|
+
forget() {
|
|
2873
|
+
return this.protocol.forget();
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Switched off or removed in the popup: the socket closes and stays closed,
|
|
2877
|
+
* the sessions and their refs go. The tabs stay; they are the owner's.
|
|
2878
|
+
*/
|
|
2879
|
+
dispose() {
|
|
2880
|
+
this.#disposed = true;
|
|
2881
|
+
if (this.#timer) {
|
|
2882
|
+
clearTimeout(this.#timer);
|
|
2883
|
+
this.#timer = void 0;
|
|
2268
2884
|
}
|
|
2269
|
-
if (
|
|
2270
|
-
|
|
2271
|
-
|
|
2885
|
+
if (this.#keepalive) {
|
|
2886
|
+
clearInterval(this.#keepalive);
|
|
2887
|
+
this.#keepalive = void 0;
|
|
2272
2888
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2889
|
+
if (this.#silence) {
|
|
2890
|
+
clearTimeout(this.#silence);
|
|
2891
|
+
this.#silence = void 0;
|
|
2892
|
+
}
|
|
2893
|
+
const socket = this.#socket;
|
|
2894
|
+
this.#socket = void 0;
|
|
2895
|
+
this.#pending = [];
|
|
2896
|
+
try {
|
|
2897
|
+
socket?.close();
|
|
2898
|
+
} catch {
|
|
2899
|
+
}
|
|
2900
|
+
this.protocol.closed();
|
|
2901
|
+
this.commands.reset();
|
|
2902
|
+
}
|
|
2903
|
+
#schedule() {
|
|
2904
|
+
if (this.#timer || this.#disposed || this.#halted) return;
|
|
2905
|
+
const wait = BACKOFF_MS[Math.min(this.#attempt, BACKOFF_MS.length - 1)];
|
|
2906
|
+
this.#attempt += 1;
|
|
2907
|
+
this.#timer = setTimeout(() => {
|
|
2908
|
+
this.#timer = void 0;
|
|
2909
|
+
safely(() => this.connect());
|
|
2910
|
+
}, wait);
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
async function answering(socket) {
|
|
2278
2914
|
let http;
|
|
2279
2915
|
try {
|
|
2280
|
-
http = new URL(
|
|
2916
|
+
http = new URL(socket);
|
|
2281
2917
|
http.protocol = http.protocol === "wss:" ? "https:" : "http:";
|
|
2282
2918
|
http.pathname = "/api/version";
|
|
2283
2919
|
http.search = "";
|
|
@@ -2291,25 +2927,109 @@ async function answering(socket2) {
|
|
|
2291
2927
|
return false;
|
|
2292
2928
|
}
|
|
2293
2929
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2930
|
+
|
|
2931
|
+
// src/background.ts
|
|
2932
|
+
var store = new PairingStore(chrome.storage.local);
|
|
2933
|
+
var groups = new GroupRegistry();
|
|
2934
|
+
var links = /* @__PURE__ */ new Map();
|
|
2935
|
+
var entries = [];
|
|
2936
|
+
var asked = null;
|
|
2937
|
+
var version = chrome.runtime.getManifest().version;
|
|
2938
|
+
function entryOf(id) {
|
|
2939
|
+
return entries.find((entry) => entry.id === id);
|
|
2940
|
+
}
|
|
2941
|
+
function pushState() {
|
|
2942
|
+
safely(() => chrome.runtime.sendMessage({ type: "buddi-state" }).catch(() => void 0));
|
|
2943
|
+
}
|
|
2944
|
+
function linkFor(entry) {
|
|
2945
|
+
const link = new Link({
|
|
2946
|
+
chrome,
|
|
2947
|
+
id: entry.id,
|
|
2948
|
+
origin: entry.origin,
|
|
2949
|
+
store,
|
|
2950
|
+
groups,
|
|
2951
|
+
version,
|
|
2952
|
+
title: () => {
|
|
2953
|
+
const current = entryOf(entry.id);
|
|
2954
|
+
return current ? displayName(current, entries) : "buddi";
|
|
2955
|
+
},
|
|
2956
|
+
colour: () => entryOf(entry.id)?.colour,
|
|
2957
|
+
onState: () => pushState(),
|
|
2958
|
+
onName: (id, name) => safely(async () => {
|
|
2959
|
+
await store.update(id, { name });
|
|
2960
|
+
entries = await store.list();
|
|
2961
|
+
pushState();
|
|
2962
|
+
})
|
|
2963
|
+
});
|
|
2964
|
+
if (chrome.downloads) link.attachDownloads(chrome.downloads);
|
|
2965
|
+
return link;
|
|
2966
|
+
}
|
|
2967
|
+
async function sync() {
|
|
2968
|
+
entries = await store.list();
|
|
2969
|
+
for (const [id, link] of links) {
|
|
2970
|
+
const entry = entryOf(id);
|
|
2971
|
+
if (!entry || !entry.enabled || !sameBuddi(entry.origin, link.origin)) {
|
|
2972
|
+
link.dispose();
|
|
2973
|
+
links.delete(id);
|
|
2974
|
+
if (!entry) groups.release(id);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
for (const entry of entries) {
|
|
2978
|
+
if (!entry.enabled) continue;
|
|
2979
|
+
let link = links.get(entry.id);
|
|
2980
|
+
if (!link) {
|
|
2981
|
+
link = linkFor(entry);
|
|
2982
|
+
links.set(entry.id, link);
|
|
2983
|
+
}
|
|
2984
|
+
await link.connect();
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
async function views() {
|
|
2988
|
+
entries = await store.list();
|
|
2989
|
+
const out = [];
|
|
2990
|
+
for (const entry of entries) {
|
|
2991
|
+
const link = links.get(entry.id);
|
|
2992
|
+
const state = link?.state() ?? { connection: "offline", code: null, installation: null, error: null };
|
|
2993
|
+
out.push({
|
|
2994
|
+
id: entry.id,
|
|
2995
|
+
origin: entry.origin,
|
|
2996
|
+
name: displayName(entry, entries),
|
|
2997
|
+
colour: entry.colour,
|
|
2998
|
+
enabled: entry.enabled,
|
|
2999
|
+
state,
|
|
3000
|
+
tabs: link && state.connection === "paired" ? await link.commands.tabCount().catch(() => null) : null
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
return out;
|
|
3004
|
+
}
|
|
3005
|
+
function firstLink() {
|
|
3006
|
+
return links.values().next().value;
|
|
3007
|
+
}
|
|
3008
|
+
chrome.permissions?.onAdded?.addListener((added) => {
|
|
3009
|
+
if (added.permissions?.includes(DOWNLOADS_PERMISSION) && chrome.downloads) for (const link of links.values()) link.attachDownloads(chrome.downloads);
|
|
3010
|
+
});
|
|
3011
|
+
function holders(session) {
|
|
3012
|
+
const holding = [...links.values()].filter((link) => link.commands.heldTab(session) !== void 0);
|
|
3013
|
+
return holding.length > 0 ? holding : [...links.values()];
|
|
2302
3014
|
}
|
|
2303
3015
|
chrome.runtime.onMessage.addListener((message, sender, respond) => {
|
|
2304
3016
|
const request = message;
|
|
2305
3017
|
if (!request || typeof request.type !== "string") return;
|
|
2306
3018
|
if (request.type === GIVE_BACK_MESSAGE) {
|
|
2307
3019
|
const tabId = sender?.tab?.id;
|
|
2308
|
-
|
|
3020
|
+
const session = request.session;
|
|
3021
|
+
if (typeof session === "string") safely(async () => {
|
|
3022
|
+
for (const link of holders(session)) await link.commands.giveBack(session, tabId);
|
|
3023
|
+
});
|
|
2309
3024
|
return;
|
|
2310
3025
|
}
|
|
2311
3026
|
if (request.type === LOGIN_MESSAGE || request.type === LOGIN_SEEN_MESSAGE || request.type === LOGIN_PENDING_MESSAGE) {
|
|
2312
|
-
const
|
|
3027
|
+
const from = sender;
|
|
3028
|
+
let reply;
|
|
3029
|
+
for (const link of links.values()) {
|
|
3030
|
+
reply = link.commands.loginMessage(message, from);
|
|
3031
|
+
if (reply) break;
|
|
3032
|
+
}
|
|
2313
3033
|
if (!reply) {
|
|
2314
3034
|
respond(null);
|
|
2315
3035
|
return;
|
|
@@ -2318,50 +3038,87 @@ chrome.runtime.onMessage.addListener((message, sender, respond) => {
|
|
|
2318
3038
|
return true;
|
|
2319
3039
|
}
|
|
2320
3040
|
if (request.type === "buddi-get-state") {
|
|
2321
|
-
respond({
|
|
2322
|
-
return;
|
|
3041
|
+
safely(async () => respond({ pairings: await views(), asked }));
|
|
3042
|
+
return true;
|
|
2323
3043
|
}
|
|
2324
|
-
if (request.type === "buddi-connect") {
|
|
3044
|
+
if (request.type === "buddi-add" || request.type === "buddi-connect") {
|
|
2325
3045
|
safely(async () => {
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
await chrome.storage.local.set({ [GATEWAY_KEY]: request.gateway });
|
|
3046
|
+
let entry;
|
|
3047
|
+
const before = new Set(links.keys());
|
|
3048
|
+
try {
|
|
3049
|
+
entry = await store.add(request.gateway?.trim() || DEFAULT_GATEWAY);
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
respond({ error: error instanceof Error ? error.message : String(error) });
|
|
3052
|
+
return;
|
|
2334
3053
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
await
|
|
2338
|
-
respond({
|
|
3054
|
+
if (asked && sameBuddi(asked, entry.origin)) asked = null;
|
|
3055
|
+
await sync();
|
|
3056
|
+
if (before.has(entry.id)) await links.get(entry.id)?.retry();
|
|
3057
|
+
respond({ pairings: await views(), added: entry.id });
|
|
3058
|
+
});
|
|
3059
|
+
return true;
|
|
3060
|
+
}
|
|
3061
|
+
if (request.type === "buddi-enable" && typeof request.id === "string") {
|
|
3062
|
+
safely(async () => {
|
|
3063
|
+
await store.update(request.id, { enabled: request.enabled !== false });
|
|
3064
|
+
await sync();
|
|
3065
|
+
respond({ pairings: await views() });
|
|
3066
|
+
});
|
|
3067
|
+
return true;
|
|
3068
|
+
}
|
|
3069
|
+
if (request.type === "buddi-retry" && typeof request.id === "string") {
|
|
3070
|
+
safely(async () => {
|
|
3071
|
+
await links.get(request.id)?.retry();
|
|
3072
|
+
respond({ pairings: await views() });
|
|
2339
3073
|
});
|
|
2340
3074
|
return true;
|
|
2341
3075
|
}
|
|
2342
3076
|
if (request.type === "buddi-forget") {
|
|
2343
|
-
safely(
|
|
3077
|
+
safely(async () => {
|
|
3078
|
+
const link = typeof request.id === "string" ? links.get(request.id) : firstLink();
|
|
3079
|
+
await link?.forget();
|
|
3080
|
+
respond({ pairings: await views() });
|
|
3081
|
+
});
|
|
2344
3082
|
return true;
|
|
2345
3083
|
}
|
|
2346
|
-
if (request.type === "buddi-
|
|
2347
|
-
safely(
|
|
3084
|
+
if (request.type === "buddi-remove" && typeof request.id === "string") {
|
|
3085
|
+
safely(async () => {
|
|
3086
|
+
await store.remove(request.id);
|
|
3087
|
+
await sync();
|
|
3088
|
+
respond({ pairings: await views() });
|
|
3089
|
+
});
|
|
2348
3090
|
return true;
|
|
2349
3091
|
}
|
|
2350
3092
|
return;
|
|
2351
3093
|
});
|
|
2352
|
-
chrome.runtime.onMessageExternal.addListener((message, sender, respond) => handleExternal(message, {
|
|
2353
|
-
|
|
2354
|
-
|
|
3094
|
+
chrome.runtime.onMessageExternal.addListener((message, sender, respond) => handleExternal(message, {
|
|
3095
|
+
origin: sender.origin,
|
|
3096
|
+
version,
|
|
3097
|
+
known: async () => {
|
|
3098
|
+
entries = await store.list().catch(() => entries);
|
|
3099
|
+
return entries.map((entry) => ({
|
|
3100
|
+
origin: entry.origin,
|
|
3101
|
+
name: displayName(entry, entries),
|
|
3102
|
+
enabled: entry.enabled,
|
|
3103
|
+
state: links.get(entry.id)?.state() ?? { connection: "offline", code: null, installation: null, error: null }
|
|
3104
|
+
}));
|
|
3105
|
+
},
|
|
3106
|
+
unknown: (origin) => {
|
|
3107
|
+
asked = origin;
|
|
3108
|
+
}
|
|
3109
|
+
}, respond));
|
|
3110
|
+
chrome.runtime.onInstalled.addListener(() => safely(() => sync()));
|
|
3111
|
+
chrome.runtime.onStartup.addListener(() => safely(() => sync()));
|
|
2355
3112
|
var RECONNECT_ALARM = "buddi-reconnect";
|
|
2356
3113
|
chrome.alarms.create(RECONNECT_ALARM, { periodInMinutes: 1 });
|
|
2357
3114
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
2358
|
-
if (alarm.name === RECONNECT_ALARM) safely(() =>
|
|
3115
|
+
if (alarm.name === RECONNECT_ALARM) safely(() => sync());
|
|
2359
3116
|
});
|
|
2360
|
-
safely(() =>
|
|
3117
|
+
safely(() => sync());
|
|
2361
3118
|
export {
|
|
2362
3119
|
DEFAULT_GATEWAY,
|
|
2363
3120
|
KEEPALIVE_MS,
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
3121
|
+
socketUrl,
|
|
3122
|
+
sync,
|
|
3123
|
+
views
|
|
2367
3124
|
};
|