local-operator-ui 0.29.4 → 0.29.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/out/main/index.js +545 -2
  2. package/out/preload/index.js +91 -0
  3. package/out/renderer/assets/{_basePickBy-gxzYvz_O.js → _basePickBy-D-VwFtKc.js} +1 -1
  4. package/out/renderer/assets/{_baseUniq-CdAUV-Hy.js → _baseUniq-DDJz-WZw.js} +1 -1
  5. package/out/renderer/assets/{agent-details-page-CeH2TZUB.js → agent-details-page-jO2PvB7X.js} +1 -1
  6. package/out/renderer/assets/{agent-hub-page-B8xcN-ll.js → agent-hub-page-xdes-D_X.js} +2 -2
  7. package/out/renderer/assets/{agents-page-BWY4HlME.js → agents-page-j8DAg52R.js} +2 -2
  8. package/out/renderer/assets/{arc-D2VjsK8k.js → arc-CjaBvXDL.js} +1 -1
  9. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-CIgohhJb.js → architectureDiagram-IEHRJDOE-CYVtl1Tr.js} +1 -1
  10. package/out/renderer/assets/{blockDiagram-JOT3LUYC-2F1T6eNY.js → blockDiagram-JOT3LUYC-DfMbDEPW.js} +1 -1
  11. package/out/renderer/assets/browser-page-Dw1FPkTB.js +1 -0
  12. package/out/renderer/assets/browser-webauthn-prompt-3S4O_0eV.js +1 -0
  13. package/out/renderer/assets/{c4Diagram-VJAJSXHY-CsTsgFRF.js → c4Diagram-VJAJSXHY-D_lTZwGe.js} +1 -1
  14. package/out/renderer/assets/channel-Dfi7uOIV.js +1 -0
  15. package/out/renderer/assets/{chunk-4BMEZGHF-C9bkAssZ.js → chunk-4BMEZGHF-BPOpt_G1.js} +1 -1
  16. package/out/renderer/assets/{chunk-A2AXSNBT-B5SR5RMq.js → chunk-A2AXSNBT-DTq_POy_.js} +1 -1
  17. package/out/renderer/assets/{chunk-AEK57VVT-CBWKf9Ru.js → chunk-AEK57VVT-DOAjbTBV.js} +1 -1
  18. package/out/renderer/assets/{chunk-D6G4REZN-l5x9wMv5.js → chunk-D6G4REZN-CLeUkHhx.js} +1 -1
  19. package/out/renderer/assets/{chunk-RZ5BOZE2-Cj3AJqLP.js → chunk-RZ5BOZE2-DR3q6hDQ.js} +1 -1
  20. package/out/renderer/assets/{chunk-XZIHB7SX-C4zCEl2t.js → chunk-XZIHB7SX-DeI7Tei8.js} +1 -1
  21. package/out/renderer/assets/classDiagram-GIVACNV2-Dln1fC0q.js +1 -0
  22. package/out/renderer/assets/classDiagram-v2-COTLJTTW-Dln1fC0q.js +1 -0
  23. package/out/renderer/assets/clone-DA5JBsPm.js +1 -0
  24. package/out/renderer/assets/{compact-pagination-gqlVwPGp.js → compact-pagination-pR_TewmZ.js} +1 -1
  25. package/out/renderer/assets/{dagre-OKDRZEBW-DTp2Roxz.js → dagre-OKDRZEBW-BzCAmNFs.js} +1 -1
  26. package/out/renderer/assets/{diagram-SSKATNLV-YNxufqAy.js → diagram-SSKATNLV-DJhkbiTK.js} +1 -1
  27. package/out/renderer/assets/{diagram-VNBRO52H-CsMvXFHA.js → diagram-VNBRO52H-C198p3CS.js} +1 -1
  28. package/out/renderer/assets/{erDiagram-Q7BY3M3F-Dp4rUc8C.js → erDiagram-Q7BY3M3F-BXMHZzg7.js} +1 -1
  29. package/out/renderer/assets/{flowDiagram-4HSFHLVR-lygSr9ii.js → flowDiagram-4HSFHLVR-BH25N-UK.js} +1 -1
  30. package/out/renderer/assets/{ganttDiagram-APWFNJXF-CXE0VGcQ.js → ganttDiagram-APWFNJXF-C90nUk9-.js} +1 -1
  31. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-DTuWq8CY.js → gitGraphDiagram-7IBYFJ6S-CGCN32Lb.js} +1 -1
  32. package/out/renderer/assets/{graph-BLjKIfOK.js → graph-CnshGmMM.js} +1 -1
  33. package/out/renderer/assets/{icon-D1d2UB7T.js → icon-B19NuJYm.js} +31 -31
  34. package/out/renderer/assets/{icon-ChKJ4xic.css → icon-DryJ1IIw.css} +1 -1
  35. package/out/renderer/assets/{index-B7nZ8nvT.js → index-CLOkmTr3.js} +1 -1
  36. package/out/renderer/assets/{index-CcdUoV4M.js → index-CbEqOOj5.js} +370 -365
  37. package/out/renderer/assets/{index-D2gQKwz6.js → index-Dic-nJw9.js} +1 -1
  38. package/out/renderer/assets/{infoDiagram-PH2N3AL5-DFdib3H2.js → infoDiagram-PH2N3AL5-yhFK8QbZ.js} +1 -1
  39. package/out/renderer/assets/{installer-DfRiHVwp.js → installer-DL2KRfdq.js} +1 -1
  40. package/out/renderer/assets/{journeyDiagram-U35MCT3I-CCKfbFqM.js → journeyDiagram-U35MCT3I-Bh_mQdVI.js} +1 -1
  41. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-gjSxgHvX.js → kanban-definition-NDS4AKOZ-CFxjCPF4.js} +1 -1
  42. package/out/renderer/assets/{layout-xZfuwHlb.js → layout-CEBnys_f.js} +1 -1
  43. package/out/renderer/assets/{legacy-agents-page-D15ktDDl.js → legacy-agents-page-BV5wNeFT.js} +2 -2
  44. package/out/renderer/assets/{mermaid.core-CuLiCFaz.js → mermaid.core-CItomUrR.js} +5 -5
  45. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-b0kWF8-L.js → mindmap-definition-ALO5MXBD-B-uWszwf.js} +1 -1
  46. package/out/renderer/assets/{page-header-8NSHzS_8.js → page-header-D1a45b0y.js} +1 -1
  47. package/out/renderer/assets/{parseISO-CN3rYRB1.js → parseISO-Dkhkl_3Q.js} +1 -1
  48. package/out/renderer/assets/{pieDiagram-IB7DONF6-CIwBolgZ.js → pieDiagram-IB7DONF6-CqJrFz8P.js} +1 -1
  49. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-D2VIfVKV.js → quadrantDiagram-7GDLP6J5-Bla_rG-3.js} +1 -1
  50. package/out/renderer/assets/{radar-MK3ICKWK-bpDgxm80.js → radar-MK3ICKWK-DM-Fttuo.js} +1 -1
  51. package/out/renderer/assets/{radient-auth-buttons-C26CyHdt.js → radient-auth-buttons-5BZmMSkU.js} +1 -1
  52. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-Ds3aLOtI.js → requirementDiagram-KVF5MWMF-f1n0Vj3U.js} +1 -1
  53. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-VHIxbH9D.js → sankeyDiagram-QLVOVGJD-WBvrc_GC.js} +1 -1
  54. package/out/renderer/assets/{schedules-page-DmAGPWPg.js → schedules-page-CufYrWWt.js} +1 -1
  55. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-iy9otA_H.js → sequenceDiagram-X6HHIX6F-DXNQl5UM.js} +1 -1
  56. package/out/renderer/assets/{settings-page-Cx0klh-0.js → settings-page-BppBUxvd.js} +8 -8
  57. package/out/renderer/assets/{stateDiagram-DGXRK772-yfApnPmq.js → stateDiagram-DGXRK772-Dc-HE3Jh.js} +1 -1
  58. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CEPLahMP.js +1 -0
  59. package/out/renderer/assets/{timeline-definition-BDJGKUSR-CxTk8pCf.js → timeline-definition-BDJGKUSR-DAE8SRiO.js} +1 -1
  60. package/out/renderer/assets/{use-agent-like-mutation-CF5zslUF.js → use-agent-like-mutation-BmPMhO1j.js} +1 -1
  61. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-B8K04QFZ.js → xychartDiagram-VJFVF3MP-DoRBXwwS.js} +1 -1
  62. package/out/renderer/index.html +3 -3
  63. package/out/renderer/installer.html +3 -3
  64. package/package.json +2 -3
  65. package/out/renderer/assets/browser-page-wXWEZV9g.js +0 -1
  66. package/out/renderer/assets/channel-BNKnhGg7.js +0 -1
  67. package/out/renderer/assets/classDiagram-GIVACNV2-CVVGZiEg.js +0 -1
  68. package/out/renderer/assets/classDiagram-v2-COTLJTTW-CVVGZiEg.js +0 -1
  69. package/out/renderer/assets/clone-I1HRwzkv.js +0 -1
  70. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-YHGl0wyW.js +0 -1
