@shawnstack/quickforge 1.7.4 → 1.7.5
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/dist/assets/AgentProfilesPage-XNjuqJs7.js +1 -0
- package/dist/assets/ChatPanelHost-CuF-PZpc.js +292 -0
- package/dist/assets/CloudAccountSettingsPage-GJSoi6sO.js +1 -0
- package/dist/assets/PluginsPage-BQgp0qN-.js +1 -0
- package/dist/assets/ScheduledTasksPage-DdekATzq.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-B53_eDf2.js → SettingsWorkspacePage-Cb54Ic91.js} +389 -378
- package/dist/assets/ShareLinksSettingsPage-C75WJpaF.js +1 -0
- package/dist/assets/SharedConversationPage-CjMtSQTW.js +1 -0
- package/dist/assets/{TerminalDock-Dgg-_ZmU.js → TerminalDock-D4zJBJc4.js} +2 -2
- package/dist/assets/WorkspaceInspector-D9kdnLDd.js +36 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-Dy7veffk.js → abnfDiagram-VRR7QNED-CMhVoYUh.js} +1 -1
- package/dist/assets/{arc-Bo3p2lGy.js → arc-w21O0TLn.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-DMwFMHxq.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-Cwx8qOvu.js → architectureDiagram-ZJ3FMSHR-C52qL6Hg.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BiuaAOFa.js → blockDiagram-677ZJIJ3-CM3XbLol.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-BTcGfhK2.js → c4Diagram-LMCZKHZV-D7Bh1ui1.js} +1 -1
- package/dist/assets/channel-Duu1CuH9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-B6rHWc4S.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-B9PuF_RF.js → chunk-32BRIVSS-xgrtbT8e.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-3zGItfIM.js → chunk-52WLFC77-CDbaHw80.js} +1 -1
- package/dist/assets/{chunk-5FCAYU7R-DQ66qlmt.js → chunk-5FCAYU7R-CcSvVbPc.js} +1 -1
- package/dist/assets/{chunk-5HE753X5-CLqTO-VF.js → chunk-5HE753X5-erFpzzgx.js} +1 -1
- package/dist/assets/{chunk-5JV3BV7I-Csxv1jr6.js → chunk-5JV3BV7I-rEhN9F6G.js} +1 -1
- package/dist/assets/{chunk-5TONJI2A-BCQttpBr.js → chunk-5TONJI2A-D8L-QTwD.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DEWJGSTb.js → chunk-5VM5RSS4-CjlfMBbM.js} +1 -1
- package/dist/assets/{chunk-7BUUIJ7U-DNorm9MM.js → chunk-7BUUIJ7U-CCcsI9XE.js} +1 -1
- package/dist/assets/{chunk-BIQX33UG-DgvY_BOT.js → chunk-BIQX33UG-FSqFbmb3.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-DMvn9IPs.js → chunk-C7G6YPKG-ZMLUvfLy.js} +1 -1
- package/dist/assets/{chunk-CQNSW5MT-Dq2duUgr.js → chunk-CQNSW5MT-DZuBpk25.js} +1 -1
- package/dist/assets/{chunk-CYSBUYHQ-BovYyc4Y.js → chunk-CYSBUYHQ-CB26wsG0.js} +1 -1
- package/dist/assets/{chunk-EMLP6XTP-BMDOpuYN.js → chunk-EMLP6XTP-BbsM8oRw.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-6eP2UPOf.js → chunk-EX3LRPZG-BcQhaqt7.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-D4q-wTSy.js → chunk-FWX5IMBZ-DrnrdrSu.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-XjIcuzH7.js → chunk-HOUHSVGY-tdc-Z9eW.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-YBXbanq9.js → chunk-ICXQ74PX-Cd7igVGt.js} +1 -1
- package/dist/assets/{chunk-JG7HCLWE-CcN3uNXF.js → chunk-JG7HCLWE-iXXGW8nf.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Cwg6AUAt.js → chunk-JWPE2WC7-BZ87mXEv.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-NNspcGIU.js → chunk-MOJQB5TN-CLTjV5ov.js} +1 -1
- package/dist/assets/{chunk-MOZMSUNE-pMJ_jMos.js → chunk-MOZMSUNE-B3SCBrRH.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-CJy-3Ldv.js → chunk-OGEWGWER-TgvkXkFX.js} +1 -1
- package/dist/assets/{chunk-OSBZ3O6U-D7cLHHOA.js → chunk-OSBZ3O6U-B9E2E5v1.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-D5YBi1Pi.js → chunk-PUDLZKDR-C5GMVYup.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-DYmqQdIf.js → chunk-Q4XR5HBZ-fo5-HsqY.js} +1 -1
- package/dist/assets/{chunk-QBLGF6JB-Bfcx00X0.js → chunk-QBLGF6JB-Dwfg8sKc.js} +1 -1
- package/dist/assets/{chunk-R7FJI6CG-oXzadpZY.js → chunk-R7FJI6CG-DpBrv4ni.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-Dt3-8yUh.js → chunk-RYQCIY6F-DzzmTl11.js} +1 -1
- package/dist/assets/{chunk-U6XO7XAA-B55eSvpx.js → chunk-U6XO7XAA-Dw6aviKa.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-Btk4JwCt.js → chunk-V7JOEXUC-CEDazV6w.js} +1 -1
- package/dist/assets/chunk-VAUOI2AC-CWYjg8zE.js +1 -0
- package/dist/assets/{chunk-VR4S4FIN-DgaSiVOY.js → chunk-VR4S4FIN-C_-eEj-9.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-rRDZ_Tta.js → chunk-WYO6CB5R-NF-MsJJR.js} +3 -3
- package/dist/assets/chunk-XXDRQBXY-DzTwx8ZH.js +1 -0
- package/dist/assets/{chunk-YOTPTUD7-BnFxj5o4.js → chunk-YOTPTUD7-DqzqtdII.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-CrnlPlmJ.js → chunk-ZGVPDNZ5-CRrAYi4g.js} +1 -1
- package/dist/assets/{chunk-ZIRB5QZD-BYTiATIH.js → chunk-ZIRB5QZD-Dc1s5bLM.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BORKJRwF.js → cose-bilkent-JH36ORCC-cL247pHg.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-C8yCvH_o.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-BXKeaSl8.js → cynefinDiagram-TSTJHNR4-D4sZ7LYI.js} +1 -1
- package/dist/assets/{dagre-CrA0XVwJ.js → dagre-DmHb3Ctt.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-ouq_lA1a.js → dagre-VKFMJZFB-BlsnSh-Q.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BxR5kUiQ.js → diagram-FQU43EPY-DW1yQiRO.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-DWonk1sL.js → diagram-G47NLZAW-BTUAsFjb.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-fmV_wV5e.js → diagram-NH7WQ7WH-BcaomqSd.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DvOSRRiV.js → diagram-OA4YK3LP-DDLX7Xwr.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-Bp-xb3uS.js → diagram-WEI45ONY-CIave0An.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-BTlnwneA.js → ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-C3fFWGCm.js → erDiagram-Q63AITRT-D3EgiVVH.js} +1 -1
- package/dist/assets/{esm-oa69b5u4.js → esm-Df4edDpj.js} +2 -2
- package/dist/assets/eventmodeling-45OFAUF4-DO1vRTM0.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DzAiojNi.js → ganttDiagram-NO4QXBWP-B0oyDIqK.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-m5F5ef09.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-Cv57oNEA.js → gitGraphDiagram-IHSO6WYX-uQtr6IqK.js} +1 -1
- package/dist/assets/icons-CXPP1fFT.js +1 -0
- package/dist/assets/index-_ZhK6bvp.js +65 -0
- package/dist/assets/index-iVV2bP_f.css +3 -0
- package/dist/assets/info-DKCQHKI2-CZxPlQJ0.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-B5Z_O3Ro.js → infoDiagram-FWYZ7A6U-B_GzlLIM.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKTK_TKV.js → ishikawaDiagram-FXEZZL3T-CKQIsSdU.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-Cm9k4ywW.js → journeyDiagram-5HDEW3XC-DrrvzZL1.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-DzNteLwx.js → kanban-definition-HUTT4EX6-CJQRTdXz.js} +1 -1
- package/dist/assets/{line-BG78c_WJ.js → line-B4DxX7tC.js} +1 -1
- package/dist/assets/{linear-1X20_EGu.js → linear-U-nKUhyW.js} +1 -1
- package/dist/assets/{map-CCI8WZkS.js → map-BneTM-ri.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-GXXD9K0U.js → mcp-servers-dialog-BUSPvaqI.js} +2 -2
- package/dist/assets/{mermaid-parser.core-Bb8HI_OS.js → mermaid-parser.core-BlKWrEdZ.js} +2 -2
- package/dist/assets/{mermaid-renderer-PIaNiXAz.js → mermaid-renderer-jEbqFaTe.js} +2 -2
- package/dist/assets/{mermaid.core-DUHOIwL5.js → mermaid.core-DWpShXCs.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-Tnbszo7b.js → mindmap-definition-LN4V7U3C-CEVwZD78.js} +1 -1
- package/dist/assets/model-identity-CgA6_Pbm.js +1 -0
- package/dist/assets/model-reference-o41fJ6jQ.js +1 -0
- package/dist/assets/{monaco-BVtCspYZ.js → monaco-D6OhXgjg.js} +1 -1
- package/dist/assets/{ordinal-v4prQcuq.js → ordinal-B2r5L1v4.js} +1 -1
- package/dist/assets/{otel-D1LswXpM.js → otel-BFnphzjt.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-zIioIZVN.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-CX824Kd0.js → pegDiagram-2B236MQR-CBLkBdnQ.js} +1 -1
- package/dist/assets/{pi-ai-DAJlO9ZB.js → pi-ai-Cr2ee4a0.js} +2 -2
- package/dist/assets/{pi-web-ui-B-RJSxjR.js → pi-web-ui-DV5jKjSg.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-GGWijBvB.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-BT04VWPQ.js → pieDiagram-ENE6RG2P-8tCnwtWS.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BvOZxkBr.js → quadrantDiagram-ABIIQ3AL-CWd28cHF.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-yvwFH3RV.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BVadlswB.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-B7R-p_2-.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-BnbJxPug.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-Ci3DcEuQ.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6-DnTU--BV.js → railroadDiagram-RFXS5EU6-CI4ouOBs.js} +1 -1
- package/dist/assets/{react-vendor-eWH0QhNU.js → react-vendor-1KC5PyRx.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE--MZX_yEv.js → requirementDiagram-TGXJPOKE-DtFz7sw4.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-SEZkHhYn.js → sankeyDiagram-HTMAVEWB--VeOwwk9.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BhC2OHiz.js → sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-Ci5LeToV.js → sizeCapture-X5ZJPWSS-vV_ArM8D.js} +1 -1
- package/dist/assets/{skills-dialog-DDh1h8eH.js → skills-dialog-BThnIUBs.js} +3 -3
- package/dist/assets/{src-CovulcRC.js → src-PYsDme0X.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-DKK5tFT4.js → stateDiagram-2N3HPSRC-etXnJ8X8.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-DxxGCk67.js → swimlanes-5IMT3BWC-pabIKAmn.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-BPXB3Sue.js → timeline-definition-FHXFAJF6-C55MKm3L.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-Cru9B2zv.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-DNOVHFaJ.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CxcBCKye.js → vennDiagram-L72KCM5P-CgNf1Q9W.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-E3m1BOJQ.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-DEL6Yes5.js → wardleyDiagram-EHGQE667-Drt5ytec.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-N8cPbcjC.js → xychartDiagram-FW5EYKEG-hhoEo_Ar.js} +1 -1
- package/dist/index.html +9 -8
- package/package.json +3 -1
- package/plugins/{openai-documents → documents}/plugin.json +2 -2
- package/plugins/{openai-presentations → presentations}/plugin.json +2 -2
- package/plugins/{openai-spreadsheets → spreadsheets}/plugin.json +2 -2
- package/server/acp/server.mjs +36 -26
- package/server/agent-manager.mjs +130 -28
- package/server/agent-profile-files.mjs +8 -4
- package/server/agent-profile-schema.mjs +47 -23
- package/server/agent-profiles.mjs +1 -1
- package/server/ai-http-logger.mjs +98 -12
- package/server/cloud/chat-idempotency-store.mjs +96 -0
- package/server/cloud/client.mjs +102 -0
- package/server/cloud/config.mjs +53 -0
- package/server/cloud/credential-store.mjs +193 -0
- package/server/cloud/identity.mjs +355 -0
- package/server/cloud/index.mjs +7 -0
- package/server/cloud/models.mjs +80 -0
- package/server/cloud/runtime.mjs +129 -0
- package/server/cloud/service-config.mjs +77 -0
- package/server/index.mjs +28 -11
- package/server/model-catalog.mjs +262 -0
- package/server/plugins/registry.mjs +29 -5
- package/server/routes/agent-profiles.mjs +41 -7
- package/server/routes/agent.mjs +20 -11
- package/server/routes/backup.mjs +7 -1
- package/server/routes/cloud.mjs +227 -0
- package/server/routes/models.mjs +7 -1
- package/server/routes/scheduled-tasks.mjs +38 -8
- package/server/routes/shared-conversation.mjs +62 -58
- package/server/routes/shares.mjs +76 -1
- package/server/routes/storage.mjs +24 -1
- package/server/routes/workspace.mjs +6 -4
- package/server/share-store.mjs +168 -13
- package/server/skills.mjs +30 -3
- package/server/storage.mjs +7 -1
- package/server/subagents.mjs +3 -3
- package/server/tools/definitions.mjs +1 -1
- package/server/tools/index.mjs +2 -2
- package/server/utils/network.mjs +9 -0
- package/server/utils/response.mjs +8 -1
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/UPSTREAM.md +6 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +247 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/dist/assets/AgentProfilesPage-B84w-r9S.js +0 -1
- package/dist/assets/ChatPanelHost-CyO8fEoj.js +0 -288
- package/dist/assets/PluginsPage-D2862wul.js +0 -1
- package/dist/assets/ScheduledTasksPage-GqbiJDPX.js +0 -2
- package/dist/assets/SharedConversationPage-Cw2QFnbC.js +0 -1
- package/dist/assets/WorkspaceInspector-3uLiu6Bv.js +0 -13
- package/dist/assets/architecture-TIHT7OUA-7FQo5GbP.js +0 -1
- package/dist/assets/channel-DToA8d-4.js +0 -1
- package/dist/assets/chunk-2Q5K7J3B-D_Wxpk-T.js +0 -1
- package/dist/assets/chunk-VAUOI2AC-CIZseNLi.js +0 -1
- package/dist/assets/chunk-XXDRQBXY-Do-XYxat.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Bu24Sy-k.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DKhh5HSW.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-oyiAgYC4.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-YB_0MefX.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-Bc73xdBd.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-Bvp27L0n.js +0 -1
- package/dist/assets/icons-cTkqD_Gi.js +0 -1
- package/dist/assets/index-B48prgLL.js +0 -65
- package/dist/assets/index-DQhwHHer.css +0 -3
- package/dist/assets/info-DKCQHKI2-BsLOivgq.js +0 -1
- package/dist/assets/packet-7NZHBO7P-BKQkXRtk.js +0 -1
- package/dist/assets/pie-RZYD4A2V-BTiTN5Ge.js +0 -1
- package/dist/assets/radar-I7S5WNFK-BU-oVjKM.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-7H2hxNbL.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-Mx6wf6XQ.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-BgXSKhYy.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6-DTEl2rAC.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-D8x3mkni.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-C4nOBNp8.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-DOE6NSdf.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-Ca7QD_b4.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-D9O4RKRm.js +0 -1
- /package/dist/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/assets/{chunk-KEIR6QF5-s7Qq-R3i.js → chunk-KEIR6QF5-jrz1pRUS.js} +0 -0
- /package/dist/assets/{chunk-Y2CYZVJY-BgISNSiw.js → chunk-Y2CYZVJY-D6N-CrJv.js} +0 -0
- /package/dist/assets/{cytoscape.esm-iekjSZ2n.js → cytoscape.esm-DyfqweF2.js} +0 -0
- /package/dist/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/assets/{dist-zzfqk5hx.js → dist-CLg13NtV.js} +0 -0
- /package/dist/assets/{graphlib-CIz_CfxW.js → graphlib-DJkmx0Cq.js} +0 -0
- /package/dist/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/assets/{plugin-api-C8hMT5W6.js → plugin-api-Uu2xAB4d.js} +0 -0
- /package/dist/assets/{rough.esm-BMOtDaaS.js → rough.esm-Bud4g4fh.js} +0 -0
- /package/dist/assets/{web-RgX7H-OH.js → web-DoV8MSMn.js} +0 -0
- /package/dist/assets/{xterm-BFzZ1FH4.js → xterm-Bm_IbozX.js} +0 -0
- /package/plugins/{openai-documents → documents}/commands/document.md +0 -0
- /package/plugins/{openai-documents → documents}/skills/documents/SKILL.md +0 -0
- /package/plugins/{openai-presentations → presentations}/commands/presentation.md +0 -0
- /package/plugins/{openai-presentations → presentations}/skills/presentations/SKILL.md +0 -0
- /package/plugins/{openai-spreadsheets → spreadsheets}/commands/spreadsheet.md +0 -0
- /package/plugins/{openai-spreadsheets → spreadsheets}/skills/spreadsheets/SKILL.md +0 -0
|
@@ -3,6 +3,8 @@ import path from 'node:path'
|
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
4
4
|
import { randomUUID } from 'node:crypto'
|
|
5
5
|
import { streamSimple } from '@earendil-works/pi-ai/compat'
|
|
6
|
+
import { resolveManagedCloudProvider } from './cloud/runtime.mjs'
|
|
7
|
+
import { ensureCloudChatIdempotencyKey } from './cloud/chat-idempotency-store.mjs'
|
|
6
8
|
import {
|
|
7
9
|
DEFAULT_AI_STREAM_DEADLINE_MS,
|
|
8
10
|
withDefaultAiProviderOptions,
|
|
@@ -45,14 +47,27 @@ function writeAiHttpRecord(record) {
|
|
|
45
47
|
})
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
51
|
+
'authorization',
|
|
52
|
+
'proxy-authorization',
|
|
53
|
+
'x-api-key',
|
|
54
|
+
'api-key',
|
|
55
|
+
'idempotency-key',
|
|
56
|
+
'cookie',
|
|
57
|
+
'set-cookie',
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
export function sanitizeHttpHeaders(headers) {
|
|
49
61
|
const result = {}
|
|
50
62
|
if (!headers) return result
|
|
51
63
|
|
|
52
64
|
try {
|
|
53
65
|
const iterable = typeof headers.entries === 'function' ? headers.entries() : Object.entries(headers)
|
|
54
66
|
for (const [key, value] of iterable) {
|
|
55
|
-
|
|
67
|
+
const name = String(key)
|
|
68
|
+
result[name] = SENSITIVE_HEADER_NAMES.has(name.toLowerCase())
|
|
69
|
+
? '[REDACTED]'
|
|
70
|
+
: Array.isArray(value) ? value.join(', ') : String(value)
|
|
56
71
|
}
|
|
57
72
|
} catch {
|
|
58
73
|
// ignore malformed headers
|
|
@@ -61,6 +76,10 @@ function headersToRecord(headers) {
|
|
|
61
76
|
return result
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
function headersToRecord(headers) {
|
|
80
|
+
return sanitizeHttpHeaders(headers)
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
function isRequest(value) {
|
|
65
84
|
return typeof Request !== 'undefined' && value instanceof Request
|
|
66
85
|
}
|
|
@@ -253,6 +272,63 @@ function wrapStreamWithDeadline(stream, deadlineController, deadlineMs) {
|
|
|
253
272
|
}
|
|
254
273
|
}
|
|
255
274
|
|
|
275
|
+
function lazyStream(streamPromise) {
|
|
276
|
+
return {
|
|
277
|
+
result: () => streamPromise.then((stream) => stream.result()),
|
|
278
|
+
[Symbol.asyncIterator]() {
|
|
279
|
+
let iteratorPromise
|
|
280
|
+
const iterator = () => {
|
|
281
|
+
iteratorPromise ||= streamPromise.then((stream) => stream[Symbol.asyncIterator]())
|
|
282
|
+
return iteratorPromise
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
next: async () => (await iterator()).next(),
|
|
286
|
+
return: async () => {
|
|
287
|
+
const resolved = await iterator()
|
|
288
|
+
if (typeof resolved.return === 'function') return resolved.return()
|
|
289
|
+
return { value: undefined, done: true }
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function createProviderStream(model, context, options) {
|
|
297
|
+
return aiHttpLogEnabled
|
|
298
|
+
? aiHttpContext.run({
|
|
299
|
+
traceId: randomUUID(),
|
|
300
|
+
sessionId: options.sessionId,
|
|
301
|
+
purpose: options.metadata?.quickforgePurpose || 'chat',
|
|
302
|
+
provider: model?.provider,
|
|
303
|
+
api: model?.api,
|
|
304
|
+
model: model?.id,
|
|
305
|
+
}, () => streamSimple(model, context, options))
|
|
306
|
+
: streamSimple(model, context, options)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const CLIENT_MESSAGE_ID_FIELD = 'quickforgeClientMessageId'
|
|
310
|
+
|
|
311
|
+
function logicalMessageIdFromContext(context) {
|
|
312
|
+
const messages = Array.isArray(context?.messages) ? context.messages : []
|
|
313
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
314
|
+
const message = messages[index]
|
|
315
|
+
if (message?.role !== 'user' && message?.role !== 'user-with-attachments') continue
|
|
316
|
+
const metadata = message.metadata
|
|
317
|
+
const messageId = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
318
|
+
? metadata[CLIENT_MESSAGE_ID_FIELD]
|
|
319
|
+
: undefined
|
|
320
|
+
return typeof messageId === 'string' && messageId ? messageId : undefined
|
|
321
|
+
}
|
|
322
|
+
return undefined
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function managedCloudIdempotencyKey(context, options) {
|
|
326
|
+
const messageId = logicalMessageIdFromContext(context)
|
|
327
|
+
return options.sessionId && messageId
|
|
328
|
+
? ensureCloudChatIdempotencyKey(options.sessionId, messageId)
|
|
329
|
+
: randomUUID()
|
|
330
|
+
}
|
|
331
|
+
|
|
256
332
|
export function streamSimpleWithAiHttpLogging(model, context, options = {}) {
|
|
257
333
|
const providerOptions = withDefaultAiProviderOptions(options)
|
|
258
334
|
const deadlineMs = Number.isFinite(providerOptions.deadlineMs)
|
|
@@ -265,16 +341,26 @@ export function streamSimpleWithAiHttpLogging(model, context, options = {}) {
|
|
|
265
341
|
}
|
|
266
342
|
delete effectiveOptions.deadlineMs
|
|
267
343
|
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
344
|
+
const managedCloud = model?.provider === 'quickforge-cloud' && model?.quickforgeModelSource === 'cloud'
|
|
345
|
+
const stream = managedCloud
|
|
346
|
+
? lazyStream(Promise.all([
|
|
347
|
+
resolveManagedCloudProvider(model, effectiveOptions.signal),
|
|
348
|
+
managedCloudIdempotencyKey(context, effectiveOptions),
|
|
349
|
+
]).then(([resolved, idempotencyKey]) => createProviderStream(
|
|
350
|
+
{
|
|
351
|
+
...resolved.model,
|
|
352
|
+
headers: {
|
|
353
|
+
...(resolved.model.headers || {}),
|
|
354
|
+
'Idempotency-Key': idempotencyKey,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
context,
|
|
358
|
+
{
|
|
359
|
+
...effectiveOptions,
|
|
360
|
+
apiKey: resolved.apiKey,
|
|
361
|
+
},
|
|
362
|
+
)))
|
|
363
|
+
: createProviderStream(model, context, effectiveOptions)
|
|
278
364
|
|
|
279
365
|
return wrapStreamWithDeadline(stream, deadlineController, deadlineMs)
|
|
280
366
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { ensureStorage, storageDir } from '../storage.mjs'
|
|
5
|
+
|
|
6
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
7
|
+
const MESSAGE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/
|
|
8
|
+
|
|
9
|
+
function digest(value) {
|
|
10
|
+
return createHash('sha256').update(value).digest('hex')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertIdentifier(value, label, pattern) {
|
|
14
|
+
const normalized = typeof value === 'string' ? value.trim() : ''
|
|
15
|
+
if (!pattern.test(normalized)) throw new Error(`Invalid ${label}.`)
|
|
16
|
+
return normalized
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createCloudChatIdempotencyStore({
|
|
20
|
+
directory = path.join(storageDir, 'security', 'cloud-chat-idempotency'),
|
|
21
|
+
ensureBaseStorage = ensureStorage,
|
|
22
|
+
} = {}) {
|
|
23
|
+
function entryPath(sessionId, messageId) {
|
|
24
|
+
return path.join(directory, digest(sessionId), `${digest(messageId)}.json`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function ensureDirectory(file) {
|
|
28
|
+
await ensureBaseStorage()
|
|
29
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readEntry(file, sessionId, messageId) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = await fs.readFile(file, 'utf8')
|
|
35
|
+
const entry = raw.trim() ? JSON.parse(raw) : null
|
|
36
|
+
if (entry?.sessionId !== sessionId || entry?.messageId !== messageId || !UUID_PATTERN.test(entry?.idempotencyKey || '')) {
|
|
37
|
+
throw new Error('Invalid QuickForge Cloud chat idempotency record.')
|
|
38
|
+
}
|
|
39
|
+
return entry
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code === 'ENOENT') return null
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ensure(sessionIdInput, messageIdInput) {
|
|
47
|
+
const sessionId = assertIdentifier(sessionIdInput, 'session ID', /^.{1,512}$/)
|
|
48
|
+
const messageId = assertIdentifier(messageIdInput, 'logical message ID', MESSAGE_ID_PATTERN)
|
|
49
|
+
const file = entryPath(sessionId, messageId)
|
|
50
|
+
await ensureDirectory(file)
|
|
51
|
+
|
|
52
|
+
const existing = await readEntry(file, sessionId, messageId)
|
|
53
|
+
if (existing) return existing.idempotencyKey
|
|
54
|
+
|
|
55
|
+
const entry = {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
sessionId,
|
|
58
|
+
messageId,
|
|
59
|
+
idempotencyKey: randomUUID(),
|
|
60
|
+
createdAt: new Date().toISOString(),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`
|
|
64
|
+
await fs.writeFile(temporary, `${JSON.stringify(entry, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 })
|
|
65
|
+
try { await fs.chmod(temporary, 0o600) } catch { /* best effort on Windows */ }
|
|
66
|
+
try {
|
|
67
|
+
await fs.link(temporary, file)
|
|
68
|
+
try { await fs.chmod(file, 0o600) } catch { /* best effort on Windows */ }
|
|
69
|
+
return entry.idempotencyKey
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error?.code !== 'EEXIST') throw error
|
|
72
|
+
const concurrent = await readEntry(file, sessionId, messageId)
|
|
73
|
+
if (!concurrent) throw error
|
|
74
|
+
return concurrent.idempotencyKey
|
|
75
|
+
} finally {
|
|
76
|
+
await fs.rm(temporary, { force: true })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function removeSession(sessionIdInput) {
|
|
81
|
+
const sessionId = assertIdentifier(sessionIdInput, 'session ID', /^.{1,512}$/)
|
|
82
|
+
await fs.rm(path.join(directory, digest(sessionId)), { recursive: true, force: true })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { directory, ensure, removeSession }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const defaultStore = createCloudChatIdempotencyStore()
|
|
89
|
+
|
|
90
|
+
export function ensureCloudChatIdempotencyKey(sessionId, messageId) {
|
|
91
|
+
return defaultStore.ensure(sessionId, messageId)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function deleteCloudChatIdempotencySession(sessionId) {
|
|
95
|
+
return defaultStore.removeSession(sessionId)
|
|
96
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { cloudEndpoint } from './config.mjs'
|
|
2
|
+
|
|
3
|
+
export class CloudApiError extends Error {
|
|
4
|
+
constructor(message, { status = 0, code = 'cloud_request_failed', retryable = false, details } = {}) {
|
|
5
|
+
super(message)
|
|
6
|
+
this.name = 'CloudApiError'
|
|
7
|
+
this.status = status
|
|
8
|
+
this.code = code
|
|
9
|
+
this.retryable = retryable
|
|
10
|
+
this.details = details
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function parseError(response) {
|
|
15
|
+
let payload
|
|
16
|
+
try { payload = await response.json() } catch { payload = undefined }
|
|
17
|
+
const error = payload?.error
|
|
18
|
+
const structured = error && typeof error === 'object' ? error : undefined
|
|
19
|
+
const oauthCode = typeof error === 'string' ? error : undefined
|
|
20
|
+
return new CloudApiError(
|
|
21
|
+
structured?.message || payload?.error_description || `QuickForge Cloud request failed (${response.status}).`,
|
|
22
|
+
{
|
|
23
|
+
status: response.status,
|
|
24
|
+
code: structured?.code || payload?.code || oauthCode || 'cloud_request_failed',
|
|
25
|
+
retryable: Boolean(structured?.retryable || payload?.retryable),
|
|
26
|
+
details: structured?.details || payload?.details,
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function timeoutSignal(timeoutMs, outerSignal) {
|
|
32
|
+
const timeout = AbortSignal.timeout(timeoutMs)
|
|
33
|
+
return outerSignal ? AbortSignal.any([outerSignal, timeout]) : timeout
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class CloudClient {
|
|
37
|
+
constructor({ baseUrl, timeoutMs = 10_000, fetchImpl = fetch } = {}) {
|
|
38
|
+
if (!(baseUrl instanceof URL)) throw new Error('CloudClient requires a configured base URL.')
|
|
39
|
+
this.baseUrl = baseUrl
|
|
40
|
+
this.timeoutMs = timeoutMs
|
|
41
|
+
this.fetch = fetchImpl
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async request(pathname, { method = 'GET', token, body, headers, signal, raw = false, timeout = true } = {}) {
|
|
45
|
+
const requestHeaders = { Accept: 'application/json', ...headers }
|
|
46
|
+
if (body !== undefined) requestHeaders['Content-Type'] = 'application/json'
|
|
47
|
+
if (token) requestHeaders.Authorization = `Bearer ${token}`
|
|
48
|
+
const response = await this.fetch(cloudEndpoint(this.baseUrl, pathname), {
|
|
49
|
+
method,
|
|
50
|
+
headers: requestHeaders,
|
|
51
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
52
|
+
signal: timeout ? timeoutSignal(this.timeoutMs, signal) : signal,
|
|
53
|
+
redirect: 'error',
|
|
54
|
+
})
|
|
55
|
+
if (!response.ok) throw await parseError(response)
|
|
56
|
+
if (raw) return response
|
|
57
|
+
if (response.status === 204) return undefined
|
|
58
|
+
return response.json()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
health(signal) { return this.request('healthz', { signal }) }
|
|
62
|
+
ready(signal) { return this.request('readyz', { signal }) }
|
|
63
|
+
registerGuest(input, idempotencyKey, signal) {
|
|
64
|
+
return this.request('v1/guest/installations', {
|
|
65
|
+
method: 'POST', body: input, headers: { 'Idempotency-Key': idempotencyKey }, signal,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
refresh(refreshToken, signal) {
|
|
69
|
+
return this.request('oauth/token', {
|
|
70
|
+
method: 'POST', body: { grantType: 'refresh_token', refreshToken }, signal,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
authorizeDevice(token, installationId, clientId = 'quickforge-desktop', signal) {
|
|
74
|
+
return this.request('oauth/device_authorization', {
|
|
75
|
+
method: 'POST', token, body: { installationId, clientId }, signal,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
exchangeDeviceCode(deviceCode, clientId = 'quickforge-desktop', signal) {
|
|
79
|
+
return this.request('oauth/token', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: {
|
|
82
|
+
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
83
|
+
deviceCode,
|
|
84
|
+
clientId,
|
|
85
|
+
},
|
|
86
|
+
signal,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
me(token, signal) { return this.request('v1/me', { token, signal }) }
|
|
90
|
+
models(token, signal) { return this.request('v1/models', { token, signal }) }
|
|
91
|
+
usage(token, signal) { return this.request('v1/usage', { token, signal }) }
|
|
92
|
+
installations(token, signal) { return this.request('v1/installations', { token, signal }) }
|
|
93
|
+
revokeInstallation(token, installationId, signal) {
|
|
94
|
+
return this.request(`v1/installations/${encodeURIComponent(installationId)}`, { method: 'DELETE', token, signal })
|
|
95
|
+
}
|
|
96
|
+
chat(token, payload, idempotencyKey, signal) {
|
|
97
|
+
return this.request('v1/chat/completions', {
|
|
98
|
+
method: 'POST', token, body: payload,
|
|
99
|
+
headers: { 'Idempotency-Key': idempotencyKey }, signal, raw: true, timeout: false,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import net from 'node:net'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10_000
|
|
4
|
+
|
|
5
|
+
function isLoopbackHost(hostname) {
|
|
6
|
+
const host = String(hostname || '').toLowerCase()
|
|
7
|
+
if (host === 'localhost' || host.endsWith('.localhost')) return true
|
|
8
|
+
const ip = net.isIP(host)
|
|
9
|
+
if (ip === 4) return host.startsWith('127.')
|
|
10
|
+
if (ip === 6) return host === '::1' || host === '0:0:0:0:0:0:0:1'
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseCloudBaseUrl(value, _options = {}) {
|
|
15
|
+
const raw = typeof value === 'string' ? value.trim() : ''
|
|
16
|
+
if (!raw) return undefined
|
|
17
|
+
|
|
18
|
+
let url
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(raw)
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error('QUICKFORGE_CLOUD_URL must be a valid URL.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
26
|
+
throw new Error('QUICKFORGE_CLOUD_URL must not contain credentials, query parameters, or fragments.')
|
|
27
|
+
}
|
|
28
|
+
if (url.protocol !== 'https:') {
|
|
29
|
+
if (!(url.protocol === 'http:' && isLoopbackHost(url.hostname))) {
|
|
30
|
+
throw new Error('QUICKFORGE_CLOUD_URL must use HTTPS. HTTP is allowed only for loopback addresses.')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname.includes('..')) throw new Error('QUICKFORGE_CLOUD_URL contains an unsafe path.')
|
|
34
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/`
|
|
35
|
+
return url
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cloudEndpoint(baseUrl, pathname) {
|
|
39
|
+
if (!(baseUrl instanceof URL)) throw new Error('QuickForge Cloud is not configured.')
|
|
40
|
+
const relative = String(pathname || '').replace(/^\/+/, '')
|
|
41
|
+
if (!relative || relative.split('/').includes('..')) throw new Error('Invalid QuickForge Cloud endpoint.')
|
|
42
|
+
return new URL(relative, baseUrl)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readCloudConfig(env = process.env) {
|
|
46
|
+
const baseUrl = parseCloudBaseUrl(env.QUICKFORGE_CLOUD_URL)
|
|
47
|
+
const timeoutValue = Number(env.QUICKFORGE_CLOUD_TIMEOUT_MS || DEFAULT_TIMEOUT_MS)
|
|
48
|
+
return {
|
|
49
|
+
enabled: Boolean(baseUrl),
|
|
50
|
+
baseUrl,
|
|
51
|
+
timeoutMs: Number.isFinite(timeoutValue) ? Math.min(60_000, Math.max(1_000, timeoutValue)) : DEFAULT_TIMEOUT_MS,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { generateKeyPair } from 'node:crypto'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
import { createRandomToken } from '../utils/password-auth.mjs'
|
|
7
|
+
import { ensureStorage, storageDir } from '../storage.mjs'
|
|
8
|
+
|
|
9
|
+
const generateKeyPairAsync = promisify(generateKeyPair)
|
|
10
|
+
const SCHEMA_VERSION = 1
|
|
11
|
+
const ACCOUNT_FIELDS = ['id', 'email', 'plan']
|
|
12
|
+
|
|
13
|
+
function publicAccount(value) {
|
|
14
|
+
if (!value || typeof value !== 'object') return undefined
|
|
15
|
+
const account = {}
|
|
16
|
+
for (const field of ACCOUNT_FIELDS) {
|
|
17
|
+
const candidate = value[field]
|
|
18
|
+
if (typeof candidate === 'string' && candidate) account[field] = candidate
|
|
19
|
+
}
|
|
20
|
+
return Object.keys(account).length ? account : undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function publicPendingDeviceFlow(value) {
|
|
24
|
+
if (!value || typeof value !== 'object') return undefined
|
|
25
|
+
const expiresAt = Number(value.expiresAt)
|
|
26
|
+
if (!Number.isFinite(expiresAt)) return undefined
|
|
27
|
+
return {
|
|
28
|
+
userCode: String(value.userCode || ''),
|
|
29
|
+
verificationUri: String(value.verificationUri || ''),
|
|
30
|
+
verificationUriComplete: String(value.verificationUriComplete || ''),
|
|
31
|
+
expiresAt,
|
|
32
|
+
interval: Math.max(1, Number(value.interval) || 5),
|
|
33
|
+
status: String(value.status || 'pending'),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function platformName(platform = process.platform) {
|
|
38
|
+
if (platform === 'win32') return 'windows'
|
|
39
|
+
if (platform === 'darwin') return 'macos'
|
|
40
|
+
return 'linux'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultRecord({ installationName, platform, clientVersion } = {}) {
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: SCHEMA_VERSION,
|
|
46
|
+
mode: 'local',
|
|
47
|
+
installationName: installationName || os.hostname() || 'QuickForge',
|
|
48
|
+
platform: platformName(platform),
|
|
49
|
+
clientVersion: clientVersion || 'unknown',
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeRecord(value, defaults) {
|
|
55
|
+
const input = value && typeof value === 'object' ? value : {}
|
|
56
|
+
const base = defaultRecord(defaults)
|
|
57
|
+
return {
|
|
58
|
+
...base,
|
|
59
|
+
...input,
|
|
60
|
+
schemaVersion: SCHEMA_VERSION,
|
|
61
|
+
mode: ['local', 'guest', 'account'].includes(input.mode) ? input.mode : 'local',
|
|
62
|
+
installationName: String(input.installationName || base.installationName).slice(0, 120),
|
|
63
|
+
platform: ['windows', 'macos', 'linux'].includes(input.platform) ? input.platform : base.platform,
|
|
64
|
+
clientVersion: String(input.clientVersion || base.clientVersion).slice(0, 80),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function publicRecord(record) {
|
|
69
|
+
return {
|
|
70
|
+
configured: true,
|
|
71
|
+
mode: record.mode,
|
|
72
|
+
installationId: record.installationId,
|
|
73
|
+
installationName: record.installationName,
|
|
74
|
+
platform: record.platform,
|
|
75
|
+
clientVersion: record.clientVersion,
|
|
76
|
+
hasInstallationKey: Boolean(record.publicKey && record.privateKeyPkcs8),
|
|
77
|
+
hasSession: Boolean(record.refreshToken),
|
|
78
|
+
account: publicAccount(record.account),
|
|
79
|
+
pendingDeviceFlow: publicPendingDeviceFlow(record.pendingDeviceFlow),
|
|
80
|
+
updatedAt: record.updatedAt,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function generateInstallationKeyPair() {
|
|
85
|
+
const { publicKey, privateKey } = await generateKeyPairAsync('ed25519')
|
|
86
|
+
const publicJwk = publicKey.export({ format: 'jwk' })
|
|
87
|
+
return {
|
|
88
|
+
publicKey: publicJwk.x,
|
|
89
|
+
privateKeyPkcs8: privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64url'),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createCloudCredentialStore({
|
|
94
|
+
filePath = path.join(storageDir, 'security', 'cloud-identity.json'),
|
|
95
|
+
ensureBaseStorage = ensureStorage,
|
|
96
|
+
installationName,
|
|
97
|
+
platform,
|
|
98
|
+
clientVersion,
|
|
99
|
+
} = {}) {
|
|
100
|
+
let queue = Promise.resolve()
|
|
101
|
+
const defaults = { installationName, platform, clientVersion }
|
|
102
|
+
|
|
103
|
+
async function ensureDirectory() {
|
|
104
|
+
await ensureBaseStorage()
|
|
105
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function read() {
|
|
109
|
+
await ensureDirectory()
|
|
110
|
+
try {
|
|
111
|
+
const raw = await fs.readFile(filePath, 'utf8')
|
|
112
|
+
return normalizeRecord(raw.trim() ? JSON.parse(raw) : {}, defaults)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code === 'ENOENT') return defaultRecord(defaults)
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeNow(record) {
|
|
120
|
+
await ensureDirectory()
|
|
121
|
+
const normalized = normalizeRecord({ ...record, updatedAt: new Date().toISOString() }, defaults)
|
|
122
|
+
const temporary = `${filePath}.${process.pid}.${createRandomToken(8)}.tmp`
|
|
123
|
+
await fs.writeFile(temporary, `${JSON.stringify(normalized, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
124
|
+
try { await fs.chmod(temporary, 0o600) } catch { /* best effort on Windows */ }
|
|
125
|
+
try {
|
|
126
|
+
await fs.rename(temporary, filePath)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!['EEXIST', 'EPERM'].includes(error?.code)) throw error
|
|
129
|
+
await fs.rm(filePath, { force: true })
|
|
130
|
+
await fs.rename(temporary, filePath)
|
|
131
|
+
}
|
|
132
|
+
try { await fs.chmod(filePath, 0o600) } catch { /* best effort on Windows */ }
|
|
133
|
+
return normalized
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function update(mutator) {
|
|
137
|
+
const next = queue.catch(() => undefined).then(async () => {
|
|
138
|
+
const current = await read()
|
|
139
|
+
const result = await mutator({ ...current })
|
|
140
|
+
return writeNow(result || current)
|
|
141
|
+
})
|
|
142
|
+
queue = next
|
|
143
|
+
return next
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function ensureInstallation() {
|
|
147
|
+
const current = await read()
|
|
148
|
+
if (current.publicKey && current.privateKeyPkcs8) return current
|
|
149
|
+
return update(async (record) => ({ ...record, ...(await generateInstallationKeyPair()) }))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function rotateInstallation() {
|
|
153
|
+
const keyPair = await generateInstallationKeyPair()
|
|
154
|
+
return update((record) => ({
|
|
155
|
+
...record,
|
|
156
|
+
...keyPair,
|
|
157
|
+
mode: 'local',
|
|
158
|
+
installationId: undefined,
|
|
159
|
+
refreshToken: undefined,
|
|
160
|
+
sessionCloudUrl: undefined,
|
|
161
|
+
pendingRegistrationKey: undefined,
|
|
162
|
+
pendingRegistrationHash: undefined,
|
|
163
|
+
pendingDeviceFlow: undefined,
|
|
164
|
+
account: undefined,
|
|
165
|
+
rotateInstallationBeforeRegistration: undefined,
|
|
166
|
+
}))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
filePath,
|
|
171
|
+
read,
|
|
172
|
+
update,
|
|
173
|
+
ensureInstallation,
|
|
174
|
+
rotateInstallation,
|
|
175
|
+
async readPublic() { return publicRecord(await read()) },
|
|
176
|
+
async clearSession({ rotateInstallationBeforeRegistration = false } = {}) {
|
|
177
|
+
return update((record) => ({
|
|
178
|
+
...record,
|
|
179
|
+
mode: 'local',
|
|
180
|
+
installationId: undefined,
|
|
181
|
+
refreshToken: undefined,
|
|
182
|
+
sessionCloudUrl: undefined,
|
|
183
|
+
pendingRegistrationKey: undefined,
|
|
184
|
+
pendingRegistrationHash: undefined,
|
|
185
|
+
pendingDeviceFlow: undefined,
|
|
186
|
+
account: undefined,
|
|
187
|
+
rotateInstallationBeforeRegistration: rotateInstallationBeforeRegistration || undefined,
|
|
188
|
+
}))
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { publicRecord as publicCloudCredentialRecord }
|