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.
- package/out/main/index.js +545 -2
- package/out/preload/index.js +91 -0
- package/out/renderer/assets/{_basePickBy-gxzYvz_O.js → _basePickBy-D-VwFtKc.js} +1 -1
- package/out/renderer/assets/{_baseUniq-CdAUV-Hy.js → _baseUniq-DDJz-WZw.js} +1 -1
- package/out/renderer/assets/{agent-details-page-CeH2TZUB.js → agent-details-page-jO2PvB7X.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-B8xcN-ll.js → agent-hub-page-xdes-D_X.js} +2 -2
- package/out/renderer/assets/{agents-page-BWY4HlME.js → agents-page-j8DAg52R.js} +2 -2
- package/out/renderer/assets/{arc-D2VjsK8k.js → arc-CjaBvXDL.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-CIgohhJb.js → architectureDiagram-IEHRJDOE-CYVtl1Tr.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-2F1T6eNY.js → blockDiagram-JOT3LUYC-DfMbDEPW.js} +1 -1
- package/out/renderer/assets/browser-page-Dw1FPkTB.js +1 -0
- package/out/renderer/assets/browser-webauthn-prompt-3S4O_0eV.js +1 -0
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-CsTsgFRF.js → c4Diagram-VJAJSXHY-D_lTZwGe.js} +1 -1
- package/out/renderer/assets/channel-Dfi7uOIV.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-C9bkAssZ.js → chunk-4BMEZGHF-BPOpt_G1.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-B5SR5RMq.js → chunk-A2AXSNBT-DTq_POy_.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-CBWKf9Ru.js → chunk-AEK57VVT-DOAjbTBV.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-l5x9wMv5.js → chunk-D6G4REZN-CLeUkHhx.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-Cj3AJqLP.js → chunk-RZ5BOZE2-DR3q6hDQ.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-C4zCEl2t.js → chunk-XZIHB7SX-DeI7Tei8.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-Dln1fC0q.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-Dln1fC0q.js +1 -0
- package/out/renderer/assets/clone-DA5JBsPm.js +1 -0
- package/out/renderer/assets/{compact-pagination-gqlVwPGp.js → compact-pagination-pR_TewmZ.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-DTp2Roxz.js → dagre-OKDRZEBW-BzCAmNFs.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-YNxufqAy.js → diagram-SSKATNLV-DJhkbiTK.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-CsMvXFHA.js → diagram-VNBRO52H-C198p3CS.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-Dp4rUc8C.js → erDiagram-Q7BY3M3F-BXMHZzg7.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-lygSr9ii.js → flowDiagram-4HSFHLVR-BH25N-UK.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-CXE0VGcQ.js → ganttDiagram-APWFNJXF-C90nUk9-.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-DTuWq8CY.js → gitGraphDiagram-7IBYFJ6S-CGCN32Lb.js} +1 -1
- package/out/renderer/assets/{graph-BLjKIfOK.js → graph-CnshGmMM.js} +1 -1
- package/out/renderer/assets/{icon-D1d2UB7T.js → icon-B19NuJYm.js} +31 -31
- package/out/renderer/assets/{icon-ChKJ4xic.css → icon-DryJ1IIw.css} +1 -1
- package/out/renderer/assets/{index-B7nZ8nvT.js → index-CLOkmTr3.js} +1 -1
- package/out/renderer/assets/{index-CcdUoV4M.js → index-CbEqOOj5.js} +370 -365
- package/out/renderer/assets/{index-D2gQKwz6.js → index-Dic-nJw9.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-DFdib3H2.js → infoDiagram-PH2N3AL5-yhFK8QbZ.js} +1 -1
- package/out/renderer/assets/{installer-DfRiHVwp.js → installer-DL2KRfdq.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-CCKfbFqM.js → journeyDiagram-U35MCT3I-Bh_mQdVI.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-gjSxgHvX.js → kanban-definition-NDS4AKOZ-CFxjCPF4.js} +1 -1
- package/out/renderer/assets/{layout-xZfuwHlb.js → layout-CEBnys_f.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-D15ktDDl.js → legacy-agents-page-BV5wNeFT.js} +2 -2
- package/out/renderer/assets/{mermaid.core-CuLiCFaz.js → mermaid.core-CItomUrR.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-b0kWF8-L.js → mindmap-definition-ALO5MXBD-B-uWszwf.js} +1 -1
- package/out/renderer/assets/{page-header-8NSHzS_8.js → page-header-D1a45b0y.js} +1 -1
- package/out/renderer/assets/{parseISO-CN3rYRB1.js → parseISO-Dkhkl_3Q.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-CIwBolgZ.js → pieDiagram-IB7DONF6-CqJrFz8P.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-D2VIfVKV.js → quadrantDiagram-7GDLP6J5-Bla_rG-3.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-bpDgxm80.js → radar-MK3ICKWK-DM-Fttuo.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-C26CyHdt.js → radient-auth-buttons-5BZmMSkU.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-Ds3aLOtI.js → requirementDiagram-KVF5MWMF-f1n0Vj3U.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-VHIxbH9D.js → sankeyDiagram-QLVOVGJD-WBvrc_GC.js} +1 -1
- package/out/renderer/assets/{schedules-page-DmAGPWPg.js → schedules-page-CufYrWWt.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-iy9otA_H.js → sequenceDiagram-X6HHIX6F-DXNQl5UM.js} +1 -1
- package/out/renderer/assets/{settings-page-Cx0klh-0.js → settings-page-BppBUxvd.js} +8 -8
- package/out/renderer/assets/{stateDiagram-DGXRK772-yfApnPmq.js → stateDiagram-DGXRK772-Dc-HE3Jh.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CEPLahMP.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-CxTk8pCf.js → timeline-definition-BDJGKUSR-DAE8SRiO.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-CF5zslUF.js → use-agent-like-mutation-BmPMhO1j.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-B8K04QFZ.js → xychartDiagram-VJFVF3MP-DoRBXwwS.js} +1 -1
- package/out/renderer/index.html +3 -3
- package/out/renderer/installer.html +3 -3
- package/package.json +2 -3
- package/out/renderer/assets/browser-page-wXWEZV9g.js +0 -1
- package/out/renderer/assets/channel-BNKnhGg7.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CVVGZiEg.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CVVGZiEg.js +0 -1
- package/out/renderer/assets/clone-I1HRwzkv.js +0 -1
- 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/${
|
|
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();
|