package/out/main/index.js CHANGED
@@ -9584,6 +9584,428 @@ class BackendInstaller {
9584
9584
  }
9585
9585
  }
9586
9586
  }
9587
+ const WEBAUTHN_GROUP_SUFFIX = ".webauthn";
9588
+ const WEBAUTHN_PROMPT_REASON = "use a passkey for $1";
9589
+ const WEBAUTHN_CHOOSER_TIMEOUT_MS = 6e4;
9590
+ const CODESIGN_TIMEOUT_MS = 5e3;
9591
+ function webauthnKeychainAccessGroup(teamIdentifier, bundleIdentifier) {
9592
+ return `${teamIdentifier}.${bundleIdentifier}${WEBAUTHN_GROUP_SUFFIX}`;
9593
+ }
9594
+ function appBundlePathFromExecutable(execPath) {
9595
+ const parts = execPath.split("/");
9596
+ const macOs = parts.lastIndexOf("MacOS");
9597
+ if (macOs < 2 || parts[macOs - 1] !== "Contents") {
9598
+ return execPath;
9599
+ }
9600
+ return parts.slice(0, macOs - 1).join("/") || "/";
9601
+ }
9602
+ const TEAM_IDENTIFIER_LINE = /^TeamIdentifier=(.*)$/m;
9603
+ const IDENTIFIER_LINE = /^Identifier=(.*)$/m;
9604
+ function parseTeamIdentifier(codesignVerboseOutput) {
9605
+ const match = TEAM_IDENTIFIER_LINE.exec(codesignVerboseOutput);
9606
+ const value = (match?.[1] ?? "").trim();
9607
+ return value && value !== "not set" ? value : null;
9608
+ }
9609
+ function parseBundleIdentifier(codesignVerboseOutput) {
9610
+ const match = IDENTIFIER_LINE.exec(codesignVerboseOutput);
9611
+ const value = (match?.[1] ?? "").trim();
9612
+ return value || null;
9613
+ }
9614
+ function parseKeychainAccessGroups(entitlementsPlist) {
9615
+ const key = entitlementsPlist.indexOf("<key>keychain-access-groups</key>");
9616
+ if (key === -1) return [];
9617
+ const after = entitlementsPlist.slice(key);
9618
+ const arrayStart = after.indexOf("<array>");
9619
+ const arrayEnd = after.indexOf("</array>", arrayStart + 1);
9620
+ if (arrayStart === -1 || arrayEnd === -1) return [];
9621
+ const body = after.slice(arrayStart + "<array>".length, arrayEnd);
9622
+ const groups = [];
9623
+ const pattern = /<string>([^<]*)<\/string>/g;
9624
+ let match = pattern.exec(body);
9625
+ while (match !== null) {
9626
+ const value = match[1].trim();
9627
+ if (value) groups.push(value);
9628
+ match = pattern.exec(body);
9629
+ }
9630
+ return groups;
9631
+ }
9632
+ function decideWebauthnGate(facts) {
9633
+ if (facts.platform !== "darwin") {
9634
+ return {
9635
+ enabled: false,
9636
+ group: null,
9637
+ reason: "not darwin",
9638
+ detail: `the platform authenticator is macOS only (platform ${facts.platform})`
9639
+ };
9640
+ }
9641
+ if (!facts.packaged) {
9642
+ return {
9643
+ enabled: false,
9644
+ group: null,
9645
+ reason: "unpackaged",
9646
+ detail: "an unpackaged run has no signature to carry the entitlement, so there is no group to configure"
9647
+ };
9648
+ }
9649
+ if (facts.unreadable) {
9650
+ return {
9651
+ enabled: false,
9652
+ group: null,
9653
+ reason: "signature unreadable",
9654
+ detail: facts.unreadable
9655
+ };
9656
+ }
9657
+ if (!facts.bundleIdentifier) {
9658
+ return {
9659
+ enabled: false,
9660
+ group: null,
9661
+ reason: "no bundle id",
9662
+ detail: `codesign reported no Identifier for ${facts.bundlePath}`
9663
+ };
9664
+ }
9665
+ if (!facts.teamIdentifier) {
9666
+ return {
9667
+ enabled: false,
9668
+ group: null,
9669
+ reason: "no team id",
9670
+ detail: `${facts.bundlePath} is not team-signed (an ad-hoc signature has no TeamIdentifier)`
9671
+ };
9672
+ }
9673
+ const group = webauthnKeychainAccessGroup(
9674
+ facts.teamIdentifier,
9675
+ facts.bundleIdentifier
9676
+ );
9677
+ if (facts.keychainAccessGroups.length === 0) {
9678
+ return {
9679
+ enabled: false,
9680
+ group,
9681
+ reason: "entitlement absent",
9682
+ detail: `the signature carries no keychain-access-groups entitlement, so ${group} cannot be used`
9683
+ };
9684
+ }
9685
+ if (!facts.keychainAccessGroups.includes(group)) {
9686
+ return {
9687
+ enabled: false,
9688
+ group,
9689
+ reason: "entitlement mismatch",
9690
+ detail: `the signature's keychain-access-groups are [${facts.keychainAccessGroups.join(", ")}] but the authenticator needs ${group}`
9691
+ };
9692
+ }
9693
+ return {
9694
+ enabled: true,
9695
+ group,
9696
+ reason: "enabled",
9697
+ detail: `the signature carries ${group}`
9698
+ };
9699
+ }
9700
+ const defaultCodesign = (file, args) => new Promise((resolve, reject) => {
9701
+ node_child_process.execFile(
9702
+ file,
9703
+ args,
9704
+ { timeout: CODESIGN_TIMEOUT_MS },
9705
+ (error, stdout, stderr) => {
9706
+ if (error) {
9707
+ reject(
9708
+ new Error(
9709
+ `${error.message}${stderr ? ` (${String(stderr).trim()})` : ""}`
9710
+ )
9711
+ );
9712
+ return;
9713
+ }
9714
+ resolve({ stdout: String(stdout), stderr: String(stderr) });
9715
+ }
9716
+ );
9717
+ });
9718
+ async function readAppSignature(options) {
9719
+ const bundlePath = appBundlePathFromExecutable(options.execPath);
9720
+ const base = {
9721
+ platform: options.platform,
9722
+ packaged: options.packaged,
9723
+ bundlePath,
9724
+ bundleIdentifier: null,
9725
+ teamIdentifier: null,
9726
+ keychainAccessGroups: [],
9727
+ unreadable: null
9728
+ };
9729
+ if (options.platform !== "darwin" || !options.packaged) return base;
9730
+ const run = options.run ?? defaultCodesign;
9731
+ try {
9732
+ const verbose = await run("/usr/bin/codesign", [
9733
+ "-dv",
9734
+ "--verbose=4",
9735
+ bundlePath
9736
+ ]);
9737
+ base.bundleIdentifier = parseBundleIdentifier(verbose.stdout);
9738
+ base.teamIdentifier = parseTeamIdentifier(verbose.stdout);
9739
+ } catch (error) {
9740
+ return { ...base, unreadable: `codesign -dv failed: ${messageOf$1(error)}` };
9741
+ }
9742
+ try {
9743
+ const entitlements = await run("/usr/bin/codesign", [
9744
+ "-d",
9745
+ "--entitlements",
9746
+ "-",
9747
+ "--xml",
9748
+ bundlePath
9749
+ ]);
9750
+ base.keychainAccessGroups = parseKeychainAccessGroups(entitlements.stdout);
9751
+ } catch (error) {
9752
+ return {
9753
+ ...base,
9754
+ unreadable: `codesign -d --entitlements failed: ${messageOf$1(error)}`
9755
+ };
9756
+ }
9757
+ return base;
9758
+ }
9759
+ function messageOf$1(error) {
9760
+ return error instanceof Error ? error.message : String(error);
9761
+ }
9762
+ let ownSignature = null;
9763
+ function ownAppSignature() {
9764
+ ownSignature ??= readAppSignature({
9765
+ platform: process.platform,
9766
+ packaged: electron.app.isPackaged,
9767
+ execPath: process.execPath
9768
+ });
9769
+ return ownSignature;
9770
+ }
9771
+ async function installWebauthn(options) {
9772
+ const facts = await (options.facts ?? ownAppSignature)();
9773
+ const gate = decideWebauthnGate(facts);
9774
+ if (!gate.enabled || !gate.group) {
9775
+ options.log(
9776
+ `[webauthn] Touch ID passkeys are off: ${gate.reason} — ${gate.detail}`
9777
+ );
9778
+ return { gate, configured: false };
9779
+ }
9780
+ const configure = options.configure ?? ((value) => electron.app.configureWebAuthn(value));
9781
+ try {
9782
+ configure({
9783
+ touchID: {
9784
+ keychainAccessGroup: gate.group,
9785
+ promptReason: WEBAUTHN_PROMPT_REASON
9786
+ }
9787
+ });
9788
+ } catch (error) {
9789
+ options.log(
9790
+ `[webauthn] Touch ID passkeys are off: configureWebAuthn failed — ${messageOf$1(error)}`
9791
+ );
9792
+ return { gate: { ...gate, enabled: false }, configured: false };
9793
+ }
9794
+ options.log(
9795
+ `[webauthn] Touch ID platform authenticator enabled (${gate.group}); credentials are device-bound and never leave this Mac`
9796
+ );
9797
+ return { gate, configured: true };
9798
+ }
9799
+ class WebauthnChooser {
9800
+ constructor(options) {
9801
+ this.options = options;
9802
+ }
9803
+ pending = /* @__PURE__ */ new Map();
9804
+ counter = 0;
9805
+ /** Handle the Session event. `accounts` is Electron's own array. */
9806
+ handle(details, callback) {
9807
+ const answer = onceAnswer(callback);
9808
+ try {
9809
+ this.prepare(details, answer);
9810
+ } catch (error) {
9811
+ this.options.log(
9812
+ `[webauthn] a passkey request could not be prepared (${String(
9813
+ error
9814
+ )}); cancelling it`
9815
+ );
9816
+ answer(null);
9817
+ }
9818
+ }
9819
+ /** The pre-timer half of `handle`: everything that can still throw. */
9820
+ prepare(details, answer) {
9821
+ const relyingPartyId = typeof details.relyingPartyId === "string" ? details.relyingPartyId : "";
9822
+ const accounts = Array.isArray(details.accounts) ? details.accounts.filter(
9823
+ (account) => typeof account === "object" && account !== null
9824
+ ).map((account) => toAccountView(account)) : [];
9825
+ if (accounts.length === 0) {
9826
+ this.options.log(
9827
+ `[webauthn] ${relyingPartyId || "a site"} asked for a passkey with no account to choose; cancelling the request`
9828
+ );
9829
+ answer(null);
9830
+ return;
9831
+ }
9832
+ const requestId2 = this.options.nextRequestId ? this.options.nextRequestId() : `webauthn-${++this.counter}-${Date.now().toString(36)}`;
9833
+ const source = this.options.describeSource?.(details.frame) ?? {
9834
+ tabId: null,
9835
+ pageTitle: null
9836
+ };
9837
+ const timer = setTimeout(
9838
+ () => this.expire(requestId2),
9839
+ this.options.timeoutMs ?? WEBAUTHN_CHOOSER_TIMEOUT_MS
9840
+ );
9841
+ timer.unref?.();
9842
+ const request = {
9843
+ requestId: requestId2,
9844
+ relyingPartyId,
9845
+ accounts,
9846
+ tabId: source.tabId,
9847
+ pageTitle: source.pageTitle
9848
+ };
9849
+ this.options.log(
9850
+ `[webauthn] ${relyingPartyId || "a site"} matched ${accounts.length} passkeys; asking the user which one to use (${requestId2})`
9851
+ );
9852
+ this.options.notify(request);
9853
+ this.pending.set(requestId2, {
9854
+ requestId: requestId2,
9855
+ relyingPartyId,
9856
+ accounts,
9857
+ tabId: source.tabId,
9858
+ pageTitle: source.pageTitle,
9859
+ answer,
9860
+ timer
9861
+ });
9862
+ }
9863
+ /**
9864
+ * Answer a surfaced chooser. Returns whether a pending request was settled.
9865
+ *
9866
+ * A `credentialId` that is not one of the offered accounts is treated as a
9867
+ * dismissal rather than passed through: Electron would cancel the request
9868
+ * anyway, and doing it here keeps the log honest about what happened.
9869
+ */
9870
+ respond(requestId2, credentialId) {
9871
+ const entry = this.pending.get(requestId2);
9872
+ if (!entry) {
9873
+ this.options.log(
9874
+ `[webauthn] ignored a choice for an unknown or already-answered request (${requestId2})`
9875
+ );
9876
+ return false;
9877
+ }
9878
+ if (credentialId !== null && !entry.accounts.some((a) => a.credentialId === credentialId)) {
9879
+ this.options.log(
9880
+ `[webauthn] a choice for ${entry.relyingPartyId} named a credential that was not offered; cancelling the request`
9881
+ );
9882
+ this.settle(entry, null, "credential-not-offered");
9883
+ return true;
9884
+ }
9885
+ if (credentialId === null) {
9886
+ this.options.log(
9887
+ `[webauthn] the passkey choice for ${entry.relyingPartyId} was dismissed; cancelling the request`
9888
+ );
9889
+ this.settle(entry, null, "dismissed");
9890
+ return true;
9891
+ }
9892
+ const chosen = entry.accounts.find((a) => a.credentialId === credentialId);
9893
+ this.options.log(
9894
+ `[webauthn] the user chose ${labelOf(chosen)} for ${entry.relyingPartyId}`
9895
+ );
9896
+ this.settle(entry, credentialId, "chosen");
9897
+ return true;
9898
+ }
9899
+ /** Ids still waiting for an answer. Read by tests and by nothing else. */
9900
+ pendingRequestIds() {
9901
+ return [...this.pending.keys()];
9902
+ }
9903
+ /**
9904
+ * Every request still waiting, OLDEST FIRST.
9905
+ *
9906
+ * This is what a surface that mounts late reads instead of a request it never
9907
+ * received (UX round 1, U2: the request was raised while the browser route was
9908
+ * not mounted, so the push had nowhere to go). Order is the map's own insertion
9909
+ * order, which is why it is stated: the renderer offers the oldest request and
9910
+ * names the rest rather than silently replacing one with the next.
9911
+ */
9912
+ pendingRequests() {
9913
+ return [...this.pending.keys()].map((requestId2) => this.view(requestId2));
9914
+ }
9915
+ /** Cancel everything still pending. Called when the host stops, because a
9916
+ * callback left behind would keep the page's promise pending in a window
9917
+ * that is going away. */
9918
+ dispose() {
9919
+ for (const entry of [...this.pending.values()]) {
9920
+ this.options.log(
9921
+ `[webauthn] the passkey request from ${entry.relyingPartyId} was cancelled: the browser host stopped`
9922
+ );
9923
+ this.settle(entry, null, "host-stopped");
9924
+ }
9925
+ }
9926
+ expire(requestId2) {
9927
+ const entry = this.pending.get(requestId2);
9928
+ if (!entry) return;
9929
+ this.options.log(
9930
+ `[webauthn] nobody chose a passkey for ${entry.relyingPartyId} within ${Math.round((this.options.timeoutMs ?? WEBAUTHN_CHOOSER_TIMEOUT_MS) / 1e3)}s; cancelling the request`
9931
+ );
9932
+ this.settle(entry, null, "expired");
9933
+ }
9934
+ /** The wire view of one pending request. Never a credential id in the token
9935
+ * position: `requestId` is this module's own. */
9936
+ view(requestId2) {
9937
+ const entry = this.pending.get(requestId2);
9938
+ if (!entry) {
9939
+ throw new Error(`no pending passkey request ${requestId2}`);
9940
+ }
9941
+ return {
9942
+ requestId: entry.requestId,
9943
+ relyingPartyId: entry.relyingPartyId,
9944
+ accounts: entry.accounts,
9945
+ tabId: entry.tabId,
9946
+ pageTitle: entry.pageTitle
9947
+ };
9948
+ }
9949
+ /** Answer exactly once, then tell the renderer the request is gone.
9950
+ *
9951
+ * Both halves matter. The map entry and the guarded callback are the
9952
+ * exactly-once half; `onSettled` is what stops the renderer offering a choice
9953
+ * for a request main has already answered — the dead-end dialog of design
9954
+ * round 1, D2 — and it fires for EVERY path, including the host's own stop.
9955
+ */
9956
+ settle(entry, credentialId, outcome) {
9957
+ if (!this.pending.has(entry.requestId)) return;
9958
+ this.pending.delete(entry.requestId);
9959
+ clearTimeout(entry.timer);
9960
+ entry.answer(credentialId);
9961
+ this.options.onSettled?.(entry.requestId, outcome);
9962
+ }
9963
+ }
9964
+ const answerableCallbacks = /* @__PURE__ */ new WeakMap();
9965
+ function onceAnswer(callback) {
9966
+ const existing = answerableCallbacks.get(callback);
9967
+ if (existing) return existing;
9968
+ let answered = false;
9969
+ const guarded = (credentialId) => {
9970
+ if (answered) return;
9971
+ answered = true;
9972
+ callback(credentialId ?? void 0);
9973
+ };
9974
+ answerableCallbacks.set(callback, guarded);
9975
+ return guarded;
9976
+ }
9977
+ const chooserListeners = /* @__PURE__ */ new WeakMap();
9978
+ function attachWebauthnChooser(session, chooser, log) {
9979
+ const previous = chooserListeners.get(session);
9980
+ if (previous) {
9981
+ session.removeListener("select-webauthn-account", previous);
9982
+ log(
9983
+ "[webauthn] replaced the previous passkey chooser listener (a second browser host started in this process)"
9984
+ );
9985
+ }
9986
+ const listener = (_event, details, callback) => {
9987
+ chooser.handle(details, callback);
9988
+ };
9989
+ session.on("select-webauthn-account", listener);
9990
+ chooserListeners.set(session, listener);
9991
+ return () => {
9992
+ if (chooserListeners.get(session) !== listener) return;
9993
+ session.removeListener("select-webauthn-account", listener);
9994
+ chooserListeners.delete(session);
9995
+ };
9996
+ }
9997
+ function toAccountView(account) {
9998
+ return {
9999
+ credentialId: typeof account.credentialId === "string" ? account.credentialId : "",
10000
+ name: typeof account.name === "string" ? account.name : null,
10001
+ displayName: typeof account.displayName === "string" ? account.displayName : null,
10002
+ userHandle: typeof account.userHandle === "string" ? account.userHandle : null
10003
+ };
10004
+ }
10005
+ function labelOf(account) {
10006
+ if (!account) return "an unnamed passkey";
10007
+ return account.displayName || account.name || "an unnamed passkey";
10008
+ }
9587
10009
  const PRIVATE_FILE_MODE = 384;
