shadow-claw 1.23.3 → 1.24.0
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/README.md +42 -5
- package/bin/assert-version-bump.mjs +60 -38
- package/bin/assert-version-bump.test.mjs +74 -11
- package/bin/cli.mjs +166 -0
- package/bin/cli.test.mjs +60 -0
- package/bin/commands/backup.mjs +70 -0
- package/bin/commands/clients.mjs +41 -0
- package/bin/commands/peer-id.mjs +76 -0
- package/bin/commands/peer-id.test.mjs +104 -0
- package/bin/commands/send.mjs +51 -0
- package/bin/commands/tasks.mjs +59 -0
- package/bin/commands/webrtc-listen.mjs +254 -0
- package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
- package/bin/rolldown.config.test.mjs +401 -8
- package/bin/utils/control-client.mjs +243 -0
- package/bin/utils/control-client.test.mjs +83 -0
- package/bin/utils/ulid.mjs +45 -0
- package/bin/utils/webrtc-control-client.mjs +699 -0
- package/bin/utils/webrtc-control-client.test.mjs +318 -0
- package/dist/electron/main.cjs +1 -1
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +42 -5
- package/dist/public/agent.worker.js +6 -6
- package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
- package/dist/public/backup-controller-C_s9in2t.js +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
- package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
- package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
- package/dist/public/config-CrHchneq.js +1 -0
- package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
- package/dist/public/docs/README.md +3 -1
- package/dist/public/docs/architecture/overview.md +12 -12
- package/dist/public/docs/architecture/storage.md +5 -2
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
- package/dist/public/docs/guides/server-development-configuration.md +24 -0
- package/dist/public/docs/publishing/index.html +19 -76
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/docs/subsystems/backup.md +56 -0
- package/dist/public/docs/subsystems/cli.md +124 -0
- package/dist/public/docs/subsystems/control-plane.md +158 -0
- package/dist/public/docs/subsystems/notifications.md +54 -2
- package/dist/public/docs/subsystems/reactive-ui.md +1 -0
- package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
- package/dist/public/e2e/README.md +5 -1
- package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
- package/dist/public/initControlPlane-C9jpnWev.js +1 -0
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
- package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
- package/dist/public/service-worker/fetch-proxy.js +1 -1
- package/dist/public/service-worker/push-handler.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
- package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
- package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
- package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
- package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
- package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
- package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
- package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
- package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
- package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
- package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/static-main-manifest.json +1 -1
- package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
- package/dist/server.js +286 -189
- package/package.json +2 -1
- package/src/__mocks__/jszip.cjs +15 -5
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
- package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
- package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
- package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
- package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
- package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
- package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
- package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
- package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
- package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
- package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
- package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
- package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
- package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
- package/src/config/config.test.ts +9 -0
- package/src/config/config.ts +15 -0
- package/src/core/app-routes.test.ts +56 -0
- package/src/core/app-routes.ts +8 -1
- package/src/core/backup-controller.test.ts +101 -0
- package/src/core/backup-controller.ts +137 -0
- package/src/core/control-plane-client.test.ts +408 -0
- package/src/core/control-plane-client.ts +425 -0
- package/src/core/orchestrator/orchestrator.ts +4 -0
- package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
- package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
- package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
- package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
- package/src/core/utils/initControlPlane.test.ts +276 -0
- package/src/core/utils/initControlPlane.ts +437 -0
- package/src/core/utils/initializeApp.ts +18 -0
- package/src/db/getSession.test.ts +13 -1
- package/src/db/openDatabase.test.ts +10 -1
- package/src/db/openDatabase.ts +3 -1
- package/src/server/app.test.ts +12 -0
- package/src/server/app.ts +18 -8
- package/src/server/client-registry.test.ts +250 -0
- package/src/server/client-registry.ts +477 -0
- package/src/server/config.ts +15 -0
- package/src/server/control-plane-types.ts +120 -0
- package/src/server/control-plane.test.ts +472 -0
- package/src/server/control-plane.ts +581 -0
- package/src/server/routes/backup.test.ts +263 -0
- package/src/server/routes/backup.ts +262 -0
- package/src/server/routes/integrations-email.test.ts +532 -0
- package/src/server/routes/llamafile.test.ts +172 -48
- package/src/server/routes/mesh-llm.test.ts +192 -0
- package/src/server/routes/ollama.test.ts +49 -0
- package/src/server/server-peer.test.ts +86 -0
- package/src/server/server-peer.ts +330 -0
- package/src/server/server.ts +35 -0
- package/src/server/services/llamafile-manager.test.ts +230 -6
- package/src/server/services/transformers-runtime.test.ts +186 -18
- package/src/server/webrtc.test.ts +21 -0
- package/src/server/webrtc.ts +72 -0
- package/src/service-worker/fetch-proxy.test.ts +64 -1
- package/src/service-worker/push-handler.ts +39 -0
- package/src/shell/vm.test.ts +744 -12
- package/src/storage/ensureMainGroupIndex.test.ts +153 -0
- package/src/storage/getGroupFile.test.ts +66 -0
- package/src/storage/memoryStorage.test.ts +82 -2
- package/src/storage/migrateLegacyOpfs.test.ts +327 -0
- package/src/storage/migrateLegacyOpfs.ts +117 -0
- package/src/storage/writeGroupFileBytes.test.ts +143 -0
- package/src/stores/chat-ui.test.ts +20 -0
- package/src/stores/orchestrator.test.ts +134 -0
- package/src/stores/toast.test.ts +50 -0
- package/src/stores/tools.test.ts +147 -10
- package/src/subsystems/channels/imessage.test.ts +59 -0
- package/src/subsystems/channels/peerjs.test.ts +500 -62
- package/src/subsystems/channels/peerjs.ts +71 -4
- package/src/subsystems/channels/telegram.test.ts +177 -0
- package/src/subsystems/email/catalog.test.ts +1 -0
- package/src/subsystems/email/connections.test.ts +39 -1
- package/src/subsystems/git/credentials.test.ts +160 -825
- package/src/subsystems/git/git.test.ts +463 -0
- package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
- package/src/subsystems/notifications/push-routes.test.ts +33 -0
- package/src/subsystems/notifications/push-routes.ts +23 -0
- package/src/subsystems/notifications/push-store.ts +1 -1
- package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
- package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
- package/src/subsystems/providers/model-registry.test.ts +115 -0
- package/src/subsystems/providers/providers.test.ts +94 -1
- package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
- package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
- package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
- package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
- package/src/subsystems/skills/activateSkill.test.ts +101 -0
- package/src/subsystems/skills/discoverSkills.test.ts +251 -0
- package/src/subsystems/tools/cron.test.ts +64 -0
- package/src/subsystems/tools/declarative.test.ts +255 -89
- package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
- package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
- package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
- package/src/testing/jest-setup.ts +328 -40
- package/src/testing/mock-v86.mjs +49 -0
- package/src/ui/a2ui/types.ts +4 -3
- package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
- package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
- package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
- package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
- package/src/worker/tools/email/email.test.ts +193 -8
- package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
- package/src/worker/tools/git/git.test.ts +214 -3
- package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
- package/src/worker/tools/rooms/leave-room.test.ts +40 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
- package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
- package/src/worker/tools/ui/ask-user.test.ts +50 -0
- package/src/worker/tools/ui/clear-chat.test.ts +19 -0
- package/src/worker/tools/ui/get-current-time.test.ts +19 -0
- package/src/worker/tools/ui/javascript.test.ts +116 -0
- package/src/worker/tools/ui/list-components.test.ts +10 -0
- package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
- package/src/worker/tools/ui/manage-tools.test.ts +34 -0
- package/src/worker/tools/ui/render-component.test.ts +346 -0
- package/src/worker/tools/ui/send-notification.test.ts +40 -0
- package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
- package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
- package/src/worker/tools/workspace/attach-file.test.ts +84 -0
- package/src/worker/tools/workspace/copy-file.test.ts +69 -0
- package/src/worker/tools/workspace/create-directory.test.ts +40 -0
- package/src/worker/tools/workspace/delete-file.test.ts +73 -0
- package/src/worker/tools/workspace/diff-files.test.ts +94 -0
- package/src/worker/tools/workspace/list-files.test.ts +36 -0
- package/src/worker/tools/workspace/move-file.test.ts +66 -0
- package/src/worker/tools/workspace/open-file.test.ts +57 -0
- package/src/worker/tools/workspace/patch-file.test.ts +81 -0
- package/src/worker/tools/workspace/read-file.test.ts +116 -0
- package/src/worker/tools/workspace/send-file.test.ts +82 -0
- package/src/worker/tools/workspace/update-memory.test.ts +29 -0
- package/src/worker/tools/workspace/write-file.test.ts +28 -0
- package/src/worker/utils/rate-limit.test.ts +199 -0
- package/src/worker/utils/tool-state.test.ts +29 -0
- package/src/worker/utils/transformers-js-utils.test.ts +59 -0
- package/dist/public/config-AL77QAx4.js +0 -1
- package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
- package/dist/public/file-viewer-CCSABMsE.js +0 -1
- package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
- package/dist/public/orchestrator-4su7iyWj.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
- package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
- package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
|
@@ -1,902 +1,237 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
2
|
|
|
3
|
+
const mockDb = {} as any;
|
|
4
|
+
let storedConfig: Record<string, any> = {};
|
|
5
|
+
|
|
3
6
|
jest.unstable_mockModule("../../db/getConfig.js", () => ({
|
|
4
|
-
getConfig:
|
|
7
|
+
getConfig: jest.fn(async (_db: any, key: string) => storedConfig[key]),
|
|
5
8
|
}));
|
|
6
9
|
|
|
7
10
|
jest.unstable_mockModule("../../db/setConfig.js", () => ({
|
|
8
|
-
setConfig: jest.fn
|
|
11
|
+
setConfig: jest.fn(async (_db: any, key: string, value: any) => {
|
|
12
|
+
storedConfig[key] = value;
|
|
13
|
+
}),
|
|
9
14
|
}));
|
|
10
15
|
|
|
11
16
|
jest.unstable_mockModule("../../security/crypto.js", () => ({
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
encryptValue: jest.fn(async (val: string) => `enc:${val}`),
|
|
18
|
+
decryptValue: jest.fn(async (val: string) =>
|
|
19
|
+
val.startsWith("enc:") ? val.slice(4) : val,
|
|
20
|
+
),
|
|
14
21
|
}));
|
|
15
22
|
|
|
16
23
|
jest.unstable_mockModule("../../config/config.js", () => ({
|
|
17
24
|
CONFIG_KEYS: {
|
|
18
25
|
GIT_ACCOUNTS: "git_accounts",
|
|
19
|
-
GIT_AUTHOR_EMAIL: "git_author_email",
|
|
20
|
-
GIT_AUTHOR_NAME: "git_author_name",
|
|
21
26
|
GIT_DEFAULT_ACCOUNT: "git_default_account",
|
|
22
|
-
GIT_PASSWORD: "git_password",
|
|
23
27
|
GIT_TOKEN: "git_token",
|
|
24
28
|
GIT_USERNAME: "git_username",
|
|
29
|
+
GIT_PASSWORD: "git_password",
|
|
30
|
+
GIT_AUTHOR_NAME: "git_author_name",
|
|
31
|
+
GIT_AUTHOR_EMAIL: "git_author_email",
|
|
25
32
|
},
|
|
26
|
-
getProviderTokenAuthScheme: (
|
|
27
|
-
providerId
|
|
28
|
-
|
|
29
|
-
serviceType = "http_api",
|
|
30
|
-
) => {
|
|
31
|
-
const schemes = {
|
|
32
|
-
github: {
|
|
33
|
-
default: {
|
|
34
|
-
oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
|
|
35
|
-
token: { headerName: "Authorization", headerPrefix: "token " },
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
gitlab: {
|
|
39
|
-
byServiceType: {
|
|
40
|
-
git_remote: {
|
|
41
|
-
oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
|
|
42
|
-
token: { headerName: "Authorization", headerPrefix: "Bearer " },
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
default: {
|
|
46
|
-
oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
|
|
47
|
-
token: { headerName: "PRIVATE-TOKEN", headerPrefix: "" },
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
azure_devops: {
|
|
51
|
-
byServiceType: {
|
|
52
|
-
git_remote: {
|
|
53
|
-
oauth: { headerName: "Authorization", headerPrefix: "Basic " },
|
|
54
|
-
token: { headerName: "Authorization", headerPrefix: "Basic " },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
default: {
|
|
58
|
-
oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
|
|
59
|
-
token: { headerName: "Authorization", headerPrefix: "Bearer " },
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const provider = schemes[providerId];
|
|
65
|
-
if (!provider) {
|
|
66
|
-
return null;
|
|
33
|
+
getProviderTokenAuthScheme: jest.fn((providerId: string) => {
|
|
34
|
+
if (providerId === "azure_devops") {
|
|
35
|
+
return { headerName: "Authorization", headerPrefix: "Basic " };
|
|
67
36
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
provider.byServiceType?.[serviceType]?.[authMode] ||
|
|
71
|
-
provider.default?.[authMode] ||
|
|
72
|
-
null
|
|
73
|
-
);
|
|
74
|
-
},
|
|
37
|
+
return { headerName: "Authorization", headerPrefix: "Bearer " };
|
|
38
|
+
}),
|
|
75
39
|
}));
|
|
76
40
|
|
|
77
|
-
const {
|
|
78
|
-
const { setConfig } = await import("../../db/setConfig.js");
|
|
79
|
-
const { resolveGitCredentials, detectProvider, buildAuthHeaders } =
|
|
41
|
+
const { detectProvider, buildAuthHeaders, resolveGitCredentials } =
|
|
80
42
|
await import("./credentials.js");
|
|
81
43
|
|
|
82
|
-
|
|
83
|
-
const mockSetConfig = setConfig as any;
|
|
84
|
-
|
|
85
|
-
function setupConfig(map: any) {
|
|
86
|
-
(mockGetConfig as any).mockImplementation(
|
|
87
|
-
async (_db: any, key: any) => map[key],
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const fakeDb: any = {} as any;
|
|
92
|
-
|
|
93
|
-
describe("resolveGitCredentials", () => {
|
|
44
|
+
describe("git credentials", () => {
|
|
94
45
|
beforeEach(() => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(mockGetConfig as any).mockResolvedValue(undefined);
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
storedConfig = {};
|
|
98
48
|
});
|
|
99
49
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
authorEmail: undefined,
|
|
105
|
-
authorName: undefined,
|
|
106
|
-
password: undefined,
|
|
107
|
-
provider: "generic",
|
|
108
|
-
token: undefined,
|
|
109
|
-
username: undefined,
|
|
50
|
+
describe("detectProvider", () => {
|
|
51
|
+
it("detects GitHub", () => {
|
|
52
|
+
expect(detectProvider("github.com")).toBe("github");
|
|
53
|
+
expect(detectProvider("https://github.com/owner/repo")).toBe("github");
|
|
110
54
|
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ── Legacy single-account fallback ─────────────────────────────
|
|
114
|
-
describe("legacy fallback (no accounts configured)", () => {
|
|
115
|
-
it("returns decrypted token from legacy GIT_TOKEN", async () => {
|
|
116
|
-
setupConfig({ git_token: "enc-tok" });
|
|
117
55
|
|
|
118
|
-
|
|
119
|
-
expect(
|
|
56
|
+
it("detects Azure DevOps", () => {
|
|
57
|
+
expect(detectProvider("dev.azure.com")).toBe("azure-devops");
|
|
58
|
+
expect(detectProvider("myorg.visualstudio.com")).toBe("azure-devops");
|
|
120
59
|
});
|
|
121
60
|
|
|
122
|
-
it("
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
git_username: "alice",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const result = await resolveGitCredentials(fakeDb);
|
|
129
|
-
expect(result.username).toBe("alice");
|
|
130
|
-
expect(result.password).toBe("decrypted:enc-pw");
|
|
61
|
+
it("detects GitLab", () => {
|
|
62
|
+
expect(detectProvider("gitlab.com")).toBe("gitlab");
|
|
63
|
+
expect(detectProvider("https://gitlab.example.com/repo")).toBe("gitlab");
|
|
131
64
|
});
|
|
132
65
|
|
|
133
|
-
it("
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
git_author_name: "Alice",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const result = await resolveGitCredentials(fakeDb);
|
|
140
|
-
expect(result.authorName).toBe("Alice");
|
|
141
|
-
expect(result.authorEmail).toBe("alice@example.com");
|
|
66
|
+
it("falls back to generic for other hosts", () => {
|
|
67
|
+
expect(detectProvider("bitbucket.org")).toBe("generic");
|
|
68
|
+
expect(detectProvider("")).toBe("generic");
|
|
142
69
|
});
|
|
143
70
|
});
|
|
144
71
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
token: "enc-gh-tok",
|
|
155
|
-
username: "",
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const azureAccount: any = {
|
|
159
|
-
authorEmail: "az@example.com",
|
|
160
|
-
authorName: "AZ User",
|
|
161
|
-
hostPattern: "dev.azure.com",
|
|
162
|
-
id: "acct-2",
|
|
163
|
-
label: "ADO",
|
|
164
|
-
password: "enc-az-pw",
|
|
165
|
-
token: "enc-az-tok",
|
|
166
|
-
username: "azuser",
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
it("matches account by URL hostname", async () => {
|
|
170
|
-
setupConfig({
|
|
171
|
-
git_accounts: [githubAccount, azureAccount],
|
|
172
|
-
git_default_account: "acct-1",
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const result = await resolveGitCredentials(
|
|
176
|
-
fakeDb,
|
|
177
|
-
"https://github.com/org/repo.git",
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
expect(result.token).toBe("decrypted:enc-gh-tok");
|
|
181
|
-
expect(result.authorName).toBe("GH User");
|
|
182
|
-
expect(result.authorEmail).toBe("gh@example.com");
|
|
72
|
+
describe("buildAuthHeaders", () => {
|
|
73
|
+
it("returns empty object when reauthRequired is true", () => {
|
|
74
|
+
expect(
|
|
75
|
+
buildAuthHeaders({
|
|
76
|
+
reauthRequired: true,
|
|
77
|
+
token: "secret-token",
|
|
78
|
+
provider: "github",
|
|
79
|
+
}),
|
|
80
|
+
).toEqual({});
|
|
183
81
|
});
|
|
184
82
|
|
|
185
|
-
it("
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
83
|
+
it("builds Authorization header for Azure DevOps basic/token", () => {
|
|
84
|
+
const headers = buildAuthHeaders({
|
|
85
|
+
provider: "azure-devops",
|
|
86
|
+
token: "pat123",
|
|
189
87
|
});
|
|
190
|
-
|
|
191
|
-
const result = await resolveGitCredentials(
|
|
192
|
-
fakeDb,
|
|
193
|
-
"https://dev.azure.com/org/project/_git/repo",
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
expect(result.authorName).toBe("AZ User");
|
|
197
|
-
expect(result.password).toBe("decrypted:enc-az-pw");
|
|
198
|
-
expect(result.token).toBe("decrypted:enc-az-tok");
|
|
199
|
-
expect(result.username).toBe("azuser");
|
|
88
|
+
expect(headers.Authorization).toBe(`Basic ${btoa(":pat123")}`);
|
|
200
89
|
});
|
|
201
90
|
|
|
202
|
-
it("
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
91
|
+
it("builds Authorization header for Azure DevOps username/password", () => {
|
|
92
|
+
const headers = buildAuthHeaders({
|
|
93
|
+
provider: "azure-devops",
|
|
94
|
+
username: "user1",
|
|
95
|
+
password: "pwd",
|
|
206
96
|
});
|
|
207
|
-
|
|
208
|
-
const result = await resolveGitCredentials(
|
|
209
|
-
fakeDb,
|
|
210
|
-
"https://gitlab.com/org/repo.git",
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// Should NOT send Azure credentials to GitLab -- that will cause 401s on
|
|
214
|
-
// public repos. Instead, return empty creds for anonymous clones
|
|
215
|
-
expect(result.token).toBeUndefined();
|
|
216
|
-
expect(result.username).toBeUndefined();
|
|
217
|
-
expect(result.password).toBeUndefined();
|
|
97
|
+
expect(headers.Authorization).toBe(`Basic ${btoa("user1:pwd")}`);
|
|
218
98
|
});
|
|
219
99
|
|
|
220
|
-
it("
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
100
|
+
it("builds Bearer Authorization header for general tokens", () => {
|
|
101
|
+
const headers = buildAuthHeaders({
|
|
102
|
+
provider: "github",
|
|
103
|
+
token: "ghp_123456",
|
|
224
104
|
});
|
|
225
|
-
|
|
226
|
-
const result = await resolveGitCredentials(fakeDb);
|
|
227
|
-
expect(result.token).toBe("decrypted:enc-gh-tok");
|
|
228
|
-
expect(result.authorName).toBe("GH User");
|
|
105
|
+
expect(headers.Authorization).toBe("Bearer ghp_123456");
|
|
229
106
|
});
|
|
230
107
|
|
|
231
|
-
it("
|
|
232
|
-
|
|
233
|
-
git_accounts: [githubAccount, azureAccount],
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const result = await resolveGitCredentials(fakeDb);
|
|
237
|
-
expect(result.token).toBe("decrypted:enc-gh-tok");
|
|
108
|
+
it("returns empty object when no token or credentials provided", () => {
|
|
109
|
+
expect(buildAuthHeaders({ provider: "github" })).toEqual({});
|
|
238
110
|
});
|
|
111
|
+
});
|
|
239
112
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
label: "No Token",
|
|
247
|
-
password: "",
|
|
248
|
-
token: "",
|
|
249
|
-
username: "user",
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
setupConfig({
|
|
253
|
-
git_accounts: [noTokenAccount],
|
|
254
|
-
git_default_account: "acct-3",
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const result = await resolveGitCredentials(
|
|
258
|
-
fakeDb,
|
|
259
|
-
"https://example.com/repo.git",
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
expect(result.token).toBeUndefined();
|
|
263
|
-
expect(result.username).toBe("user");
|
|
264
|
-
expect(result.password).toBeUndefined();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("supports partial hostPattern matching (substring of hostname)", async () => {
|
|
268
|
-
const account: any = {
|
|
269
|
-
authorEmail: "",
|
|
270
|
-
authorName: "",
|
|
271
|
-
hostPattern: "gitlab.mycompany.com",
|
|
272
|
-
id: "acct-4",
|
|
273
|
-
label: "Custom GitLab",
|
|
274
|
-
password: "",
|
|
275
|
-
token: "enc-custom-tok",
|
|
276
|
-
username: "",
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
setupConfig({
|
|
280
|
-
git_accounts: [account],
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const result = await resolveGitCredentials(
|
|
284
|
-
fakeDb,
|
|
285
|
-
"https://gitlab.mycompany.com/group/project.git",
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
expect(result.token).toBe("decrypted:enc-custom-tok");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("supports selecting an explicit git account id", async () => {
|
|
292
|
-
const patAccount: any = {
|
|
293
|
-
authMode: "token",
|
|
294
|
-
authorEmail: "",
|
|
295
|
-
authorName: "",
|
|
296
|
-
hostPattern: "github.com",
|
|
297
|
-
id: "acct-pat",
|
|
298
|
-
label: "GitHub PAT",
|
|
299
|
-
password: "",
|
|
300
|
-
token: "enc-pat-tok",
|
|
301
|
-
username: "",
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const oauthAccount: any = {
|
|
305
|
-
authMode: "oauth",
|
|
306
|
-
authorEmail: "",
|
|
307
|
-
authorName: "",
|
|
308
|
-
hostPattern: "github.com",
|
|
309
|
-
id: "acct-oauth",
|
|
310
|
-
label: "GitHub OAuth",
|
|
311
|
-
password: "",
|
|
312
|
-
token: "enc-oauth-tok",
|
|
313
|
-
username: "",
|
|
314
|
-
};
|
|
113
|
+
describe("resolveGitCredentials", () => {
|
|
114
|
+
it("resolves from legacy single-key config when no accounts configured", async () => {
|
|
115
|
+
storedConfig["git_token"] = "enc:legacy-token";
|
|
116
|
+
storedConfig["git_username"] = "legacy-user";
|
|
117
|
+
storedConfig["git_author_name"] = "Legacy Author";
|
|
118
|
+
storedConfig["git_author_email"] = "legacy@example.com";
|
|
315
119
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
const result = await resolveGitCredentials(
|
|
321
|
-
fakeDb,
|
|
120
|
+
const creds = await resolveGitCredentials(
|
|
121
|
+
mockDb,
|
|
322
122
|
"https://github.com/org/repo.git",
|
|
323
|
-
{ accountId: "acct-oauth" },
|
|
324
123
|
);
|
|
325
|
-
|
|
326
|
-
expect(
|
|
327
|
-
expect(
|
|
328
|
-
expect(
|
|
124
|
+
expect(creds.token).toBe("legacy-token");
|
|
125
|
+
expect(creds.username).toBe("legacy-user");
|
|
126
|
+
expect(creds.authorName).toBe("Legacy Author");
|
|
127
|
+
expect(creds.authorEmail).toBe("legacy@example.com");
|
|
128
|
+
expect(creds.provider).toBe("generic");
|
|
329
129
|
});
|
|
330
130
|
|
|
331
|
-
it("
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
authorEmail: "",
|
|
347
|
-
authorName: "",
|
|
348
|
-
hostPattern: "github.com",
|
|
349
|
-
id: "acct-oauth",
|
|
350
|
-
label: "GitHub OAuth",
|
|
351
|
-
password: "",
|
|
352
|
-
token: "enc-oauth-tok",
|
|
353
|
-
username: "",
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
setupConfig({
|
|
357
|
-
git_accounts: [patAccount, oauthAccount],
|
|
358
|
-
});
|
|
131
|
+
it("resolves and decrypts credentials for matching host URL with longest pattern match", async () => {
|
|
132
|
+
storedConfig["git_accounts"] = [
|
|
133
|
+
{
|
|
134
|
+
id: "general-azure",
|
|
135
|
+
hostPattern: "dev.azure.com",
|
|
136
|
+
provider: "azure-devops",
|
|
137
|
+
token: "enc:general-token",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "specific-azure",
|
|
141
|
+
hostPattern: "dev.azure.com/specific-org",
|
|
142
|
+
provider: "azure-devops",
|
|
143
|
+
token: "enc:specific-token",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
359
146
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
"https://
|
|
363
|
-
{ authMode: "oauth" },
|
|
147
|
+
const creds = await resolveGitCredentials(
|
|
148
|
+
mockDb,
|
|
149
|
+
"https://dev.azure.com/specific-org/project/_git/repo",
|
|
364
150
|
);
|
|
365
151
|
|
|
366
|
-
expect(
|
|
367
|
-
expect(
|
|
368
|
-
expect(
|
|
152
|
+
expect(creds.accountId).toBe("specific-azure");
|
|
153
|
+
expect(creds.token).toBe("specific-token");
|
|
154
|
+
expect(creds.provider).toBe("azure-devops");
|
|
369
155
|
});
|
|
370
156
|
|
|
371
|
-
it("
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
scopes: ["repo"],
|
|
385
|
-
token: "enc-old-access",
|
|
386
|
-
username: "",
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
setupConfig({
|
|
390
|
-
git_accounts: [oauthAccount],
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
(global as any).fetch = (jest.fn() as any).mockResolvedValue({
|
|
394
|
-
json: async () => ({
|
|
395
|
-
accessToken: "new-access-token",
|
|
396
|
-
expiresIn: 3600,
|
|
397
|
-
refreshToken: "new-refresh-token",
|
|
398
|
-
scope: "repo read:user",
|
|
399
|
-
tokenType: "Bearer",
|
|
400
|
-
}),
|
|
401
|
-
ok: true,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
const result = await resolveGitCredentials(
|
|
405
|
-
fakeDb,
|
|
406
|
-
"https://github.com/org/repo.git",
|
|
407
|
-
{ authMode: "oauth" },
|
|
408
|
-
);
|
|
157
|
+
it("resolves specific account when accountId option is passed", async () => {
|
|
158
|
+
storedConfig["git_accounts"] = [
|
|
159
|
+
{
|
|
160
|
+
id: "acct-1",
|
|
161
|
+
hostPattern: "github.com",
|
|
162
|
+
token: "enc:tok-1",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "acct-2",
|
|
166
|
+
hostPattern: "github.com",
|
|
167
|
+
token: "enc:tok-2",
|
|
168
|
+
},
|
|
169
|
+
];
|
|
409
170
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
expect.objectContaining({
|
|
417
|
-
id: "oauth-1",
|
|
418
|
-
refreshToken: "encrypted:new-refresh-token",
|
|
419
|
-
token: "encrypted:new-access-token",
|
|
420
|
-
}),
|
|
421
|
-
]),
|
|
171
|
+
const creds = await resolveGitCredentials(
|
|
172
|
+
mockDb,
|
|
173
|
+
"https://github.com/foo/bar",
|
|
174
|
+
{
|
|
175
|
+
accountId: "acct-2",
|
|
176
|
+
},
|
|
422
177
|
);
|
|
178
|
+
expect(creds.accountId).toBe("acct-2");
|
|
179
|
+
expect(creds.token).toBe("tok-2");
|
|
423
180
|
});
|
|
424
181
|
|
|
425
|
-
it("
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
oauthProviderId: "github",
|
|
436
|
-
password: "",
|
|
437
|
-
refreshToken: "enc-refresh",
|
|
438
|
-
scopes: ["repo"],
|
|
439
|
-
token: "enc-old-access",
|
|
440
|
-
username: "",
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
setupConfig({
|
|
444
|
-
git_accounts: [oauthAccount],
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
(global as any).fetch = (jest.fn() as any).mockResolvedValue({
|
|
448
|
-
json: async () => ({
|
|
449
|
-
accessToken: "forced-access-token",
|
|
450
|
-
expiresIn: 1800,
|
|
451
|
-
}),
|
|
452
|
-
ok: true,
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
const result = await resolveGitCredentials(
|
|
456
|
-
fakeDb,
|
|
457
|
-
"https://github.com/org/repo.git",
|
|
458
|
-
{ authMode: "oauth", forceRefresh: true },
|
|
459
|
-
);
|
|
182
|
+
it("resolves default account when URL is omitted or matches default host", async () => {
|
|
183
|
+
storedConfig["git_default_account"] = "default-acct";
|
|
184
|
+
storedConfig["git_accounts"] = [
|
|
185
|
+
{
|
|
186
|
+
id: "default-acct",
|
|
187
|
+
hostPattern: "github.com",
|
|
188
|
+
provider: "github",
|
|
189
|
+
token: "enc:default-tok",
|
|
190
|
+
},
|
|
191
|
+
];
|
|
460
192
|
|
|
461
|
-
|
|
462
|
-
expect(
|
|
463
|
-
|
|
464
|
-
expect.objectContaining({ method: "POST" }),
|
|
465
|
-
);
|
|
193
|
+
const creds = await resolveGitCredentials(mockDb);
|
|
194
|
+
expect(creds.accountId).toBe("default-acct");
|
|
195
|
+
expect(creds.token).toBe("default-tok");
|
|
466
196
|
});
|
|
467
197
|
|
|
468
|
-
it("
|
|
469
|
-
|
|
470
|
-
accessTokenExpiresAt: Date.now() - 1_000,
|
|
471
|
-
authMode: "oauth",
|
|
472
|
-
authorEmail: "",
|
|
473
|
-
authorName: "",
|
|
474
|
-
hostPattern: "github.com",
|
|
475
|
-
id: "oauth-3",
|
|
476
|
-
label: "GitHub OAuth",
|
|
477
|
-
oauthClientId: "client-123",
|
|
478
|
-
oauthProviderId: "github",
|
|
479
|
-
password: "",
|
|
480
|
-
refreshToken: "enc-refresh",
|
|
481
|
-
token: "enc-old-access",
|
|
482
|
-
username: "",
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
setupConfig({
|
|
486
|
-
git_accounts: [oauthAccount],
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
(global as any).fetch = (jest.fn() as any).mockResolvedValue({
|
|
490
|
-
json: async () => ({ error: "invalid_grant" }),
|
|
491
|
-
ok: false,
|
|
492
|
-
status: 401,
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
await resolveGitCredentials(fakeDb, "https://github.com/org/repo.git", {
|
|
496
|
-
authMode: "oauth",
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
await resolveGitCredentials(fakeDb, "https://github.com/org/repo.git", {
|
|
500
|
-
authMode: "oauth",
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
const third = await resolveGitCredentials(
|
|
504
|
-
fakeDb,
|
|
505
|
-
"https://github.com/org/repo.git",
|
|
198
|
+
it("handles oauth reauth required state", async () => {
|
|
199
|
+
storedConfig["git_accounts"] = [
|
|
506
200
|
{
|
|
201
|
+
id: "oauth-acct",
|
|
202
|
+
hostPattern: "github.com",
|
|
203
|
+
provider: "github",
|
|
507
204
|
authMode: "oauth",
|
|
205
|
+
oauthReauthRequired: true,
|
|
206
|
+
authorEmail: "oauth@example.com",
|
|
508
207
|
},
|
|
509
|
-
|
|
208
|
+
];
|
|
510
209
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
"git_accounts",
|
|
515
|
-
expect.arrayContaining([
|
|
516
|
-
expect.objectContaining({
|
|
517
|
-
id: "oauth-3",
|
|
518
|
-
oauthReauthRequired: true,
|
|
519
|
-
oauthRefreshFailureCount: 3,
|
|
520
|
-
}),
|
|
521
|
-
]),
|
|
210
|
+
const creds = await resolveGitCredentials(
|
|
211
|
+
mockDb,
|
|
212
|
+
"https://github.com/foo/bar",
|
|
522
213
|
);
|
|
214
|
+
expect(creds.reauthRequired).toBe(true);
|
|
215
|
+
expect(creds.authMode).toBe("oauth");
|
|
216
|
+
expect(creds.authorEmail).toBe("oauth@example.com");
|
|
523
217
|
});
|
|
524
218
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
password: "",
|
|
534
|
-
authorName: "ExampleOrg2 Dev",
|
|
535
|
-
authorEmail: "dev@exampleOrg2.example.com",
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const exampleOrg1Account: any = {
|
|
539
|
-
id: "acct-exampleOrg1",
|
|
540
|
-
label: "ADO - ExampleOrg1",
|
|
541
|
-
hostPattern: "dev.azure.com/exampleOrg1",
|
|
542
|
-
token: "enc-exampleOrg1-tok",
|
|
543
|
-
username: "",
|
|
544
|
-
password: "",
|
|
545
|
-
authorName: "ExampleOrg1 Dev",
|
|
546
|
-
authorEmail: "dev@exampleOrg1.example.com",
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
it("matches account with host+path pattern against URL", async () => {
|
|
550
|
-
setupConfig({
|
|
551
|
-
git_accounts: [exampleOrg2Account, exampleOrg1Account],
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const result = await resolveGitCredentials(
|
|
555
|
-
fakeDb,
|
|
556
|
-
"https://dev.azure.com/exampleOrg2/project/_git/repo",
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
expect(result.token).toBe("decrypted:enc-exampleOrg2-tok");
|
|
560
|
-
expect(result.authorName).toBe("ExampleOrg2 Dev");
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it("matches second account with different org path", async () => {
|
|
564
|
-
setupConfig({
|
|
565
|
-
git_accounts: [exampleOrg2Account, exampleOrg1Account],
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const result = await resolveGitCredentials(
|
|
569
|
-
fakeDb,
|
|
570
|
-
"https://dev.azure.com/exampleOrg1/project/_git/repo",
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
expect(result.token).toBe("decrypted:enc-exampleOrg1-tok");
|
|
574
|
-
expect(result.authorName).toBe("ExampleOrg1 Dev");
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it("prefers more specific path pattern over domain-only pattern", async () => {
|
|
578
|
-
const domainOnlyAccount: any = {
|
|
579
|
-
id: "acct-az-generic",
|
|
580
|
-
label: "ADO (generic)",
|
|
581
|
-
hostPattern: "dev.azure.com",
|
|
582
|
-
token: "enc-generic-tok",
|
|
583
|
-
username: "",
|
|
584
|
-
password: "",
|
|
585
|
-
authorName: "",
|
|
586
|
-
authorEmail: "",
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
setupConfig({
|
|
590
|
-
git_accounts: [domainOnlyAccount, exampleOrg2Account],
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
const result = await resolveGitCredentials(
|
|
594
|
-
fakeDb,
|
|
595
|
-
"https://dev.azure.com/exampleOrg2/project/_git/repo",
|
|
596
|
-
);
|
|
597
|
-
|
|
598
|
-
// Should prefer the more specific path-based match
|
|
599
|
-
expect(result.token).toBe("decrypted:enc-exampleOrg2-tok");
|
|
600
|
-
expect(result.authorName).toBe("ExampleOrg2 Dev");
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it("falls back to domain-only pattern when path does not match", async () => {
|
|
604
|
-
const domainOnlyAccount: any = {
|
|
605
|
-
id: "acct-az-generic",
|
|
606
|
-
label: "ADO (generic)",
|
|
607
|
-
hostPattern: "dev.azure.com",
|
|
608
|
-
token: "enc-generic-tok",
|
|
609
|
-
username: "",
|
|
610
|
-
password: "",
|
|
611
|
-
authorName: "",
|
|
612
|
-
authorEmail: "",
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
setupConfig({
|
|
616
|
-
git_accounts: [exampleOrg2Account, domainOnlyAccount],
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
const result = await resolveGitCredentials(
|
|
620
|
-
fakeDb,
|
|
621
|
-
"https://dev.azure.com/other-org/project/_git/repo",
|
|
622
|
-
);
|
|
623
|
-
expect(result.token).toBe("decrypted:enc-generic-tok");
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
it("detects azure-devops provider from path-based host pattern", () => {
|
|
627
|
-
expect(detectProvider("dev.azure.com/exampleOrg2")).toBe(
|
|
628
|
-
"azure-devops",
|
|
629
|
-
);
|
|
630
|
-
expect(detectProvider("dev.azure.com/exampleOrg1")).toBe(
|
|
631
|
-
"azure-devops",
|
|
632
|
-
);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it("builds correct auth headers from path-based host pattern", () => {
|
|
636
|
-
const headers = buildAuthHeaders({
|
|
637
|
-
token: "pat123",
|
|
638
|
-
hostPattern: "dev.azure.com/exampleOrg2",
|
|
639
|
-
});
|
|
640
|
-
const expected = btoa(":pat123");
|
|
641
|
-
expect(headers).toEqual({
|
|
642
|
-
Authorization: `Basic ${expected}`,
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
// ── Priority: accounts over legacy ─────────────────────────────
|
|
649
|
-
describe("priority", () => {
|
|
650
|
-
it("uses accounts over legacy when both exist", async () => {
|
|
651
|
-
const account: any = {
|
|
652
|
-
id: "acct-1",
|
|
653
|
-
label: "GitHub",
|
|
654
|
-
hostPattern: "github.com",
|
|
655
|
-
token: "enc-new-tok",
|
|
656
|
-
username: "",
|
|
657
|
-
password: "",
|
|
658
|
-
authorName: "",
|
|
659
|
-
authorEmail: "",
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
setupConfig({
|
|
663
|
-
git_accounts: [account],
|
|
664
|
-
git_default_account: "acct-1",
|
|
665
|
-
git_token: "enc-old-tok",
|
|
666
|
-
git_username: "old-user",
|
|
667
|
-
});
|
|
219
|
+
it("returns empty default object when accounts exist but none match url", async () => {
|
|
220
|
+
storedConfig["git_accounts"] = [
|
|
221
|
+
{
|
|
222
|
+
id: "acct-github",
|
|
223
|
+
hostPattern: "github.com",
|
|
224
|
+
token: "enc:gh-tok",
|
|
225
|
+
},
|
|
226
|
+
];
|
|
668
227
|
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
"https://
|
|
228
|
+
const creds = await resolveGitCredentials(
|
|
229
|
+
mockDb,
|
|
230
|
+
"https://gitlab.com/other/repo",
|
|
672
231
|
);
|
|
673
|
-
expect(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// ── detectProvider ─────────────────────────────────────────────
|
|
679
|
-
describe("detectProvider", () => {
|
|
680
|
-
it("detects github from hostPattern", () => {
|
|
681
|
-
expect(detectProvider("github.com")).toBe("github");
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
it("detects github enterprise", () => {
|
|
685
|
-
expect(detectProvider("github.mycompany.com")).toBe("github");
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
it("detects azure-devops from dev.azure.com", () => {
|
|
689
|
-
expect(detectProvider("dev.azure.com")).toBe("azure-devops");
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it("detects azure-devops from visualstudio.com", () => {
|
|
693
|
-
expect(detectProvider("myorg.visualstudio.com")).toBe("azure-devops");
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
it("detects gitlab from gitlab.com", () => {
|
|
697
|
-
expect(detectProvider("gitlab.com")).toBe("gitlab");
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
it("detects gitlab from self-hosted", () => {
|
|
701
|
-
expect(detectProvider("gitlab.mycompany.com")).toBe("gitlab");
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
it("returns generic for unknown hosts", () => {
|
|
705
|
-
expect(detectProvider("bitbucket.org")).toBe("generic");
|
|
706
|
-
expect(detectProvider("example.com")).toBe("generic");
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
it("returns generic for empty string", () => {
|
|
710
|
-
expect(detectProvider("")).toBe("generic");
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
// ── buildAuthHeaders ───────────────────────────────────────────
|
|
715
|
-
describe("buildAuthHeaders", () => {
|
|
716
|
-
it("returns token header for github provider", () => {
|
|
717
|
-
const headers = buildAuthHeaders({
|
|
718
|
-
token: "fake",
|
|
719
|
-
provider: "github",
|
|
720
|
-
});
|
|
721
|
-
expect(headers).toEqual({
|
|
722
|
-
Authorization: "token fake",
|
|
723
|
-
});
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it("returns Basic header for azure-devops with token", () => {
|
|
727
|
-
const headers = buildAuthHeaders({
|
|
728
|
-
token: "azure-fake",
|
|
729
|
-
provider: "azure-devops",
|
|
730
|
-
});
|
|
731
|
-
// ADO: Basic base64(:PAT) — empty username, PAT as password
|
|
732
|
-
const expected = btoa(":azure-fake");
|
|
733
|
-
expect(headers).toEqual({
|
|
734
|
-
Authorization: `Basic ${expected}`,
|
|
735
|
-
});
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
it("returns Basic header for azure-devops with username+password", () => {
|
|
739
|
-
const headers = buildAuthHeaders({
|
|
740
|
-
username: "azuser",
|
|
741
|
-
password: "azpass",
|
|
742
|
-
provider: "azure-devops",
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
const expected = btoa("azuser:azpass");
|
|
746
|
-
expect(headers).toEqual({
|
|
747
|
-
Authorization: `Basic ${expected}`,
|
|
748
|
-
});
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it("prefers token over username+password for azure-devops", () => {
|
|
752
|
-
const headers = buildAuthHeaders({
|
|
753
|
-
token: "azure-fake",
|
|
754
|
-
username: "azuser",
|
|
755
|
-
password: "azpass",
|
|
756
|
-
provider: "azure-devops",
|
|
232
|
+
expect(creds.accountId).toBeUndefined();
|
|
233
|
+
expect(creds.token).toBeUndefined();
|
|
234
|
+
expect(creds.provider).toBe("generic");
|
|
757
235
|
});
|
|
758
|
-
|
|
759
|
-
const expected = btoa(":azure-fake");
|
|
760
|
-
expect(headers).toEqual({
|
|
761
|
-
Authorization: `Basic ${expected}`,
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
it("returns Bearer header for gitlab provider", () => {
|
|
766
|
-
const headers = buildAuthHeaders({
|
|
767
|
-
token: "glpat-fake",
|
|
768
|
-
provider: "gitlab",
|
|
769
|
-
});
|
|
770
|
-
expect(headers).toEqual({
|
|
771
|
-
Authorization: "Bearer glpat-fake",
|
|
772
|
-
});
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
it("returns Bearer header for generic provider", () => {
|
|
776
|
-
const headers = buildAuthHeaders({
|
|
777
|
-
token: "some-token",
|
|
778
|
-
provider: "generic",
|
|
779
|
-
});
|
|
780
|
-
expect(headers).toEqual({
|
|
781
|
-
Authorization: "Bearer some-token",
|
|
782
|
-
});
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
it("returns empty object when no credentials", () => {
|
|
786
|
-
const headers = buildAuthHeaders({ provider: "github" });
|
|
787
|
-
expect(headers).toEqual({} as any);
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
it("returns empty object when token is undefined", () => {
|
|
791
|
-
const headers = buildAuthHeaders({
|
|
792
|
-
token: undefined,
|
|
793
|
-
provider: "github",
|
|
794
|
-
});
|
|
795
|
-
expect(headers).toEqual({} as any);
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
it("infers provider from hostPattern when provider not set", () => {
|
|
799
|
-
const headers = buildAuthHeaders({
|
|
800
|
-
token: "pat123",
|
|
801
|
-
hostPattern: "dev.azure.com",
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
const expected = btoa(":pat123");
|
|
805
|
-
expect(headers).toEqual({
|
|
806
|
-
Authorization: `Basic ${expected}`,
|
|
807
|
-
});
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
it("explicit provider overrides hostPattern inference", () => {
|
|
811
|
-
const headers = buildAuthHeaders({
|
|
812
|
-
token: "pat123",
|
|
813
|
-
provider: "github",
|
|
814
|
-
hostPattern: "dev.azure.com",
|
|
815
|
-
});
|
|
816
|
-
expect(headers).toEqual({
|
|
817
|
-
Authorization: "token pat123",
|
|
818
|
-
});
|
|
819
|
-
});
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
// ── resolveGitCredentials returns provider ─────────────────────
|
|
823
|
-
describe("resolveGitCredentials — provider field", () => {
|
|
824
|
-
beforeEach(() => {
|
|
825
|
-
mockGetConfig.mockReset();
|
|
826
|
-
(mockGetConfig as any).mockResolvedValue(undefined);
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
it("returns provider from account hostPattern", async () => {
|
|
830
|
-
const azureAccount: any = {
|
|
831
|
-
id: "acct-az",
|
|
832
|
-
label: "ADO",
|
|
833
|
-
hostPattern: "dev.azure.com",
|
|
834
|
-
token: "enc-tok",
|
|
835
|
-
username: "",
|
|
836
|
-
password: "",
|
|
837
|
-
authorName: "",
|
|
838
|
-
authorEmail: "",
|
|
839
|
-
};
|
|
840
|
-
setupConfig({
|
|
841
|
-
git_accounts: [azureAccount],
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
const result = await resolveGitCredentials(
|
|
845
|
-
fakeDb,
|
|
846
|
-
"https://dev.azure.com/org/project/_git/repo",
|
|
847
|
-
);
|
|
848
|
-
expect(result.provider).toBe("azure-devops");
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
it("returns explicit provider from account when set", async () => {
|
|
852
|
-
const customAccount: any = {
|
|
853
|
-
id: "acct-custom",
|
|
854
|
-
label: "Custom",
|
|
855
|
-
hostPattern: "example.com",
|
|
856
|
-
provider: "gitlab",
|
|
857
|
-
token: "enc-tok",
|
|
858
|
-
username: "",
|
|
859
|
-
password: "",
|
|
860
|
-
authorName: "",
|
|
861
|
-
authorEmail: "",
|
|
862
|
-
};
|
|
863
|
-
setupConfig({
|
|
864
|
-
git_accounts: [customAccount],
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
const result = await resolveGitCredentials(
|
|
868
|
-
fakeDb,
|
|
869
|
-
"https://example.com/repo.git",
|
|
870
|
-
);
|
|
871
|
-
expect(result.provider).toBe("gitlab");
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
it("returns generic provider for legacy credentials", async () => {
|
|
875
|
-
setupConfig({ git_token: "enc-tok" });
|
|
876
|
-
const result = await resolveGitCredentials(fakeDb);
|
|
877
|
-
expect(result.provider).toBe("generic");
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
it("returns hostPattern in resolved credentials", async () => {
|
|
881
|
-
const ghAccount: any = {
|
|
882
|
-
id: "acct-gh",
|
|
883
|
-
label: "GitHub",
|
|
884
|
-
hostPattern: "github.com",
|
|
885
|
-
token: "enc-tok",
|
|
886
|
-
username: "",
|
|
887
|
-
password: "",
|
|
888
|
-
authorName: "",
|
|
889
|
-
authorEmail: "",
|
|
890
|
-
};
|
|
891
|
-
setupConfig({
|
|
892
|
-
git_accounts: [ghAccount],
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
const result = await resolveGitCredentials(
|
|
896
|
-
fakeDb,
|
|
897
|
-
"https://github.com/org/repo",
|
|
898
|
-
);
|
|
899
|
-
expect(result.hostPattern).toBe("github.com");
|
|
900
|
-
expect(result.provider).toBe("github");
|
|
901
236
|
});
|
|
902
237
|
});
|