9588
10010
  const PRIVATE_DIR_MODE = 448;
9589
10011
  function writeJsonAtomic(path2, value) {
@@ -11000,14 +11422,48 @@ function settle(contents, timeoutMs = 3e4) {
11000
11422
  function permittedScheme(url) {
11001
11423
  return url.protocol === "http:" || url.protocol === "https:";
11002
11424
  }
11425
+ async function resolveMainFrameId(ctx, contents) {
11426
+ let pageEnabled = false;
11427
+ try {
11428
+ await ctx.cdp.send(contents, "Page.enable", {});
11429
+ pageEnabled = true;
11430
+ const tree = await ctx.cdp.send(contents, "Page.getFrameTree", {});
11431
+ const id2 = tree?.frameTree?.frame?.id;
11432
+ return {
11433
+ mainFrameId: typeof id2 === "string" && id2 ? id2 : null,
11434
+ pageEnabled
11435
+ };
11436
+ } catch {
11437
+ return { mainFrameId: null, pageEnabled };
11438
+ }
11439
+ }
11003
11440
  async function withOriginGate(ctx, view, requester, approved, operation) {
11004
11441
  const contents = view.webContents;
11005
11442
  const blocked = [];
11006
11443
  const seen = /* @__PURE__ */ new Set();
11444
+ const frameTree = await resolveMainFrameId(ctx, contents);
11445
+ const mainFrameId = frameTree.mainFrameId;
11446
+ if (mainFrameId === null) {
11447
+ ctx.log(
11448
+ `[browser] could not read the frame tree for ${requester}; this navigation is gated on every Document request, which may refuse cross-origin frames the page needs${// The domain's state travels with the fallback because it is not
11449
+ // inferable from the failure: an enable that resolved and a read that
11450
+ // did not left `Page` armed, an enable that never resolved did not
11451
+ // (agent review round 2, R3).
11452
+ frameTree.pageEnabled ? " (the Page domain may still be enabled)" : ""}`
11453
+ );
11454
+ }
11455
+ let unattributed = 0;
11007
11456
  const unsubscribe = ctx.cdp.subscribe(contents.id, (method, params) => {
11008
11457
  if (method !== "Fetch.requestPaused") return;
11009
11458
  const requestId2 = typeof params.requestId === "string" ? params.requestId : "";
11010
11459
  if (!requestId2) return;
11460
+ const frameId = typeof params.frameId === "string" ? params.frameId : "";
11461
+ if (mainFrameId !== null && frameId && frameId !== mainFrameId) {
11462
+ void ctx.cdp.send(contents, "Fetch.continueRequest", { requestId: requestId2 }).catch(() => {
11463
+ });
11464
+ return;
11465
+ }
11466
+ if (mainFrameId !== null && !frameId) unattributed += 1;
11011
11467
  const rawUrl = typeof params.request === "object" && params.request !== null ? params.request.url : void 0;
11012
11468
  let url = null;
11013
11469
  try {
@@ -11056,10 +11512,21 @@ async function withOriginGate(ctx, view, requester, approved, operation) {
11056
11512
  throw error;
11057
11513
  } finally {
11058
11514
  unsubscribe();
11515
+ if (unattributed > 0) {
11516
+ ctx.log(
11517
+ `[browser] ${unattributed} paused document request(s) during ${requester}'s navigation carried no frame id; they were gated as navigation hops`
11518
+ );
11519
+ }
11059
11520
  try {
11060
11521
  await ctx.cdp.send(contents, "Fetch.disable", {});
11061
11522
  } catch {
11062
11523
  }
11524
+ if (frameTree.pageEnabled) {
11525
+ try {
11526
+ await ctx.cdp.send(contents, "Page.disable", {});
11527
+ } catch {
11528
+ }
11529
+ }
11063
11530
  }
11064
11531
  }
11065
11532
  async function navigateView(ctx, view, url, requester, approved, timeoutMs = 3e4) {
@@ -11681,6 +12148,20 @@ class TabRegistry {
11681
12148
  list() {
11682
12149
  return [...this.tabs.values()].sort((a, b) => a.tabId - b.tabId);
11683
12150
  }
12151
+ /**
12152
+ * The tab whose view owns this webContents id, if any.
12153
+ *
12154
+ * The passkey chooser needs it to name the page a request came from: Electron
12155
+ * hands the initiating frame, `webContents.fromFrame` turns that into a
12156
+ * webContents, and this is the mapping from there to a tab of THIS host — the
12157
+ * frame may belong to no tab here at all, which is why the answer is optional
12158
+ * rather than an error.
12159
+ */
12160
+ byWebContentsId(webContentsId) {
12161
+ return this.list().find(
12162
+ (record) => !record.view.webContents.isDestroyed() && record.view.webContents.id === webContentsId
12163
+ );
12164
+ }
11684
12165
  get activeTab() {
11685
12166
  return this.activeTabId === null ? null : this.tabs.get(this.activeTabId) ?? null;
11686
12167
  }
@@ -13027,7 +13508,9 @@ const BROWSER_IPC_CHANNELS = [
13027
13508
  "browser-revoke-approval",
13028
13509
  "browser-revoke-all-approvals",
13029
13510
  "browser-forget-site",
13030
- "browser-clear-data"
13511
+ "browser-clear-data",
13512
+ "browser-webauthn-respond",
13513
+ "browser-webauthn-pending"
13031
13514
  ];
13032
13515
  function registerBrowserIpc(options) {
13033
13516
  function authorize(event) {
@@ -13097,6 +13580,25 @@ function registerBrowserIpc(options) {
13097
13580
  const host = authorize(event);
13098
13581
  return host.revokeHandOver(numberOrThrow(tabId, "tabId"));
13099
13582
  });
13583
+ electron.ipcMain.handle(
13584
+ "browser-webauthn-respond",
13585
+ (event, requestId2, credentialId) => {
13586
+ authorize(event);
13587
+ const chooser = options.webauthn();
13588
+ if (!chooser) throw new Error("The browser host is not running.");
13589
+ if (typeof requestId2 !== "string" || !requestId2) {
13590
+ throw new Error("A pending passkey request id is required.");
13591
+ }
13592
+ if (credentialId !== null && typeof credentialId !== "string") {
13593
+ throw new Error("A passkey choice must name a credential or nothing.");
13594
+ }
13595
+ return chooser.respond(requestId2, credentialId);
13596
+ }
13597
+ );
13598
+ electron.ipcMain.handle("browser-webauthn-pending", (event) => {
13599
+ authorize(event);
13600
+ return options.webauthn()?.pendingRequests() ?? [];
13601
+ });
13100
13602
  electron.ipcMain.handle(
13101
13603
  "browser-consent-respond",
13102
13604
  (event, entryId, decision) => {
@@ -13488,8 +13990,9 @@ class OwnershipLedger {
13488
13990
  const BROWSER_PARTITION = "persist:local-operator-browser";
13489
13991
  function browserUserAgent() {
13490
13992
  const chrome = process.versions.chrome ?? "0";
13993
+ const major = chrome.split(".")[0] || "0";
13491
13994
  const platform = process.platform === "darwin" ? "Macintosh; Intel Mac OS X 10_15_7" : process.platform === "win32" ? "Windows NT 10.0; Win64; x64" : "X11; Linux x86_64";
13492
- return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chrome} Safari/537.36 LocalOperator/${electron.app.getVersion()}`;
13995
+ return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`;
13493
13996
  }
13494
13997
  function resolveBrowserSession() {
13495
13998
  return electron.session.fromPartition(BROWSER_PARTITION);
@@ -14930,6 +15433,43 @@ async function startBrowserHost(options) {
14930
15433
  log
14931
15434
  });
14932
15435
  configurePslRules(PSL_RULES);
15436
+ await installWebauthn({ log });
15437
+ const describeSource = (frame) => {
15438
+ if (!frame || typeof frame !== "object") {
15439
+ return { tabId: null, pageTitle: null };
15440
+ }
15441
+ const contents = electron.webContents.fromFrame(frame);
15442
+ const record = contents === void 0 ? void 0 : registry.byWebContentsId(contents.id);
15443
+ if (!record) return { tabId: null, pageTitle: null };
15444
+ const title = record.view.webContents.isDestroyed() ? "" : record.view.webContents.getTitle();
15445
+ return { tabId: record.tabId, pageTitle: title || null };
15446
+ };
15447
+ const webauthn = new WebauthnChooser({
15448
+ notify: (request) => {
15449
+ if (options.window.isDestroyed()) return;
15450
+ options.window.webContents.send("browser-webauthn-request", request);
15451
+ },
15452
+ /*
15453
+ * AND THE OTHER DIRECTION: every settle is pushed, not only every arrival.
15454
+ *
15455
+ * Without this the renderer cannot tell a live request from one main has
15456
+ * already cancelled, so a chooser whose request expired while the user was
15457
+ * away stays on screen offering rows that can no longer do anything (design
15458
+ * round 1, D2) — and the click is then discarded silently. The outcome travels
15459
+ * with the push because the two cases differ for the user: their own answer is
15460
+ * already off the screen, an expiry has to be explained.
15461
+ */
15462
+ onSettled: (requestId2, outcome) => {
15463
+ if (options.window.isDestroyed()) return;
15464
+ options.window.webContents.send("browser-webauthn-settled", {
15465
+ requestId: requestId2,
15466
+ outcome
15467
+ });
15468
+ },
15469
+ describeSource,
15470
+ log
15471
+ });
15472
+ const detachWebauthn = attachWebauthnChooser(browserSession, webauthn, log);
14933
15473
  const cookieJar = createHiddenCookieJarTarget(
14934
15474
  new electron.WebContentsView({
14935
15475
  webPreferences: { ...HIDDEN_COOKIE_JAR_WEB_PREFERENCES }
@@ -15074,6 +15614,7 @@ async function startBrowserHost(options) {
15074
15614
  window: () => options.window,
15075
15615
  expectedUrl: options.expectedUrl,
15076
15616
  host: () => host,
15617
+ webauthn: () => webauthn,
15077
15618
  clearData: (what) => sessionCookies.clearBrowsingData(what),
15078
15619
  log
15079
15620
  });
@@ -15102,6 +15643,8 @@ async function startBrowserHost(options) {
15102
15643
  await cdp.close();
15103
15644
  await server.close();
15104
15645
  unregisterBrowserIpc();
15646
+ detachWebauthn();
15647
+ webauthn.dispose();
15105
15648
  stateWriter?.clear();
15106
15649
  approvals.resetPending();
15107
15650
  ownership.clear();