@posthog/wizard 2.79.1 → 2.80.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/dist/{PickerMenu-dG5DQCZA.js → PickerMenu-m4CCPX4f.js} +10 -10
- package/dist/PickerMenu-m4CCPX4f.js.map +1 -0
- package/dist/{agent-BWFhuLgo.js → agent-BkBbVuNq.js} +48 -37
- package/dist/agent-BkBbVuNq.js.map +1 -0
- package/dist/agent-QGK2YzlX.js +6 -0
- package/dist/{agent-interface-FkbaBJwz.js → agent-interface-CzyCmp7q.js} +68 -31
- package/dist/agent-interface-CzyCmp7q.js.map +1 -0
- package/dist/{agent-prompt-loader-D0weQ-7k.js → agent-prompt-loader-4wMxlYSX.js} +5 -5
- package/dist/{agent-prompt-loader-D0weQ-7k.js.map → agent-prompt-loader-4wMxlYSX.js.map} +1 -1
- package/dist/{analytics-DXK1gc12.js → analytics-B6q5PN7a.js} +5 -5
- package/dist/analytics-B6q5PN7a.js.map +1 -0
- package/dist/analytics-BXtfK9o7.js +2 -0
- package/dist/{api-CrbCIQOz.js → api-C3Dc4LFl.js} +3 -3
- package/dist/{api-CrbCIQOz.js.map → api-C3Dc4LFl.js.map} +1 -1
- package/dist/api-yNjrfZs2.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/{bounded-fs-vAq-T-K7.js → bounded-fs-CC_NDtoP.js} +3 -3
- package/dist/{bounded-fs-vAq-T-K7.js.map → bounded-fs-CC_NDtoP.js.map} +1 -1
- package/dist/check-screens.tsx +5 -5
- package/dist/{ci-install-COcV8SY2.js → ci-install-DfcAMhlu.js} +7 -7
- package/dist/ci-install-DfcAMhlu.js.map +1 -0
- package/dist/{constants-DHCBSfnx.js → constants-DO4X70po.js} +3 -3
- package/dist/{constants-DHCBSfnx.js.map → constants-DO4X70po.js.map} +1 -1
- package/dist/{debug-BuYHGr6_.js → debug-BueK5mRR.js} +1 -1
- package/dist/{debug-kAHwWGg7.js → debug-DyARf5TF.js} +3 -3
- package/dist/debug-DyARf5TF.js.map +1 -0
- package/dist/{defaults-a9TelDhJ.js → defaults-DpfMnH2q.js} +2 -2
- package/dist/defaults-DpfMnH2q.js.map +1 -0
- package/dist/{endpoints-BVMYm9F4.js → endpoints-D-4KxZiB.js} +3 -3
- package/dist/{endpoints-BVMYm9F4.js.map → endpoints-D-4KxZiB.js.map} +1 -1
- package/dist/env-api-key-CHGLe0iz.js +2 -0
- package/dist/{env-api-key-BP-NFFXt.js → env-api-key-COf0U-Au.js} +2 -2
- package/dist/{env-api-key-BP-NFFXt.js.map → env-api-key-COf0U-Au.js.map} +1 -1
- package/dist/{environment-BATlQxcQ.js → environment-DrLaSsA8.js} +4 -4
- package/dist/{environment-BATlQxcQ.js.map → environment-DrLaSsA8.js.map} +1 -1
- package/dist/environment-hBj7ngmO.js +2 -0
- package/dist/{file-DcRKycV8.js → file-CVA9ocIC.js} +3 -3
- package/dist/file-CVA9ocIC.js.map +1 -0
- package/dist/file-CmoKK_Pp.js +2 -0
- package/dist/{file-watcher-Brq2nnHV.js → file-watcher-C3Su8vXm.js} +3 -3
- package/dist/file-watcher-C3Su8vXm.js.map +1 -0
- package/dist/{gateway-C9-GlLO9.js → gateway-BmjYUSua.js} +6 -6
- package/dist/{gateway-C9-GlLO9.js.map → gateway-BmjYUSua.js.map} +1 -1
- package/dist/{headless-ui-CqI2gXlD.js → headless-ui-CUbv12Yx.js} +3 -3
- package/dist/headless-ui-CUbv12Yx.js.map +1 -0
- package/dist/host-resolution-DVSb7kXb.js +114 -0
- package/dist/host-resolution-DVSb7kXb.js.map +1 -0
- package/dist/{add-mcp-server-to-clients-DIaTRWMY.js → install-Bk5TNk27.js} +15 -14
- package/dist/install-Bk5TNk27.js.map +1 -0
- package/dist/{add-mcp-server-to-clients-artmN7mz.js → install-F4wm3RSl.js} +1 -1
- package/dist/interactive-C5LNvDj6.js +11 -0
- package/dist/interactive-C5LNvDj6.js.map +1 -0
- package/dist/{telemetry-B7zJdRmA.js → links-9LWZPSCq.js} +4 -13
- package/dist/links-9LWZPSCq.js.map +1 -0
- package/dist/{logging-ui-Dza4D0MK.js → logging-ui-p6XKVmTx.js} +6 -6
- package/dist/logging-ui-p6XKVmTx.js.map +1 -0
- package/dist/{main-CgKxbwMP.js → main-DKPjFHEV.js} +149 -141
- package/dist/main-DKPjFHEV.js.map +1 -0
- package/dist/{mcp-DnAyW0-w.js → mcp-Co8VRF62.js} +4 -4
- package/dist/{mcp-DnAyW0-w.js.map → mcp-Co8VRF62.js.map} +1 -1
- package/dist/mcp-install-smoke-test.ts +1 -1
- package/dist/{mcp-prompt-streaming-DDWQ-FCA.js → mcp-prompt-streaming-ljCVoQ9I.js} +6 -6
- package/dist/{mcp-prompt-streaming-DDWQ-FCA.js.map → mcp-prompt-streaming-ljCVoQ9I.js.map} +1 -1
- package/dist/{mint-failure-u8xQKNOn.js → mint-failure-C0HyvEwO.js} +103 -92
- package/dist/mint-failure-C0HyvEwO.js.map +1 -0
- package/dist/{models-WcwUnSKb.js → models-Dba8ITeo.js} +2 -2
- package/dist/{models-WcwUnSKb.js.map → models-Dba8ITeo.js.map} +1 -1
- package/dist/{non-interactive-DyVC1AGc.js → non-interactive-CLMJrj_7.js} +3 -3
- package/dist/non-interactive-CLMJrj_7.js.map +1 -0
- package/dist/{oauth-session-DYM2Hrl2.js → oauth-session-D-RfcVmr.js} +2 -2
- package/dist/{oauth-session-DYM2Hrl2.js.map → oauth-session-D-RfcVmr.js.map} +1 -1
- package/dist/{orchestrator-tools-DbgNBFGA.js → orchestrator-tools-CVJ1F9Ub.js} +3 -3
- package/dist/{orchestrator-tools-DbgNBFGA.js.map → orchestrator-tools-CVJ1F9Ub.js.map} +1 -1
- package/dist/package-json--ppioh2F.js +2 -0
- package/dist/{package-json-Atg34fa4.js → package-json-Cowya9-3.js} +1 -1
- package/dist/{package-json-Atg34fa4.js.map → package-json-Cowya9-3.js.map} +1 -1
- package/dist/{pi-8KsYsPmM.js → pi-B5YTBRDv.js} +18 -17
- package/dist/pi-B5YTBRDv.js.map +1 -0
- package/dist/{playground-BlYRBMj7.js → playground-BLsCfHmQ.js} +33 -33
- package/dist/playground-BLsCfHmQ.js.map +1 -0
- package/dist/posthog-B2E0hpZt.js +2 -0
- package/dist/{posthog-Bt0Aplb8.js → posthog-B42rt_1S.js} +3 -3
- package/dist/posthog-B42rt_1S.js.map +1 -0
- package/dist/project-data--L-WZyvj.js +2 -0
- package/dist/{setup-utils-B26Lf2yF.js → project-data-BO9IXn9y.js} +566 -947
- package/dist/project-data-BO9IXn9y.js.map +1 -0
- package/dist/provisioning-C01REIU2.js +2 -0
- package/dist/{provisioning-Bo2NRxFz.js → provisioning-CLhuKqXH.js} +6 -67
- package/dist/provisioning-CLhuKqXH.js.map +1 -0
- package/dist/{queue-tools-C8mXOh7e.js → queue-tools-knnPQP0e.js} +4 -4
- package/dist/{queue-tools-C8mXOh7e.js.map → queue-tools-knnPQP0e.js.map} +1 -1
- package/dist/{registry-CD5RQt38.js → registry-B3rC5U4b.js} +6 -6
- package/dist/{registry-CD5RQt38.js.map → registry-B3rC5U4b.js.map} +1 -1
- package/dist/registry-BamsM7J9.js +2 -0
- package/dist/run-agent-legacy-CwUmPfQF.js +2 -0
- package/dist/{run-agent-legacy-DOlM6DQu.js → run-agent-legacy-DWBajsMh.js} +38 -111
- package/dist/run-agent-legacy-DWBajsMh.js.map +1 -0
- package/dist/{runners-ov3EXF1n.js → runners-D1PqqpjA.js} +38 -38
- package/dist/runners-D1PqqpjA.js.map +1 -0
- package/dist/{security-C71pkwqH.js → security-BvdSFDuM.js} +5 -5
- package/dist/{security-C71pkwqH.js.map → security-BvdSFDuM.js.map} +1 -1
- package/dist/setup-utils-ChXXTi1m.js +301 -0
- package/dist/setup-utils-ChXXTi1m.js.map +1 -0
- package/dist/{start-tui-Bo2L0bar.js → start-tui-DicLfgsj.js} +92 -90
- package/dist/start-tui-DicLfgsj.js.map +1 -0
- package/dist/{steps-BPcg3_3Q.js → steps-Hc28zVc_.js} +10 -10
- package/dist/steps-Hc28zVc_.js.map +1 -0
- package/dist/store-COCO6U0P.js +5 -0
- package/dist/{store-BTzcvRGT.js → store-D3509G87.js} +66 -28
- package/dist/store-D3509G87.js.map +1 -0
- package/dist/{subagent-D1TrTchE.js → subagent-D_Sqak94.js} +3 -3
- package/dist/{subagent-D1TrTchE.js.map → subagent-D_Sqak94.js.map} +1 -1
- package/dist/{task-DqRlQBfs.js → task-DiTI6Sak.js} +16 -16
- package/dist/task-DiTI6Sak.js.map +1 -0
- package/dist/{wizard-ui-DVqrYaOM.js → task-status-CmrrR7zo.js} +2 -2
- package/dist/task-status-CmrrR7zo.js.map +1 -0
- package/dist/{task-stream-Bw6JaLCX.js → task-stream-BckhG_cB.js} +10 -10
- package/dist/task-stream-BckhG_cB.js.map +1 -0
- package/dist/{tasks-ChWufsdO.js → tasks-ChZ_F1YP.js} +1 -1
- package/dist/{tasks-ChWufsdO.js.map → tasks-ChZ_F1YP.js.map} +1 -1
- package/dist/telemetry-BNKsDxNS.js +13 -0
- package/dist/telemetry-BNKsDxNS.js.map +1 -0
- package/dist/{terminal-zgkEfoCL.js → terminal-PK9bCLRi.js} +123 -97
- package/dist/terminal-PK9bCLRi.js.map +1 -0
- package/dist/{tools-CkJ2y9Wt.js → tools-C8LQ8QFz.js} +8 -8
- package/dist/{tools-CkJ2y9Wt.js.map → tools-C8LQ8QFz.js.map} +1 -1
- package/dist/{ui-Q6-RvERF.js → ui-tPNpM6vj.js} +3 -3
- package/dist/ui-tPNpM6vj.js.map +1 -0
- package/dist/urls-BI4kJ4qQ.js +67 -0
- package/dist/urls-BI4kJ4qQ.js.map +1 -0
- package/dist/{version-CHhJjz_H.js → version-5_ydHkM6.js} +2 -2
- package/dist/{version-CHhJjz_H.js.map → version-5_ydHkM6.js.map} +1 -1
- package/dist/{wizard-abort-Br1qMDNa.js → wizard-abort-BVjqLyeA.js} +1 -1
- package/dist/{wizard-abort-BPdv07np.js → wizard-abort-DBKtfnBR.js} +18 -16
- package/dist/wizard-abort-DBKtfnBR.js.map +1 -0
- package/dist/wizard-session-B7BHnay0.js +2 -0
- package/dist/{wizard-session-2w4Ru-Wz.js → wizard-session-BYw6LzpS.js} +30 -28
- package/dist/wizard-session-BYw6LzpS.js.map +1 -0
- package/dist/{yara-hooks-BnxejtXK.js → yara-hooks-C3L6lBij.js} +5 -5
- package/dist/{yara-hooks-BnxejtXK.js.map → yara-hooks-C3L6lBij.js.map} +1 -1
- package/dist/yara-hooks-CF_IsoaA.js +2 -0
- package/package.json +2 -3
- package/dist/PickerMenu-dG5DQCZA.js.map +0 -1
- package/dist/add-mcp-server-to-clients-DIaTRWMY.js.map +0 -1
- package/dist/agent-BEjARD2a.js +0 -6
- package/dist/agent-BWFhuLgo.js.map +0 -1
- package/dist/agent-interface-FkbaBJwz.js.map +0 -1
- package/dist/analytics-DFtbRjYh.js +0 -2
- package/dist/analytics-DXK1gc12.js.map +0 -1
- package/dist/api-Cm9UtKZh.js +0 -2
- package/dist/ci-install-COcV8SY2.js.map +0 -1
- package/dist/debug-kAHwWGg7.js.map +0 -1
- package/dist/defaults-a9TelDhJ.js.map +0 -1
- package/dist/env-api-key-CaWEDj_9.js +0 -2
- package/dist/environment-CwD8OPYj.js +0 -2
- package/dist/file-C5JMsyR1.js +0 -2
- package/dist/file-DcRKycV8.js.map +0 -1
- package/dist/file-watcher-Brq2nnHV.js.map +0 -1
- package/dist/headless-ui-CqI2gXlD.js.map +0 -1
- package/dist/interactive-B_Ti_CsS.js +0 -11
- package/dist/interactive-B_Ti_CsS.js.map +0 -1
- package/dist/logging-ui-Dza4D0MK.js.map +0 -1
- package/dist/main-CgKxbwMP.js.map +0 -1
- package/dist/mint-failure-u8xQKNOn.js.map +0 -1
- package/dist/non-interactive-DyVC1AGc.js.map +0 -1
- package/dist/package-json-D8Ga03tK.js +0 -2
- package/dist/pi-8KsYsPmM.js.map +0 -1
- package/dist/playground-BlYRBMj7.js.map +0 -1
- package/dist/posthog-Bt0Aplb8.js.map +0 -1
- package/dist/posthog-CUgCEhH0.js +0 -2
- package/dist/provisioning-Bo2NRxFz.js.map +0 -1
- package/dist/provisioning-OGFGlzf7.js +0 -2
- package/dist/registry-C06e_x-p.js +0 -2
- package/dist/run-agent-legacy-ChacZImU.js +0 -2
- package/dist/run-agent-legacy-DOlM6DQu.js.map +0 -1
- package/dist/runners-ov3EXF1n.js.map +0 -1
- package/dist/setup-utils-B26Lf2yF.js.map +0 -1
- package/dist/setup-utils-CVS86NYR.js +0 -2
- package/dist/start-tui-Bo2L0bar.js.map +0 -1
- package/dist/steps-BPcg3_3Q.js.map +0 -1
- package/dist/store-BTzcvRGT.js.map +0 -1
- package/dist/store-BpqySPXm.js +0 -5
- package/dist/task-DqRlQBfs.js.map +0 -1
- package/dist/task-stream-Bw6JaLCX.js.map +0 -1
- package/dist/telemetry-B7zJdRmA.js.map +0 -1
- package/dist/terminal-zgkEfoCL.js.map +0 -1
- package/dist/ui-Q6-RvERF.js.map +0 -1
- package/dist/wizard-abort-BPdv07np.js.map +0 -1
- package/dist/wizard-session-2w4Ru-Wz.js.map +0 -1
- package/dist/wizard-session-DGaUHOF3.js +0 -2
- package/dist/wizard-ui-DVqrYaOM.js.map +0 -1
- package/dist/yara-hooks-DSuExwb4.js +0 -2
|
@@ -1,602 +1,113 @@
|
|
|
1
|
-
import { s as logToFile } from "./debug-
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import "./
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { t as getUI } from "./ui-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import * as path$2 from "path";
|
|
15
|
-
import { basename, join as join$1 } from "node:path";
|
|
16
|
-
import * as childProcess from "node:child_process";
|
|
1
|
+
import { s as logToFile } from "./debug-DyARf5TF.js";
|
|
2
|
+
import { B as WIZARD_CONTACT_EMAIL, C as ISSUES_URL, E as OAUTH_TIMEOUT_MS, J as WIZARD_PROVISIONING_SCOPES, N as POSTHOG_PROXY_CLIENT_ID, O as POSTHOG_DEV_CLIENT_ID, P as POSTHOG_STATUS_PAGE_URL, Q as WIZARD_USER_AGENT, T as OAUTH_PORTS, W as WIZARD_OAUTH_SCOPES, d as DUMMY_PROJECT_API_KEY } from "./constants-DO4X70po.js";
|
|
3
|
+
import "./wizard-session-BYw6LzpS.js";
|
|
4
|
+
import { n as analytics } from "./analytics-B6q5PN7a.js";
|
|
5
|
+
import { r as withUtm, t as openTrackedLink } from "./links-9LWZPSCq.js";
|
|
6
|
+
import { t as withProgress } from "./telemetry-BNKsDxNS.js";
|
|
7
|
+
import { n as detectOrgAndProject, t as abort } from "./setup-utils-ChXXTi1m.js";
|
|
8
|
+
import { t as getUI } from "./ui-tPNpM6vj.js";
|
|
9
|
+
import { r as wizardAbort } from "./wizard-abort-DBKtfnBR.js";
|
|
10
|
+
import { a as getOAuthUrl, s as resolveBaseUrl } from "./urls-BI4kJ4qQ.js";
|
|
11
|
+
import { t as HostResolution } from "./host-resolution-DVSb7kXb.js";
|
|
12
|
+
import { n as provisionNewAccount, t as ProvisionedAccountUnreadableError } from "./provisioning-CLhuKqXH.js";
|
|
13
|
+
import { c as fetchUserData, o as fetchProjectData } from "./api-C3Dc4LFl.js";
|
|
17
14
|
import { execSync } from "node:child_process";
|
|
18
|
-
import * as fs$1 from "node:fs";
|
|
19
15
|
import axios from "axios";
|
|
20
16
|
import * as crypto from "node:crypto";
|
|
21
|
-
import * as http from "node:http";
|
|
22
17
|
import { z } from "zod";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return fs$2.existsSync(path$2.join(installDir, file));
|
|
26
|
-
}
|
|
27
|
-
function lockfileHeaderContains(installDir, file, needle) {
|
|
28
|
-
return readFileHead(path$2.join(installDir, file), 500)?.includes(needle) ?? false;
|
|
29
|
-
}
|
|
30
|
-
async function writeOverride(slot, pkgName, pkgVersion, { installDir }) {
|
|
31
|
-
const pkg = await getPackageDotJson({ installDir });
|
|
32
|
-
let next;
|
|
33
|
-
if (slot === "yarn") next = {
|
|
34
|
-
...pkg,
|
|
35
|
-
resolutions: {
|
|
36
|
-
...pkg.resolutions ?? {},
|
|
37
|
-
[pkgName]: pkgVersion
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
else if (slot === "pnpm") next = {
|
|
41
|
-
...pkg,
|
|
42
|
-
pnpm: {
|
|
43
|
-
...pkg.pnpm ?? {},
|
|
44
|
-
overrides: {
|
|
45
|
-
...pkg.pnpm?.overrides ?? {},
|
|
46
|
-
[pkgName]: pkgVersion
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
else next = {
|
|
51
|
-
...pkg,
|
|
52
|
-
overrides: {
|
|
53
|
-
...pkg.overrides ?? {},
|
|
54
|
-
[pkgName]: pkgVersion
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
await updatePackageDotJson(next, { installDir });
|
|
58
|
-
}
|
|
59
|
-
const packageManagers = [
|
|
60
|
-
{
|
|
61
|
-
name: "bun",
|
|
62
|
-
label: "Bun",
|
|
63
|
-
installCommand: "bun add",
|
|
64
|
-
buildCommand: "bun run build",
|
|
65
|
-
runScriptCommand: "bun run",
|
|
66
|
-
flags: "",
|
|
67
|
-
detect: ({ installDir }) => hasLockfile(installDir, "bun.lockb") || hasLockfile(installDir, "bun.lock"),
|
|
68
|
-
addOverride: (pkgName, pkgVersion, opts) => writeOverride("npm", pkgName, pkgVersion, opts)
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: "yarn",
|
|
72
|
-
label: "Yarn V1",
|
|
73
|
-
installCommand: "yarn add",
|
|
74
|
-
buildCommand: "yarn build",
|
|
75
|
-
runScriptCommand: "yarn",
|
|
76
|
-
flags: "--ignore-workspace-root-check",
|
|
77
|
-
detect: ({ installDir }) => lockfileHeaderContains(installDir, "yarn.lock", "yarn lockfile v1"),
|
|
78
|
-
addOverride: (pkgName, pkgVersion, opts) => writeOverride("yarn", pkgName, pkgVersion, opts)
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: "yarn",
|
|
82
|
-
label: "Yarn V2/3/4",
|
|
83
|
-
installCommand: "yarn add",
|
|
84
|
-
buildCommand: "yarn build",
|
|
85
|
-
runScriptCommand: "yarn",
|
|
86
|
-
flags: "",
|
|
87
|
-
detect: ({ installDir }) => lockfileHeaderContains(installDir, "yarn.lock", "__metadata"),
|
|
88
|
-
addOverride: (pkgName, pkgVersion, opts) => writeOverride("yarn", pkgName, pkgVersion, opts)
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: "pnpm",
|
|
92
|
-
label: "pnpm",
|
|
93
|
-
installCommand: "pnpm add",
|
|
94
|
-
buildCommand: "pnpm build",
|
|
95
|
-
runScriptCommand: "pnpm",
|
|
96
|
-
flags: "--ignore-workspace-root-check",
|
|
97
|
-
detect: ({ installDir }) => hasLockfile(installDir, "pnpm-lock.yaml"),
|
|
98
|
-
addOverride: (pkgName, pkgVersion, opts) => writeOverride("pnpm", pkgName, pkgVersion, opts)
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: "npm",
|
|
102
|
-
label: "npm",
|
|
103
|
-
installCommand: "npm add",
|
|
104
|
-
buildCommand: "npm run build",
|
|
105
|
-
runScriptCommand: "npm run",
|
|
106
|
-
flags: "",
|
|
107
|
-
detect: ({ installDir }) => hasLockfile(installDir, "package-lock.json"),
|
|
108
|
-
addOverride: (pkgName, pkgVersion, opts) => writeOverride("npm", pkgName, pkgVersion, opts)
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: "expo",
|
|
112
|
-
label: "Expo",
|
|
113
|
-
installCommand: "npx expo install",
|
|
114
|
-
buildCommand: "npx expo build",
|
|
115
|
-
runScriptCommand: "npx expo run",
|
|
116
|
-
flags: "",
|
|
117
|
-
detect: () => false,
|
|
118
|
-
addOverride: (pkgName, pkgVersion, opts) => writeOverride("npm", pkgName, pkgVersion, opts)
|
|
119
|
-
}
|
|
120
|
-
];
|
|
121
|
-
function detectAllPackageManagers({ installDir }) {
|
|
122
|
-
return withProgress("detect-package-manager", () => {
|
|
123
|
-
const matches = packageManagers.filter((pm) => pm.detect({ installDir }));
|
|
124
|
-
if (matches.length === 0) analytics.setTag("package-manager", "not-detected");
|
|
125
|
-
return matches;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
function serializeNodePM(pm) {
|
|
129
|
-
return {
|
|
130
|
-
name: pm.name,
|
|
131
|
-
label: pm.label,
|
|
132
|
-
installCommand: pm.installCommand,
|
|
133
|
-
runCommand: pm.runScriptCommand
|
|
134
|
-
};
|
|
135
|
-
}
|
|
18
|
+
import * as http from "node:http";
|
|
19
|
+
//#region src/shared/utils/oauth-errors.ts
|
|
136
20
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
21
|
+
* OAuth failure presentation — the structured error the flow throws and the
|
|
22
|
+
* user-facing messaging built from it.
|
|
23
|
+
*
|
|
24
|
+
* `oauth.ts` owns the flow machinery (callback server, token exchange, port
|
|
25
|
+
* retries); this module owns what a failure *means*: the RFC 6749 error
|
|
26
|
+
* fields (`error`, `error_description`, `error_uri`), per-code remediation
|
|
27
|
+
* copy, and safe rendering for the browser callback page. Everything here is
|
|
28
|
+
* pure so the copy can be unit-tested without the HTTP machinery.
|
|
139
29
|
*/
|
|
140
|
-
function detectNodePackageManagers(installDir) {
|
|
141
|
-
const detected = detectAllPackageManagers({ installDir }).map(serializeNodePM);
|
|
142
|
-
if (detected.length === 0) return Promise.resolve({
|
|
143
|
-
detected: [],
|
|
144
|
-
primary: null,
|
|
145
|
-
recommendation: "No lockfile found. Default to npm (npm add, npm run)."
|
|
146
|
-
});
|
|
147
|
-
const primary = detected[0];
|
|
148
|
-
return Promise.resolve({
|
|
149
|
-
detected,
|
|
150
|
-
primary,
|
|
151
|
-
recommendation: detected.length === 1 ? `Use ${primary.label} (${primary.installCommand}).` : `Multiple package managers detected. Prefer ${primary.label} (${primary.installCommand}).`
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
//#endregion
|
|
155
|
-
//#region src/programs/oauth/program-scopes.ts
|
|
156
30
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* list lives in the README under "OAuth app scope ceiling". The
|
|
162
|
-
* tutorial's prompts and follow-ups touch most of the read surface,
|
|
163
|
-
* plus annotation write for the "PostHog wizard install" verify-prompt.
|
|
31
|
+
* A structured OAuth failure (RFC 6749 §4.1.2.1 authorize errors, §5.2 token
|
|
32
|
+
* errors). Thrown by both the authorize callback and the token exchange so
|
|
33
|
+
* the flow's error handler keys remediation copy and analytics fingerprints
|
|
34
|
+
* off `code` instead of re-parsing `message`.
|
|
164
35
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* • query:read — HogQL
|
|
169
|
-
* • dashboard:write, insight:write, notebook:write — Phase-5 persist
|
|
36
|
+
* The message stays exactly `OAuth error: <code>` — the analytics fingerprint
|
|
37
|
+
* scheme (`wizard_oauth_<code>`) and the legacy message-prefix parsing both
|
|
38
|
+
* depend on that shape. The description/uri ride along as properties.
|
|
170
39
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
40
|
+
* NAMING: never bind an instance to a variable or property whose name
|
|
41
|
+
* contains "oauth" (use `flowError`, `callbackError`, ...). CodeQL's
|
|
42
|
+
* clear-text-logging heuristic classifies any `*oauth*`-named variable read
|
|
43
|
+
* as password data, and these errors are deliberately shown to the user, so
|
|
44
|
+
* such a binding flags every log statement downstream of it (CWE-312).
|
|
175
45
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"annotation:read",
|
|
191
|
-
"annotation:write",
|
|
192
|
-
"activity_log:read",
|
|
193
|
-
"property_definition:read",
|
|
194
|
-
"event_definition:read",
|
|
195
|
-
"action:read",
|
|
196
|
-
"warehouse_table:read",
|
|
197
|
-
"warehouse_view:read",
|
|
198
|
-
"alert:read",
|
|
199
|
-
"subscription:read",
|
|
200
|
-
"integration:read"
|
|
201
|
-
];
|
|
46
|
+
var OAuthError = class extends Error {
|
|
47
|
+
code;
|
|
48
|
+
/** The server's human-readable explanation (`error_description`), if sent. */
|
|
49
|
+
description;
|
|
50
|
+
/** The server's "more info" link (`error_uri`), if sent. */
|
|
51
|
+
uri;
|
|
52
|
+
constructor(code, description, uri) {
|
|
53
|
+
super(`OAuth error: ${code}`);
|
|
54
|
+
this.name = "OAuthError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.description = description;
|
|
57
|
+
this.uri = uri;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
202
60
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* strings requested — `:write` does not imply `:read` — so listing existing
|
|
208
|
-
* flags to avoid key collisions needs `feature_flag:read` explicitly.
|
|
209
|
-
* `property_definition:read` lets the agent discover person properties when
|
|
210
|
-
* building flag rollout filters instead of having to ask the user verbatim.
|
|
61
|
+
* Sanitize an OAuth error param before logging or rendering it: these arrive
|
|
62
|
+
* on a query string (or a token-endpoint body) that anything able to hit the
|
|
63
|
+
* local callback port can populate. Strips non-printable/non-ASCII characters
|
|
64
|
+
* and caps the length. Returns undefined when nothing printable remains.
|
|
211
65
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
66
|
+
function sanitizeOAuthParam(value, maxLength = 200) {
|
|
67
|
+
if (!value) return void 0;
|
|
68
|
+
return value.replace(/[^\x20-\x7e]/g, "").slice(0, maxLength).trim() || void 0;
|
|
69
|
+
}
|
|
70
|
+
/** Description sentences get more room than bare codes/URIs. */
|
|
71
|
+
const MAX_DESCRIPTION_LENGTH = 500;
|
|
72
|
+
function escapeHtml(value) {
|
|
73
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
74
|
+
}
|
|
75
|
+
const OAuthErrorBodySchema = z.object({
|
|
76
|
+
error: z.string().min(1),
|
|
77
|
+
error_description: z.string().optional(),
|
|
78
|
+
error_uri: z.string().optional()
|
|
79
|
+
});
|
|
217
80
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* agent drives during the run:
|
|
221
|
-
* • task:read / task:write — the signal source config API
|
|
222
|
-
* (`inbox-source-configs-*`) is permissioned under the generic
|
|
223
|
-
* `task` scope object, NOT a signals-specific one. Unrelated to
|
|
224
|
-
* the Tasks product.
|
|
225
|
-
* • integration:read — `integrations-list`, to check whether the
|
|
226
|
-
* team already has a GitHub integration and to verify the connect
|
|
227
|
-
* flow completed.
|
|
228
|
-
* • signal_scout:read / signal_scout:write — list, sync, and tune
|
|
229
|
-
* the Signals scout troop (`signals-scout-config-*`).
|
|
230
|
-
* • session_recording:read / survey:read / error_tracking:read —
|
|
231
|
-
* server-side product-usage probes (`query-session-recordings-list`,
|
|
232
|
-
* `survey-list`, `error-issue-list`). Product usage is a
|
|
233
|
-
* project-level fact (often instrumented in another repo or via
|
|
234
|
-
* the snippet), so the agent asks the server instead of inferring
|
|
235
|
-
* only from the local setup report. All three are read-only and
|
|
236
|
-
* already in the wizard OAuth app's production scope ceiling (the
|
|
237
|
-
* mcp-tutorial program requests them).
|
|
238
|
-
* • external_data_source:read / external_data_source:write — the
|
|
239
|
-
* connected-tools step creates the GitHub Issues / Linear warehouse
|
|
240
|
-
* sources directly (`external-data-sources-create`) and verifies
|
|
241
|
-
* what's actually connected (`external-data-sources-list`) instead
|
|
242
|
-
* of taking the user's word for it.
|
|
243
|
-
* • llm_skill:read / llm_skill:write — the custom-scouts step
|
|
244
|
-
* (skill step 6b): read the seeded `authoring-signals-scouts`
|
|
245
|
-
* guide and canonical scout bodies (`llma-skill-get` /
|
|
246
|
-
* `llma-skill-file-get`) and author the user-approved custom
|
|
247
|
-
* `signals-scout-*` skills (`llma-skill-create`). Canonical scout
|
|
248
|
-
* bodies are never edited.
|
|
249
|
-
* • product_enablement:write — the "Enable products" step turns on
|
|
250
|
-
* Session Replay / Error Tracking / Support so their sources have
|
|
251
|
-
* data to read (`products-enable`). A purpose-built scope: the
|
|
252
|
-
* server owns each enable recipe, so this can flip the product
|
|
253
|
-
* toggles without the far broader `project:write`.
|
|
254
|
-
* • replay_scanner:read / replay_scanner:write — the Replay Vision
|
|
255
|
-
* scanners step (skill step 6c) lists the team's existing scanners
|
|
256
|
-
* and creates the `emits_signals` ones whose findings land in the
|
|
257
|
-
* inbox (`vision-scanners-list` / `-create` / `-update`, and the
|
|
258
|
-
* advisory `vision-scanners-estimate-create` / `vision-quota-retrieve`).
|
|
259
|
-
* The scope OBJECT is `replay_scanner` — the `vision-scanners-*`
|
|
260
|
-
* names are MCP tool names, not scopes. Configuring a scanner also
|
|
261
|
-
* requires `session_recording:read` (the API pairs the two, since a
|
|
262
|
-
* scanner's config indirectly exposes recording contents); that one
|
|
263
|
-
* is already in this list for the step-2 usage probes.
|
|
264
|
-
*
|
|
265
|
-
* No OAuth-ceiling edit is needed for any scope here: they are all normal
|
|
266
|
-
* public (unprivileged, non-internal, non-hidden) scope objects, and the
|
|
267
|
-
* live wizard apps' ceiling is the `@default` sentinel, which resolves to
|
|
268
|
-
* every such scope (`UNPRIVILEGED_SCOPES`) and auto-tracks new ones. Only a
|
|
269
|
-
* privileged/internal/hidden object (e.g. `llm_gateway:*`) would need a
|
|
270
|
-
* manual per-app edit. See README → "OAuth app scope ceiling".
|
|
81
|
+
* Build an `OAuthError` from an authorize-callback's query params.
|
|
82
|
+
* Sanitizes every field — the callback URL is attacker-reachable input.
|
|
271
83
|
*/
|
|
272
|
-
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
"integration:read",
|
|
276
|
-
"signal_scout:read",
|
|
277
|
-
"signal_scout:write",
|
|
278
|
-
"session_recording:read",
|
|
279
|
-
"survey:read",
|
|
280
|
-
"error_tracking:read",
|
|
281
|
-
"external_data_source:read",
|
|
282
|
-
"external_data_source:write",
|
|
283
|
-
"llm_skill:read",
|
|
284
|
-
"llm_skill:write",
|
|
285
|
-
"product_enablement:write",
|
|
286
|
-
"replay_scanner:read",
|
|
287
|
-
"replay_scanner:write"
|
|
288
|
-
];
|
|
84
|
+
function oauthErrorFromCallbackParams(params) {
|
|
85
|
+
return new OAuthError(sanitizeOAuthParam(params.get("error")) ?? "unknown", sanitizeOAuthParam(params.get("error_description"), MAX_DESCRIPTION_LENGTH), sanitizeOAuthParam(params.get("error_uri")));
|
|
86
|
+
}
|
|
289
87
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* the wizard OAuth app's scope ceiling — the self-driving program requests the
|
|
295
|
-
* same pair.
|
|
88
|
+
* Build an `OAuthError` from a token-endpoint JSON error body (RFC 6749
|
|
89
|
+
* §5.2). Returns null when the body isn't an OAuth error shape (HTML error
|
|
90
|
+
* pages, proxy bodies, network failures) — the caller rethrows the raw
|
|
91
|
+
* error in that case.
|
|
296
92
|
*/
|
|
297
|
-
|
|
93
|
+
function oauthErrorFromTokenBody(data) {
|
|
94
|
+
const parsed = OAuthErrorBodySchema.safeParse(data);
|
|
95
|
+
if (!parsed.success) return null;
|
|
96
|
+
return new OAuthError(sanitizeOAuthParam(parsed.data.error) ?? "unknown", sanitizeOAuthParam(parsed.data.error_description, MAX_DESCRIPTION_LENGTH), sanitizeOAuthParam(parsed.data.error_uri));
|
|
97
|
+
}
|
|
298
98
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* completes the Slack OAuth step in the browser. Without `integration:read`
|
|
304
|
-
* the first poll 403s, the screen stops polling, and an already-connected
|
|
305
|
-
* project is nagged with the connect nudge. Used by the default integration
|
|
306
|
-
* run (the step ends the run) and by the standalone `wizard slack` flow
|
|
307
|
-
* (the step is the whole program).
|
|
99
|
+
* The error paragraphs for the browser callback page. The constant framing
|
|
100
|
+
* (title, styles, "return to your terminal") stays in `oauth.ts`; this owns
|
|
101
|
+
* the part that varies by error, with everything HTML-escaped since the
|
|
102
|
+
* values come straight off the callback query string.
|
|
308
103
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
* `vision-scanners-estimate-create` / `vision-quota-retrieve`). The scope
|
|
317
|
-
* OBJECT is `replay_scanner`; `vision-scanners-*` are MCP tool names.
|
|
318
|
-
* • session_recording:read — the scanner API pairs it with
|
|
319
|
-
* `replay_scanner:*`, since a scanner's config indirectly exposes
|
|
320
|
-
* recording contents. Configuring a scanner fails without it.
|
|
321
|
-
* • product_enablement:write — the enable-replay task's server half turns on
|
|
322
|
-
* Session Replay (`products-enable`) so there are recordings to scan.
|
|
323
|
-
*
|
|
324
|
-
* Without these the PostHog MCP omits the tools from the catalog it serves
|
|
325
|
-
* this token, every scanner task takes its "tool unknown" skip path, and the
|
|
326
|
-
* run reports success having created nothing (run 69afc6f8).
|
|
327
|
-
*
|
|
328
|
-
* No OAuth-ceiling edit needed — all are unprivileged public scope objects
|
|
329
|
-
* covered by the apps' `@default` sentinel, and self-driving already requests
|
|
330
|
-
* every one of them.
|
|
331
|
-
*/
|
|
332
|
-
const REPLAY_VISION_SCOPE_ADDITIONS = [
|
|
333
|
-
"session_recording:read",
|
|
334
|
-
"product_enablement:write",
|
|
335
|
-
"replay_scanner:read",
|
|
336
|
-
"replay_scanner:write"
|
|
337
|
-
];
|
|
338
|
-
/**
|
|
339
|
-
* Per-program scope additions, layered on top of `WIZARD_OAUTH_SCOPES`.
|
|
340
|
-
*
|
|
341
|
-
* Programs not listed here request the unchanged base set. Use this
|
|
342
|
-
* map only for programs that need *more* than the base — never for
|
|
343
|
-
* narrowing, since narrowing risks breaking shared infrastructure
|
|
344
|
-
* (e.g. dropping `llm_gateway:read` would 401 every agent call).
|
|
345
|
-
*
|
|
346
|
-
* Keyed by `ProgramId` so TypeScript catches stale entries when a
|
|
347
|
-
* program is renamed or removed.
|
|
348
|
-
*/
|
|
349
|
-
const PROGRAM_SCOPE_ADDITIONS = {
|
|
350
|
-
"mcp-tutorial": MCP_TUTORIAL_SCOPE_ADDITIONS,
|
|
351
|
-
"agent-skill": AGENT_SKILL_SCOPE_ADDITIONS,
|
|
352
|
-
"self-driving": SELF_DRIVING_SCOPE_ADDITIONS,
|
|
353
|
-
"warehouse-source": WAREHOUSE_SOURCE_SCOPE_ADDITIONS,
|
|
354
|
-
"posthog-integration": [...CONNECT_SLACK_SCOPE_ADDITIONS, ...WAREHOUSE_SOURCE_SCOPE_ADDITIONS],
|
|
355
|
-
slack: CONNECT_SLACK_SCOPE_ADDITIONS,
|
|
356
|
-
"replay-vision": REPLAY_VISION_SCOPE_ADDITIONS
|
|
357
|
-
};
|
|
358
|
-
/**
|
|
359
|
-
* Resolve the OAuth scope list to request for a given program. Returns
|
|
360
|
-
* `WIZARD_OAUTH_SCOPES` for programs without an addition entry; for
|
|
361
|
-
* programs that do have one, returns the union of base + additions
|
|
362
|
-
* with duplicates dropped (declaration order preserved, base first).
|
|
363
|
-
*
|
|
364
|
-
* `null` / `undefined` programId falls through to the default — same
|
|
365
|
-
* behavior as the historical hardcoded `WIZARD_OAUTH_SCOPES` reference
|
|
366
|
-
* in `askForWizardLogin`, so call sites that haven't been updated to
|
|
367
|
-
* pass a programId continue to work unchanged.
|
|
368
|
-
*/
|
|
369
|
-
function getOAuthScopesForProgram(programId) {
|
|
370
|
-
const additions = programId && PROGRAM_SCOPE_ADDITIONS[programId] || [];
|
|
371
|
-
if (additions.length === 0) return WIZARD_OAUTH_SCOPES;
|
|
372
|
-
const seen = /* @__PURE__ */ new Set();
|
|
373
|
-
const merged = [];
|
|
374
|
-
for (const s of [...WIZARD_OAUTH_SCOPES, ...additions]) {
|
|
375
|
-
if (seen.has(s)) continue;
|
|
376
|
-
seen.add(s);
|
|
377
|
-
merged.push(s);
|
|
378
|
-
}
|
|
379
|
-
return merged;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Resolve the scope list for the signup provisioning path. Same
|
|
383
|
-
* base-plus-additions shape as `getOAuthScopesForProgram`, but layered on
|
|
384
|
-
* `WIZARD_PROVISIONING_SCOPES` so a program's extra scopes only reach
|
|
385
|
-
* tokens provisioned for that program.
|
|
386
|
-
*/
|
|
387
|
-
function getProvisioningScopesForProgram(programId) {
|
|
388
|
-
const additions = programId && PROGRAM_SCOPE_ADDITIONS[programId] || [];
|
|
389
|
-
if (additions.length === 0) return WIZARD_PROVISIONING_SCOPES;
|
|
390
|
-
const seen = /* @__PURE__ */ new Set();
|
|
391
|
-
const merged = [];
|
|
392
|
-
for (const s of [...WIZARD_PROVISIONING_SCOPES, ...additions]) {
|
|
393
|
-
if (seen.has(s)) continue;
|
|
394
|
-
seen.add(s);
|
|
395
|
-
merged.push(s);
|
|
396
|
-
}
|
|
397
|
-
return merged;
|
|
398
|
-
}
|
|
399
|
-
//#endregion
|
|
400
|
-
//#region src/shared/host-resolution.ts
|
|
401
|
-
/**
|
|
402
|
-
* HostResolution — the single, immutable snapshot of where the wizard talks to
|
|
403
|
-
* PostHog for one run.
|
|
404
|
-
*
|
|
405
|
-
* One cloud region (or a pinned `--base-url`) implies a whole family of hosts:
|
|
406
|
-
* the event-ingestion/API host, the user-facing web app host, the CDN asset
|
|
407
|
-
* host, the LLM gateway, and the MCP server. Rather than re-deriving each of
|
|
408
|
-
* these at every call site and threading `region` + `baseUrl` around as loose
|
|
409
|
-
* params, resolve once at auth time and pass this frozen object around — read
|
|
410
|
-
* the property you need.
|
|
411
|
-
*
|
|
412
|
-
* The actual region→URL math (and the `--base-url` override) lives in
|
|
413
|
-
* `@utils/urls`; this class is a thin, immutable façade over those resolvers so
|
|
414
|
-
* the override flows through every field without callers having to know about
|
|
415
|
-
* it. The OAuth-server URL stays in `@utils/urls` (`getOAuthUrl`) because it is
|
|
416
|
-
* needed *before* a region is known, so it can't come from this post-auth object.
|
|
417
|
-
*/
|
|
418
|
-
const MCP_PROD_URL = "https://mcp.posthog.com/mcp";
|
|
419
|
-
function assetHostFor(region, baseUrl) {
|
|
420
|
-
const override = resolveBaseUrl(baseUrl);
|
|
421
|
-
if (override) return override;
|
|
422
|
-
return region === "eu" ? "https://eu-assets.i.posthog.com" : "https://us-assets.i.posthog.com";
|
|
423
|
-
}
|
|
424
|
-
function assetHostFromApiHost(apiHost) {
|
|
425
|
-
if (apiHost.includes("us.i.posthog.com")) return "https://us-assets.i.posthog.com";
|
|
426
|
-
if (apiHost.includes("eu.i.posthog.com")) return "https://eu-assets.i.posthog.com";
|
|
427
|
-
return apiHost;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Dev/CI only: the env override (see env.ts) wins even under --local-mcp;
|
|
431
|
-
* published builds strip the read. Don't name that env var in comments here —
|
|
432
|
-
* smoke-test.sh greps dist/*.js for it, and JSDoc survives bundling.
|
|
433
|
-
*/
|
|
434
|
-
function mcpUrlFor(localMcp) {
|
|
435
|
-
return localMcp ? MCP_LOCAL_URL : MCP_PROD_URL;
|
|
436
|
-
}
|
|
437
|
-
var HostResolution = class HostResolution {
|
|
438
|
-
/** The resolved cloud region. `'us'` when a base URL is pinned. */
|
|
439
|
-
region;
|
|
440
|
-
/**
|
|
441
|
-
* Event-ingestion / REST API host (e.g. `https://us.i.posthog.com`, or the
|
|
442
|
-
* pinned `--base-url`). The SDK `host` written into the user's `.env`, the
|
|
443
|
-
* base for the wizard-session REST calls, and the host shown to the agent.
|
|
444
|
-
*/
|
|
445
|
-
apiHost;
|
|
446
|
-
/**
|
|
447
|
-
* User-facing web app host (e.g. `https://us.posthog.com`). Use for any link
|
|
448
|
-
* we hand to the user or open in their browser (dashboards, settings, inbox,
|
|
449
|
-
* deep-link base).
|
|
450
|
-
*/
|
|
451
|
-
appHost;
|
|
452
|
-
/** CDN asset host (e.g. `https://us-assets.i.posthog.com`). */
|
|
453
|
-
assetHost;
|
|
454
|
-
/**
|
|
455
|
-
* PostHog MCP server URL the agent connects to. Region-independent — the
|
|
456
|
-
* server resolves the user's region from the bearer token — so this is driven
|
|
457
|
-
* only by `--local-mcp` / `--local-dev` and the dev env override, not by
|
|
458
|
-
* region/base-url.
|
|
459
|
-
*/
|
|
460
|
-
mcpUrl;
|
|
461
|
-
constructor(fields) {
|
|
462
|
-
this.region = fields.region;
|
|
463
|
-
this.apiHost = fields.apiHost;
|
|
464
|
-
this.appHost = fields.appHost;
|
|
465
|
-
this.assetHost = fields.assetHost;
|
|
466
|
-
this.mcpUrl = fields.mcpUrl;
|
|
467
|
-
Object.freeze(this);
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Canonical path: build the host family from a resolved region. A pinned
|
|
471
|
-
* `--base-url` (via `opts.baseUrl`) wins for every origin; otherwise the
|
|
472
|
-
* region's standard hosts are used. Honors IS_DEV through `resolveBaseUrl`.
|
|
473
|
-
*/
|
|
474
|
-
static fromRegion(region, opts = {}) {
|
|
475
|
-
return new HostResolution({
|
|
476
|
-
region,
|
|
477
|
-
apiHost: getHost(region, opts.baseUrl),
|
|
478
|
-
appHost: getCloudUrl(region, opts.baseUrl),
|
|
479
|
-
assetHost: assetHostFor(region, opts.baseUrl),
|
|
480
|
-
mcpUrl: mcpUrlFor(opts.localMcp ?? false)
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Build from an ingestion host string (the provisioning API returns one). The
|
|
485
|
-
* given host is preserved verbatim as `apiHost`; the region and the derived
|
|
486
|
-
* app/asset/gateway hosts are inferred from it.
|
|
487
|
-
*/
|
|
488
|
-
static fromApiHost(apiHost, opts = {}) {
|
|
489
|
-
return new HostResolution({
|
|
490
|
-
region: apiHost.includes("eu.") ? "eu" : "us",
|
|
491
|
-
apiHost,
|
|
492
|
-
appHost: getUiHostFromHost(apiHost),
|
|
493
|
-
assetHost: assetHostFromApiHost(apiHost),
|
|
494
|
-
mcpUrl: mcpUrlFor(opts.localMcp ?? false)
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Resolve the region from an access token (the us/eu probe, skipped when a
|
|
499
|
-
* base URL is pinned), then build the host family. Used after OAuth and after
|
|
500
|
-
* CI-mode API-key auth.
|
|
501
|
-
*/
|
|
502
|
-
static async fromAccessToken(accessToken, opts = {}) {
|
|
503
|
-
const region = opts.region ?? await detectRegion(accessToken, opts.baseUrl);
|
|
504
|
-
return HostResolution.fromRegion(region, opts);
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
//#endregion
|
|
508
|
-
//#region src/shared/utils/oauth-errors.ts
|
|
509
|
-
/**
|
|
510
|
-
* OAuth failure presentation — the structured error the flow throws and the
|
|
511
|
-
* user-facing messaging built from it.
|
|
512
|
-
*
|
|
513
|
-
* `oauth.ts` owns the flow machinery (callback server, token exchange, port
|
|
514
|
-
* retries); this module owns what a failure *means*: the RFC 6749 error
|
|
515
|
-
* fields (`error`, `error_description`, `error_uri`), per-code remediation
|
|
516
|
-
* copy, and safe rendering for the browser callback page. Everything here is
|
|
517
|
-
* pure so the copy can be unit-tested without the HTTP machinery.
|
|
518
|
-
*/
|
|
519
|
-
/**
|
|
520
|
-
* A structured OAuth failure (RFC 6749 §4.1.2.1 authorize errors, §5.2 token
|
|
521
|
-
* errors). Thrown by both the authorize callback and the token exchange so
|
|
522
|
-
* the flow's error handler keys remediation copy and analytics fingerprints
|
|
523
|
-
* off `code` instead of re-parsing `message`.
|
|
524
|
-
*
|
|
525
|
-
* The message stays exactly `OAuth error: <code>` — the analytics fingerprint
|
|
526
|
-
* scheme (`wizard_oauth_<code>`) and the legacy message-prefix parsing both
|
|
527
|
-
* depend on that shape. The description/uri ride along as properties.
|
|
528
|
-
*
|
|
529
|
-
* NAMING: never bind an instance to a variable or property whose name
|
|
530
|
-
* contains "oauth" (use `flowError`, `callbackError`, ...). CodeQL's
|
|
531
|
-
* clear-text-logging heuristic classifies any `*oauth*`-named variable read
|
|
532
|
-
* as password data, and these errors are deliberately shown to the user, so
|
|
533
|
-
* such a binding flags every log statement downstream of it (CWE-312).
|
|
534
|
-
*/
|
|
535
|
-
var OAuthError = class extends Error {
|
|
536
|
-
code;
|
|
537
|
-
/** The server's human-readable explanation (`error_description`), if sent. */
|
|
538
|
-
description;
|
|
539
|
-
/** The server's "more info" link (`error_uri`), if sent. */
|
|
540
|
-
uri;
|
|
541
|
-
constructor(code, description, uri) {
|
|
542
|
-
super(`OAuth error: ${code}`);
|
|
543
|
-
this.name = "OAuthError";
|
|
544
|
-
this.code = code;
|
|
545
|
-
this.description = description;
|
|
546
|
-
this.uri = uri;
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
/**
|
|
550
|
-
* Sanitize an OAuth error param before logging or rendering it: these arrive
|
|
551
|
-
* on a query string (or a token-endpoint body) that anything able to hit the
|
|
552
|
-
* local callback port can populate. Strips non-printable/non-ASCII characters
|
|
553
|
-
* and caps the length. Returns undefined when nothing printable remains.
|
|
554
|
-
*/
|
|
555
|
-
function sanitizeOAuthParam(value, maxLength = 200) {
|
|
556
|
-
if (!value) return void 0;
|
|
557
|
-
return value.replace(/[^\x20-\x7e]/g, "").slice(0, maxLength).trim() || void 0;
|
|
558
|
-
}
|
|
559
|
-
/** Description sentences get more room than bare codes/URIs. */
|
|
560
|
-
const MAX_DESCRIPTION_LENGTH = 500;
|
|
561
|
-
function escapeHtml(value) {
|
|
562
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
563
|
-
}
|
|
564
|
-
const OAuthErrorBodySchema = z.object({
|
|
565
|
-
error: z.string().min(1),
|
|
566
|
-
error_description: z.string().optional(),
|
|
567
|
-
error_uri: z.string().optional()
|
|
568
|
-
});
|
|
569
|
-
/**
|
|
570
|
-
* Build an `OAuthError` from an authorize-callback's query params.
|
|
571
|
-
* Sanitizes every field — the callback URL is attacker-reachable input.
|
|
572
|
-
*/
|
|
573
|
-
function oauthErrorFromCallbackParams(params) {
|
|
574
|
-
return new OAuthError(sanitizeOAuthParam(params.get("error")) ?? "unknown", sanitizeOAuthParam(params.get("error_description"), MAX_DESCRIPTION_LENGTH), sanitizeOAuthParam(params.get("error_uri")));
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Build an `OAuthError` from a token-endpoint JSON error body (RFC 6749
|
|
578
|
-
* §5.2). Returns null when the body isn't an OAuth error shape (HTML error
|
|
579
|
-
* pages, proxy bodies, network failures) — the caller rethrows the raw
|
|
580
|
-
* error in that case.
|
|
581
|
-
*/
|
|
582
|
-
function oauthErrorFromTokenBody(data) {
|
|
583
|
-
const parsed = OAuthErrorBodySchema.safeParse(data);
|
|
584
|
-
if (!parsed.success) return null;
|
|
585
|
-
return new OAuthError(sanitizeOAuthParam(parsed.data.error) ?? "unknown", sanitizeOAuthParam(parsed.data.error_description, MAX_DESCRIPTION_LENGTH), sanitizeOAuthParam(parsed.data.error_uri));
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* The error paragraphs for the browser callback page. The constant framing
|
|
589
|
-
* (title, styles, "return to your terminal") stays in `oauth.ts`; this owns
|
|
590
|
-
* the part that varies by error, with everything HTML-escaped since the
|
|
591
|
-
* values come straight off the callback query string.
|
|
592
|
-
*/
|
|
593
|
-
function buildCallbackErrorHtml(error) {
|
|
594
|
-
if (error.code === "access_denied") return "<p>Authorization cancelled.</p>";
|
|
595
|
-
const paragraphs = [`<p>Authorization failed (${escapeHtml(error.code)}).</p>`];
|
|
596
|
-
if (error.description) paragraphs.push(`<p>${escapeHtml(error.description)}</p>`);
|
|
597
|
-
if (error.uri) paragraphs.push(`<p>More info: ${escapeHtml(error.uri)}</p>`);
|
|
598
|
-
return paragraphs.join("\n");
|
|
599
|
-
}
|
|
104
|
+
function buildCallbackErrorHtml(error) {
|
|
105
|
+
if (error.code === "access_denied") return "<p>Authorization cancelled.</p>";
|
|
106
|
+
const paragraphs = [`<p>Authorization failed (${escapeHtml(error.code)}).</p>`];
|
|
107
|
+
if (error.description) paragraphs.push(`<p>${escapeHtml(error.description)}</p>`);
|
|
108
|
+
if (error.uri) paragraphs.push(`<p>More info: ${escapeHtml(error.uri)}</p>`);
|
|
109
|
+
return paragraphs.join("\n");
|
|
110
|
+
}
|
|
600
111
|
/**
|
|
601
112
|
* Per-code headline + "what to do" remediation for the terminal failure
|
|
602
113
|
* message. Codes not listed fall through to a generic rendering of the
|
|
@@ -657,29 +168,7 @@ function buildOAuthFailureMessage(params) {
|
|
|
657
168
|
return sections.join("\n\n");
|
|
658
169
|
}
|
|
659
170
|
//#endregion
|
|
660
|
-
//#region src/
|
|
661
|
-
const OAUTH_CALLBACK_STYLES = `
|
|
662
|
-
<style>
|
|
663
|
-
* {
|
|
664
|
-
font-family: monospace;
|
|
665
|
-
background-color: #1b0a00;
|
|
666
|
-
color: #F7A502;
|
|
667
|
-
font-weight: medium;
|
|
668
|
-
font-size: 24px;
|
|
669
|
-
margin: .25rem;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
.blink {
|
|
673
|
-
animation: blink-animation 1s steps(2, start) infinite;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
@keyframes blink-animation {
|
|
677
|
-
to {
|
|
678
|
-
opacity: 0;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
</style>
|
|
682
|
-
`;
|
|
171
|
+
//#region src/programs/oauth/tokens.ts
|
|
683
172
|
const OAuthTokenResponseSchema = z.object({
|
|
684
173
|
access_token: z.string(),
|
|
685
174
|
expires_in: z.number(),
|
|
@@ -691,7 +180,6 @@ const OAuthTokenResponseSchema = z.object({
|
|
|
691
180
|
posthog_region: z.enum(["us", "eu"]).optional().catch(void 0),
|
|
692
181
|
posthog_base_url: z.string().optional().catch(void 0)
|
|
693
182
|
});
|
|
694
|
-
const WIZARD_COMPLETION_SCOPE = "event_definition:write";
|
|
695
183
|
function parseOAuthScopes(scope) {
|
|
696
184
|
return scope.split(/\s+/).filter(Boolean);
|
|
697
185
|
}
|
|
@@ -709,14 +197,6 @@ function missingOAuthScopes(requested, grantedScope) {
|
|
|
709
197
|
const granted = new Set(parseOAuthScopes(grantedScope));
|
|
710
198
|
return requested.filter((scope) => !granted.has(scope));
|
|
711
199
|
}
|
|
712
|
-
function assertWizardCompletionScope(scope) {
|
|
713
|
-
if (parseOAuthScopes(scope).includes("event_definition:write")) return;
|
|
714
|
-
throw new Error(`This run was authorized without the ${WIZARD_COMPLETION_SCOPE} permission, which the wizard needs to finish setup. Please try again, approving all permissions on the PostHog authorization screen. If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first.`);
|
|
715
|
-
}
|
|
716
|
-
const AUTHORIZATION_TIMEOUT_MESSAGE = "Authorization timed out";
|
|
717
|
-
function isAuthorizationTimeout(error) {
|
|
718
|
-
return error.message === AUTHORIZATION_TIMEOUT_MESSAGE;
|
|
719
|
-
}
|
|
720
200
|
/**
|
|
721
201
|
* OAuth client ID for the current target. A pinned base URL (`--base-url`, or
|
|
722
202
|
* IS_DEV's implicit localhost) means we're talking to a dev-seeded stack, which
|
|
@@ -730,42 +210,98 @@ function isAuthorizationTimeout(error) {
|
|
|
730
210
|
function getOAuthClientId(baseUrl) {
|
|
731
211
|
return resolveBaseUrl(baseUrl) ? POSTHOG_DEV_CLIENT_ID : POSTHOG_PROXY_CLIENT_ID;
|
|
732
212
|
}
|
|
733
|
-
function
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
function getCallbackUrl(port) {
|
|
737
|
-
return `${getLocalOAuthOrigin(port)}/callback`;
|
|
738
|
-
}
|
|
739
|
-
function getLocalLoginUrl(port) {
|
|
740
|
-
return `${getLocalOAuthOrigin(port)}/authorize`;
|
|
741
|
-
}
|
|
742
|
-
function getLocalSignupUrl(port) {
|
|
743
|
-
return `${getLocalLoginUrl(port)}?signup=true`;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Extract an OAuth authorization code from raw user input. Accepts either the
|
|
747
|
-
* bare code, the full callback URL the browser was redirected to
|
|
748
|
-
* (`http://localhost:8239/callback?code=abc123&...`), or just the query
|
|
749
|
-
* string. Returns null when no code can be found.
|
|
750
|
-
*
|
|
751
|
-
* This backs the manual-entry fallback: in headless/remote environments the
|
|
752
|
-
* browser can't reach the wizard's local callback server, so the user copies
|
|
753
|
-
* the failed callback URL (or the code from it) back into the terminal.
|
|
754
|
-
*/
|
|
755
|
-
function extractOAuthCode(input) {
|
|
756
|
-
const trimmed = input.trim();
|
|
757
|
-
if (!trimmed) return null;
|
|
758
|
-
let looksLikeUrl = false;
|
|
213
|
+
async function refreshAccessToken(refreshToken, baseUrl, clientId) {
|
|
214
|
+
const oauthUrl = getOAuthUrl(baseUrl);
|
|
215
|
+
logToFile(`[oauth] refreshing access token at ${oauthUrl}/oauth/token`);
|
|
759
216
|
try {
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
217
|
+
const response = await axios.post(`${oauthUrl}/oauth/token`, {
|
|
218
|
+
grant_type: "refresh_token",
|
|
219
|
+
refresh_token: refreshToken,
|
|
220
|
+
client_id: clientId ?? getOAuthClientId(baseUrl)
|
|
221
|
+
}, {
|
|
222
|
+
headers: {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
"User-Agent": WIZARD_USER_AGENT
|
|
225
|
+
},
|
|
226
|
+
timeout: 3e4
|
|
227
|
+
});
|
|
228
|
+
const token = OAuthTokenResponseSchema.parse(response.data);
|
|
229
|
+
logToFile("[oauth] access token refreshed");
|
|
230
|
+
return token;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
logToFile("[oauth] token refresh failed:", e instanceof Error ? e.message : e);
|
|
233
|
+
throw (axios.isAxiosError(e) ? oauthErrorFromTokenBody(e.response?.data) : null) ?? e;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/tui/auth/oauth.ts
|
|
238
|
+
const OAUTH_CALLBACK_STYLES = `
|
|
239
|
+
<style>
|
|
240
|
+
* {
|
|
241
|
+
font-family: monospace;
|
|
242
|
+
background-color: #1b0a00;
|
|
243
|
+
color: #F7A502;
|
|
244
|
+
font-weight: medium;
|
|
245
|
+
font-size: 24px;
|
|
246
|
+
margin: .25rem;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.blink {
|
|
250
|
+
animation: blink-animation 1s steps(2, start) infinite;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@keyframes blink-animation {
|
|
254
|
+
to {
|
|
255
|
+
opacity: 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
</style>
|
|
259
|
+
`;
|
|
260
|
+
const WIZARD_COMPLETION_SCOPE = "event_definition:write";
|
|
261
|
+
function assertWizardCompletionScope(scope) {
|
|
262
|
+
if (parseOAuthScopes(scope).includes("event_definition:write")) return;
|
|
263
|
+
throw new Error(`This run was authorized without the ${WIZARD_COMPLETION_SCOPE} permission, which the wizard needs to finish setup. Please try again, approving all permissions on the PostHog authorization screen. If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first.`);
|
|
264
|
+
}
|
|
265
|
+
const AUTHORIZATION_TIMEOUT_MESSAGE = "Authorization timed out";
|
|
266
|
+
function isAuthorizationTimeout(error) {
|
|
267
|
+
return error.message === AUTHORIZATION_TIMEOUT_MESSAGE;
|
|
268
|
+
}
|
|
269
|
+
function getLocalOAuthOrigin(port) {
|
|
270
|
+
return `http://localhost:${port}`;
|
|
271
|
+
}
|
|
272
|
+
function getCallbackUrl(port) {
|
|
273
|
+
return `${getLocalOAuthOrigin(port)}/callback`;
|
|
274
|
+
}
|
|
275
|
+
function getLocalLoginUrl(port) {
|
|
276
|
+
return `${getLocalOAuthOrigin(port)}/authorize`;
|
|
277
|
+
}
|
|
278
|
+
function getLocalSignupUrl(port) {
|
|
279
|
+
return `${getLocalLoginUrl(port)}?signup=true`;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Extract an OAuth authorization code from raw user input. Accepts either the
|
|
283
|
+
* bare code, the full callback URL the browser was redirected to
|
|
284
|
+
* (`http://localhost:8239/callback?code=abc123&...`), or just the query
|
|
285
|
+
* string. Returns null when no code can be found.
|
|
286
|
+
*
|
|
287
|
+
* This backs the manual-entry fallback: in headless/remote environments the
|
|
288
|
+
* browser can't reach the wizard's local callback server, so the user copies
|
|
289
|
+
* the failed callback URL (or the code from it) back into the terminal.
|
|
290
|
+
*/
|
|
291
|
+
function extractOAuthCode(input) {
|
|
292
|
+
const trimmed = input.trim();
|
|
293
|
+
if (!trimmed) return null;
|
|
294
|
+
let looksLikeUrl = false;
|
|
295
|
+
try {
|
|
296
|
+
const url = new URL(trimmed);
|
|
297
|
+
looksLikeUrl = true;
|
|
298
|
+
const code = url.searchParams.get("code");
|
|
299
|
+
if (code) return code;
|
|
300
|
+
} catch {}
|
|
301
|
+
const match = trimmed.match(/[?&]?code=([^&\s]+)/);
|
|
302
|
+
if (match) return decodeURIComponent(match[1]);
|
|
303
|
+
if (looksLikeUrl) return null;
|
|
304
|
+
if (!/\s/.test(trimmed)) return trimmed;
|
|
769
305
|
return null;
|
|
770
306
|
}
|
|
771
307
|
function generateCodeVerifier() {
|
|
@@ -963,142 +499,374 @@ async function exchangeCodeForToken(code, codeVerifier, callbackUrl, baseUrl) {
|
|
|
963
499
|
logToFile(`[oauth] token exchange succeeded, granted scopes: ${token.scope}${token.posthog_region ? `, region: ${token.posthog_region}` : ""}${token.scoped_teams ? `, scoped_teams: [${token.scoped_teams.join(", ")}]` : ""}${token.scoped_organizations ? `, scoped_organizations: ${token.scoped_organizations.length}` : ""}`);
|
|
964
500
|
return token;
|
|
965
501
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Warn — at login, while the user is still watching — when the grant came back
|
|
504
|
+
* narrower than the request, and record the gap so narrowed runs are countable.
|
|
505
|
+
* Non-fatal by design: deselecting an optional scope is the user's call, and
|
|
506
|
+
* most flows survive it. The one scope the wizard cannot run without has its
|
|
507
|
+
* own hard check (`assertWizardCompletionScope`).
|
|
508
|
+
*/
|
|
509
|
+
function reportNarrowedGrant(requestedScopes, grantedScope) {
|
|
510
|
+
const missing = missingOAuthScopes(requestedScopes, grantedScope);
|
|
511
|
+
if (missing.length === 0) return;
|
|
512
|
+
logToFile(`[oauth] grant narrower than request, missing: ${missing.join(" ")}`);
|
|
513
|
+
analytics.wizardCapture("oauth grant narrowed", {
|
|
514
|
+
requested_scopes: [...requestedScopes].sort().join(" "),
|
|
515
|
+
granted_scopes: parseOAuthScopes(grantedScope).sort().join(" "),
|
|
516
|
+
missing_scopes: missing.join(" "),
|
|
517
|
+
missing_scope_count: missing.length
|
|
518
|
+
});
|
|
519
|
+
const plural = missing.length > 1;
|
|
520
|
+
getUI().log.warn(`Your PostHog authorization is missing ${plural ? `${missing.length} permissions` : "a permission"} the wizard asked for: ${missing.join(", ")}. Setup will continue, but steps that need ${plural ? "them" : "it"} may fail. To grant ${plural ? "them" : "it"}, re-run the wizard and approve all permissions on the authorization screen. If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first.`);
|
|
521
|
+
}
|
|
522
|
+
async function performOAuthFlow(config) {
|
|
523
|
+
const clientId = getOAuthClientId(config.baseUrl);
|
|
524
|
+
const oauthUrl = getOAuthUrl(config.baseUrl);
|
|
525
|
+
const codeVerifier = generateCodeVerifier();
|
|
526
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
527
|
+
let shouldRetry = false;
|
|
528
|
+
logToFile(`[oauth] starting flow against ${oauthUrl}, requested scopes: ${config.scopes.join(" ")}`);
|
|
529
|
+
do {
|
|
530
|
+
shouldRetry = false;
|
|
531
|
+
let lastProcessInfo = null;
|
|
532
|
+
for (const port of OAUTH_PORTS) {
|
|
533
|
+
const callbackUrl = getCallbackUrl(port);
|
|
534
|
+
const authUrl = new URL(`${oauthUrl}/oauth/authorize`);
|
|
535
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
536
|
+
authUrl.searchParams.set("redirect_uri", callbackUrl);
|
|
537
|
+
authUrl.searchParams.set("response_type", "code");
|
|
538
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
539
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
540
|
+
authUrl.searchParams.set("scope", config.scopes.join(" "));
|
|
541
|
+
authUrl.searchParams.set("required_access_level", "project");
|
|
542
|
+
if (config.projectId !== void 0) authUrl.searchParams.set("team_id", String(config.projectId));
|
|
543
|
+
const taggedAuthUrl = withUtm(authUrl.toString(), "oauth-authorize");
|
|
544
|
+
const signupUrl = new URL(withUtm(`${oauthUrl}/signup?next=${encodeURIComponent(taggedAuthUrl)}`, "oauth-signup"));
|
|
545
|
+
const localSignupUrl = getLocalSignupUrl(port);
|
|
546
|
+
const localLoginUrl = getLocalLoginUrl(port);
|
|
547
|
+
const urlToOpen = config.signup ? localSignupUrl : localLoginUrl;
|
|
548
|
+
logToFile(`[oauth] attempting callback server on port ${port}`);
|
|
549
|
+
let server;
|
|
550
|
+
let waitForCallback;
|
|
551
|
+
try {
|
|
552
|
+
({server, waitForCallback} = await startCallbackServer(taggedAuthUrl, signupUrl.toString(), port));
|
|
553
|
+
} catch (e) {
|
|
554
|
+
if (!isPortInUseError(e)) throw e;
|
|
555
|
+
lastProcessInfo = getPortProcessInfo(port);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
logToFile("[oauth] callback server ready, showing login URL");
|
|
559
|
+
getUI().setLoginUrl(urlToOpen);
|
|
560
|
+
getUI().setAuthorizeUrl(config.signup ? signupUrl.toString() : taggedAuthUrl);
|
|
561
|
+
openTrackedLink(urlToOpen, "oauth", {
|
|
562
|
+
auto: true,
|
|
563
|
+
skipUtm: true
|
|
564
|
+
});
|
|
565
|
+
const loginSpinner = getUI().spinner();
|
|
566
|
+
loginSpinner.start("Waiting for authorization...");
|
|
567
|
+
try {
|
|
568
|
+
const token = await exchangeCodeForToken(await Promise.race([
|
|
569
|
+
waitForCallback(),
|
|
570
|
+
getUI().waitForManualAuthCode(),
|
|
571
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(AUTHORIZATION_TIMEOUT_MESSAGE)), OAUTH_TIMEOUT_MS))
|
|
572
|
+
]), codeVerifier, callbackUrl, config.baseUrl);
|
|
573
|
+
server.close();
|
|
574
|
+
getUI().setLoginUrl(null);
|
|
575
|
+
getUI().setAuthorizeUrl(null);
|
|
576
|
+
loginSpinner.stop("Authorization complete!");
|
|
577
|
+
reportNarrowedGrant(config.scopes, token.scope);
|
|
578
|
+
return token;
|
|
579
|
+
} catch (e) {
|
|
580
|
+
const error = e instanceof Error ? e : /* @__PURE__ */ new Error("Unknown error");
|
|
581
|
+
const timedOut = isAuthorizationTimeout(error);
|
|
582
|
+
const flowError = error instanceof OAuthError ? error : null;
|
|
583
|
+
loginSpinner.stop(timedOut ? "Session timed out." : "Authorization failed.");
|
|
584
|
+
server.close();
|
|
585
|
+
logToFile("[oauth] flow failed:", error);
|
|
586
|
+
if (flowError?.description) logToFile(`[oauth] server error_description: ${flowError.description}`);
|
|
587
|
+
const accessDenied = flowError ? flowError.code === "access_denied" : error.message.includes("access_denied");
|
|
588
|
+
if (timedOut) getUI().showSessionTimeout();
|
|
589
|
+
else if (accessDenied) getUI().log.info(`Authorization was cancelled.\n\nYou denied access to PostHog. To use the wizard, you need to authorize access to your PostHog account.\n\nYou can try again by re-running the wizard.`);
|
|
590
|
+
else getUI().log.error(buildOAuthFailureMessage({
|
|
591
|
+
error,
|
|
592
|
+
requestedScopes: config.scopes,
|
|
593
|
+
clientId,
|
|
594
|
+
oauthUrl,
|
|
595
|
+
isDevStack: resolveBaseUrl(config.baseUrl) !== void 0
|
|
596
|
+
}));
|
|
597
|
+
const oauthErrorCode = flowError ? flowError.code : error.message.startsWith("OAuth error: ") ? error.message.slice(13) : timedOut ? "timeout" : "unknown";
|
|
598
|
+
analytics.captureException(error, {
|
|
599
|
+
step: "oauth_flow",
|
|
600
|
+
oauth_error_code: oauthErrorCode,
|
|
601
|
+
oauth_error_description: flowError?.description,
|
|
602
|
+
client_id: clientId,
|
|
603
|
+
requested_scopes: config.scopes.join(" "),
|
|
604
|
+
$exception_fingerprint: `wizard_oauth_${oauthErrorCode}`
|
|
605
|
+
});
|
|
606
|
+
await abort();
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (!lastProcessInfo) throw new Error("No OAuth callback ports configured");
|
|
611
|
+
await getUI().showPortConflict(lastProcessInfo);
|
|
612
|
+
shouldRetry = true;
|
|
613
|
+
} while (shouldRetry);
|
|
614
|
+
throw new Error("OAuth port retry loop exited unexpectedly");
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/programs/agent-skill/scopes.ts
|
|
618
|
+
/**
|
|
619
|
+
* Extra scopes the agent-skill program needs on top of `WIZARD_OAUTH_SCOPES`.
|
|
620
|
+
*
|
|
621
|
+
* Skills under this program (e.g. `creating-product-tours`) create feature
|
|
622
|
+
* flags during the install flow. PostHog's consent grants exactly the scope
|
|
623
|
+
* strings requested — `:write` does not imply `:read` — so listing existing
|
|
624
|
+
* flags to avoid key collisions needs `feature_flag:read` explicitly.
|
|
625
|
+
* `property_definition:read` lets the agent discover person properties when
|
|
626
|
+
* building flag rollout filters instead of having to ask the user verbatim.
|
|
627
|
+
*/
|
|
628
|
+
const AGENT_SKILL_SCOPE_ADDITIONS = [
|
|
629
|
+
"feature_flag:read",
|
|
630
|
+
"feature_flag:write",
|
|
631
|
+
"property_definition:read"
|
|
632
|
+
];
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/programs/replay-vision/scopes.ts
|
|
635
|
+
/**
|
|
636
|
+
* Extra scopes the replay-vision program needs on top of `WIZARD_OAUTH_SCOPES`.
|
|
637
|
+
* The same set self-driving's step 6c uses, narrowed to just this flow:
|
|
638
|
+
* • replay_scanner:read / replay_scanner:write — the scanner tasks list the
|
|
639
|
+
* team's existing scanners and create the ones scoped to the product's key
|
|
640
|
+
* flows (`vision-scanners-list` / `-create`, plus the advisory
|
|
641
|
+
* `vision-scanners-estimate-create` / `vision-quota-retrieve`). The scope
|
|
642
|
+
* OBJECT is `replay_scanner`; `vision-scanners-*` are MCP tool names.
|
|
643
|
+
* • session_recording:read — the scanner API pairs it with
|
|
644
|
+
* `replay_scanner:*`, since a scanner's config indirectly exposes
|
|
645
|
+
* recording contents. Configuring a scanner fails without it.
|
|
646
|
+
* • product_enablement:write — the enable-replay task's server half turns on
|
|
647
|
+
* Session Replay (`products-enable`) so there are recordings to scan.
|
|
648
|
+
*
|
|
649
|
+
* Without these the PostHog MCP omits the tools from the catalog it serves
|
|
650
|
+
* this token, every scanner task takes its "tool unknown" skip path, and the
|
|
651
|
+
* run reports success having created nothing (run 69afc6f8).
|
|
652
|
+
*
|
|
653
|
+
* No OAuth-ceiling edit needed — all are unprivileged public scope objects
|
|
654
|
+
* covered by the apps' `@default` sentinel, and self-driving already requests
|
|
655
|
+
* every one of them.
|
|
656
|
+
*/
|
|
657
|
+
const REPLAY_VISION_SCOPE_ADDITIONS = [
|
|
658
|
+
"session_recording:read",
|
|
659
|
+
"product_enablement:write",
|
|
660
|
+
"replay_scanner:read",
|
|
661
|
+
"replay_scanner:write"
|
|
662
|
+
];
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/programs/self-driving/scopes.ts
|
|
665
|
+
/**
|
|
666
|
+
* Extra scopes the self-driving program needs on top of
|
|
667
|
+
* `WIZARD_OAUTH_SCOPES`. All consumed by the PostHog MCP tools the
|
|
668
|
+
* agent drives during the run:
|
|
669
|
+
* • task:read / task:write — the signal source config API
|
|
670
|
+
* (`inbox-source-configs-*`) is permissioned under the generic
|
|
671
|
+
* `task` scope object, NOT a signals-specific one. Unrelated to
|
|
672
|
+
* the Tasks product.
|
|
673
|
+
* • integration:read — `integrations-list`, to check whether the
|
|
674
|
+
* team already has a GitHub integration and to verify the connect
|
|
675
|
+
* flow completed.
|
|
676
|
+
* • signal_scout:read / signal_scout:write — list, sync, and tune
|
|
677
|
+
* the Signals scout troop (`signals-scout-config-*`).
|
|
678
|
+
* • session_recording:read / survey:read / error_tracking:read —
|
|
679
|
+
* server-side product-usage probes (`query-session-recordings-list`,
|
|
680
|
+
* `survey-list`, `error-issue-list`). Product usage is a
|
|
681
|
+
* project-level fact (often instrumented in another repo or via
|
|
682
|
+
* the snippet), so the agent asks the server instead of inferring
|
|
683
|
+
* only from the local setup report. All three are read-only and
|
|
684
|
+
* already in the wizard OAuth app's production scope ceiling (the
|
|
685
|
+
* mcp-tutorial program requests them).
|
|
686
|
+
* • external_data_source:read / external_data_source:write — the
|
|
687
|
+
* connected-tools step creates the GitHub Issues / Linear warehouse
|
|
688
|
+
* sources directly (`external-data-sources-create`) and verifies
|
|
689
|
+
* what's actually connected (`external-data-sources-list`) instead
|
|
690
|
+
* of taking the user's word for it.
|
|
691
|
+
* • llm_skill:read / llm_skill:write — the custom-scouts step
|
|
692
|
+
* (skill step 6b): read the seeded `authoring-signals-scouts`
|
|
693
|
+
* guide and canonical scout bodies (`llma-skill-get` /
|
|
694
|
+
* `llma-skill-file-get`) and author the user-approved custom
|
|
695
|
+
* `signals-scout-*` skills (`llma-skill-create`). Canonical scout
|
|
696
|
+
* bodies are never edited.
|
|
697
|
+
* • product_enablement:write — the "Enable products" step turns on
|
|
698
|
+
* Session Replay / Error Tracking / Support so their sources have
|
|
699
|
+
* data to read (`products-enable`). A purpose-built scope: the
|
|
700
|
+
* server owns each enable recipe, so this can flip the product
|
|
701
|
+
* toggles without the far broader `project:write`.
|
|
702
|
+
* • replay_scanner:read / replay_scanner:write — the Replay Vision
|
|
703
|
+
* scanners step (skill step 6c) lists the team's existing scanners
|
|
704
|
+
* and creates the `emits_signals` ones whose findings land in the
|
|
705
|
+
* inbox (`vision-scanners-list` / `-create` / `-update`, and the
|
|
706
|
+
* advisory `vision-scanners-estimate-create` / `vision-quota-retrieve`).
|
|
707
|
+
* The scope OBJECT is `replay_scanner` — the `vision-scanners-*`
|
|
708
|
+
* names are MCP tool names, not scopes. Configuring a scanner also
|
|
709
|
+
* requires `session_recording:read` (the API pairs the two, since a
|
|
710
|
+
* scanner's config indirectly exposes recording contents); that one
|
|
711
|
+
* is already in this list for the step-2 usage probes.
|
|
712
|
+
*
|
|
713
|
+
* No OAuth-ceiling edit is needed for any scope here: they are all normal
|
|
714
|
+
* public (unprivileged, non-internal, non-hidden) scope objects, and the
|
|
715
|
+
* live wizard apps' ceiling is the `@default` sentinel, which resolves to
|
|
716
|
+
* every such scope (`UNPRIVILEGED_SCOPES`) and auto-tracks new ones. Only a
|
|
717
|
+
* privileged/internal/hidden object (e.g. `llm_gateway:*`) would need a
|
|
718
|
+
* manual per-app edit. See README → "OAuth app scope ceiling".
|
|
719
|
+
*/
|
|
720
|
+
const SELF_DRIVING_SCOPE_ADDITIONS = [
|
|
721
|
+
"task:read",
|
|
722
|
+
"task:write",
|
|
723
|
+
"integration:read",
|
|
724
|
+
"signal_scout:read",
|
|
725
|
+
"signal_scout:write",
|
|
726
|
+
"session_recording:read",
|
|
727
|
+
"survey:read",
|
|
728
|
+
"error_tracking:read",
|
|
729
|
+
"external_data_source:read",
|
|
730
|
+
"external_data_source:write",
|
|
731
|
+
"llm_skill:read",
|
|
732
|
+
"llm_skill:write",
|
|
733
|
+
"product_enablement:write",
|
|
734
|
+
"replay_scanner:read",
|
|
735
|
+
"replay_scanner:write"
|
|
736
|
+
];
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/shared/oauth-scopes.ts
|
|
739
|
+
/**
|
|
740
|
+
* Extra scope the Connect-Slack step needs on top of `WIZARD_OAUTH_SCOPES`.
|
|
741
|
+
*
|
|
742
|
+
* The step polls `/api/projects/:id/integrations/` (`fetchSlackConnected`)
|
|
743
|
+
* to render the already-connected variant and to flip live once the user
|
|
744
|
+
* completes the Slack OAuth step in the browser. Without `integration:read`
|
|
745
|
+
* the first poll 403s, the screen stops polling, and an already-connected
|
|
746
|
+
* project is nagged with the connect nudge. Used by the default integration
|
|
747
|
+
* run (the step ends the run) and by the standalone `wizard slack` flow
|
|
748
|
+
* (the step is the whole program).
|
|
749
|
+
*/
|
|
750
|
+
const CONNECT_SLACK_SCOPE_ADDITIONS = ["integration:read"];
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/tools/mcp/scopes.ts
|
|
753
|
+
/**
|
|
754
|
+
* Extra scopes the MCP tutorial needs on top of `WIZARD_OAUTH_SCOPES`.
|
|
755
|
+
*
|
|
756
|
+
* Every scope requested here must stay within the wizard OAuth app's
|
|
757
|
+
* ceiling on the PostHog side (`OAuthApplication.scopes`) — the full
|
|
758
|
+
* list lives in the README under "OAuth app scope ceiling". The
|
|
759
|
+
* tutorial's prompts and follow-ups touch most of the read surface,
|
|
760
|
+
* plus annotation write for the "PostHog wizard install" verify-prompt.
|
|
761
|
+
*
|
|
762
|
+
* Already in the base `WIZARD_OAUTH_SCOPES` (and therefore not
|
|
763
|
+
* repeated here):
|
|
764
|
+
* • user:read, project:read, llm_gateway:read — auth + gateway
|
|
765
|
+
* • query:read — HogQL
|
|
766
|
+
* • dashboard:write, insight:write, notebook:write — Phase-5 persist
|
|
767
|
+
*
|
|
768
|
+
* Deliberately omitted (writes on read-only product surfaces):
|
|
769
|
+
* • feature_flag:write, experiment:write, survey:write,
|
|
770
|
+
* cohort:write, session_recording:write, error_tracking:write,
|
|
771
|
+
* alert:write, subscription:write
|
|
772
|
+
*/
|
|
773
|
+
const MCP_TUTORIAL_SCOPE_ADDITIONS = [
|
|
774
|
+
"dashboard:read",
|
|
775
|
+
"insight:read",
|
|
776
|
+
"notebook:read",
|
|
777
|
+
"feature_flag:read",
|
|
778
|
+
"experiment:read",
|
|
779
|
+
"experiment_saved_metric:read",
|
|
780
|
+
"survey:read",
|
|
781
|
+
"session_recording:read",
|
|
782
|
+
"error_tracking:read",
|
|
783
|
+
"web_analytics:read",
|
|
784
|
+
"llm_analytics:read",
|
|
785
|
+
"cohort:read",
|
|
786
|
+
"person:read",
|
|
787
|
+
"annotation:read",
|
|
788
|
+
"annotation:write",
|
|
789
|
+
"activity_log:read",
|
|
790
|
+
"property_definition:read",
|
|
791
|
+
"event_definition:read",
|
|
792
|
+
"action:read",
|
|
793
|
+
"warehouse_table:read",
|
|
794
|
+
"warehouse_view:read",
|
|
795
|
+
"alert:read",
|
|
796
|
+
"subscription:read",
|
|
797
|
+
"integration:read"
|
|
798
|
+
];
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region src/programs/oauth/program-scopes.ts
|
|
801
|
+
/**
|
|
802
|
+
* Extra scopes the warehouse-source program needs on top of
|
|
803
|
+
* `WIZARD_OAUTH_SCOPES`. The agent creates data warehouse sources directly
|
|
804
|
+
* (`external-data-sources-create`) and lists what's connected
|
|
805
|
+
* (`external-data-sources-list`) to verify the result. Both are already within
|
|
806
|
+
* the wizard OAuth app's scope ceiling — the self-driving program requests the
|
|
807
|
+
* same pair.
|
|
808
|
+
*/
|
|
809
|
+
const WAREHOUSE_SOURCE_SCOPE_ADDITIONS = ["external_data_source:read", "external_data_source:write"];
|
|
810
|
+
/**
|
|
811
|
+
* Per-program scope additions, layered on top of `WIZARD_OAUTH_SCOPES`.
|
|
812
|
+
*
|
|
813
|
+
* Programs not listed here request the unchanged base set. Use this
|
|
814
|
+
* map only for programs that need *more* than the base — never for
|
|
815
|
+
* narrowing, since narrowing risks breaking shared infrastructure
|
|
816
|
+
* (e.g. dropping `llm_gateway:read` would 401 every agent call).
|
|
817
|
+
*
|
|
818
|
+
* Keyed by `ProgramId` so TypeScript catches stale entries when a
|
|
819
|
+
* program is renamed or removed.
|
|
820
|
+
*/
|
|
821
|
+
const PROGRAM_SCOPE_ADDITIONS = {
|
|
822
|
+
"mcp-tutorial": MCP_TUTORIAL_SCOPE_ADDITIONS,
|
|
823
|
+
"agent-skill": AGENT_SKILL_SCOPE_ADDITIONS,
|
|
824
|
+
"self-driving": SELF_DRIVING_SCOPE_ADDITIONS,
|
|
825
|
+
"warehouse-source": WAREHOUSE_SOURCE_SCOPE_ADDITIONS,
|
|
826
|
+
"posthog-integration": [...CONNECT_SLACK_SCOPE_ADDITIONS, ...WAREHOUSE_SOURCE_SCOPE_ADDITIONS],
|
|
827
|
+
slack: CONNECT_SLACK_SCOPE_ADDITIONS,
|
|
828
|
+
"replay-vision": REPLAY_VISION_SCOPE_ADDITIONS
|
|
829
|
+
};
|
|
830
|
+
/**
|
|
831
|
+
* Resolve the OAuth scope list to request for a given program. Returns
|
|
832
|
+
* `WIZARD_OAUTH_SCOPES` for programs without an addition entry; for
|
|
833
|
+
* programs that do have one, returns the union of base + additions
|
|
834
|
+
* with duplicates dropped (declaration order preserved, base first).
|
|
835
|
+
*
|
|
836
|
+
* `null` / `undefined` programId falls through to the default — same
|
|
837
|
+
* behavior as the historical hardcoded `WIZARD_OAUTH_SCOPES` reference
|
|
838
|
+
* in `askForWizardLogin`, so call sites that haven't been updated to
|
|
839
|
+
* pass a programId continue to work unchanged.
|
|
840
|
+
*/
|
|
841
|
+
function getOAuthScopesForProgram(programId) {
|
|
842
|
+
const additions = programId && PROGRAM_SCOPE_ADDITIONS[programId] || [];
|
|
843
|
+
if (additions.length === 0) return WIZARD_OAUTH_SCOPES;
|
|
844
|
+
const seen = /* @__PURE__ */ new Set();
|
|
845
|
+
const merged = [];
|
|
846
|
+
for (const s of [...WIZARD_OAUTH_SCOPES, ...additions]) {
|
|
847
|
+
if (seen.has(s)) continue;
|
|
848
|
+
seen.add(s);
|
|
849
|
+
merged.push(s);
|
|
987
850
|
}
|
|
851
|
+
return merged;
|
|
988
852
|
}
|
|
989
853
|
/**
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
* own hard check (`assertWizardCompletionScope`).
|
|
854
|
+
* Resolve the scope list for the signup provisioning path. Same
|
|
855
|
+
* base-plus-additions shape as `getOAuthScopesForProgram`, but layered on
|
|
856
|
+
* `WIZARD_PROVISIONING_SCOPES` so a program's extra scopes only reach
|
|
857
|
+
* tokens provisioned for that program.
|
|
995
858
|
*/
|
|
996
|
-
function
|
|
997
|
-
const
|
|
998
|
-
if (
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
getUI().log.warn(`Your PostHog authorization is missing ${plural ? `${missing.length} permissions` : "a permission"} the wizard asked for: ${missing.join(", ")}. Setup will continue, but steps that need ${plural ? "them" : "it"} may fail. To grant ${plural ? "them" : "it"}, re-run the wizard and approve all permissions on the authorization screen. If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first.`);
|
|
1008
|
-
}
|
|
1009
|
-
async function performOAuthFlow(config) {
|
|
1010
|
-
const clientId = getOAuthClientId(config.baseUrl);
|
|
1011
|
-
const oauthUrl = getOAuthUrl(config.baseUrl);
|
|
1012
|
-
const codeVerifier = generateCodeVerifier();
|
|
1013
|
-
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
1014
|
-
let shouldRetry = false;
|
|
1015
|
-
logToFile(`[oauth] starting flow against ${oauthUrl}, requested scopes: ${config.scopes.join(" ")}`);
|
|
1016
|
-
do {
|
|
1017
|
-
shouldRetry = false;
|
|
1018
|
-
let lastProcessInfo = null;
|
|
1019
|
-
for (const port of OAUTH_PORTS) {
|
|
1020
|
-
const callbackUrl = getCallbackUrl(port);
|
|
1021
|
-
const authUrl = new URL(`${oauthUrl}/oauth/authorize`);
|
|
1022
|
-
authUrl.searchParams.set("client_id", clientId);
|
|
1023
|
-
authUrl.searchParams.set("redirect_uri", callbackUrl);
|
|
1024
|
-
authUrl.searchParams.set("response_type", "code");
|
|
1025
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
1026
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1027
|
-
authUrl.searchParams.set("scope", config.scopes.join(" "));
|
|
1028
|
-
authUrl.searchParams.set("required_access_level", "project");
|
|
1029
|
-
if (config.projectId !== void 0) authUrl.searchParams.set("team_id", String(config.projectId));
|
|
1030
|
-
const taggedAuthUrl = withUtm(authUrl.toString(), "oauth-authorize");
|
|
1031
|
-
const signupUrl = new URL(withUtm(`${oauthUrl}/signup?next=${encodeURIComponent(taggedAuthUrl)}`, "oauth-signup"));
|
|
1032
|
-
const localSignupUrl = getLocalSignupUrl(port);
|
|
1033
|
-
const localLoginUrl = getLocalLoginUrl(port);
|
|
1034
|
-
const urlToOpen = config.signup ? localSignupUrl : localLoginUrl;
|
|
1035
|
-
logToFile(`[oauth] attempting callback server on port ${port}`);
|
|
1036
|
-
let server;
|
|
1037
|
-
let waitForCallback;
|
|
1038
|
-
try {
|
|
1039
|
-
({server, waitForCallback} = await startCallbackServer(taggedAuthUrl, signupUrl.toString(), port));
|
|
1040
|
-
} catch (e) {
|
|
1041
|
-
if (!isPortInUseError(e)) throw e;
|
|
1042
|
-
lastProcessInfo = getPortProcessInfo(port);
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
logToFile("[oauth] callback server ready, showing login URL");
|
|
1046
|
-
getUI().setLoginUrl(urlToOpen);
|
|
1047
|
-
getUI().setAuthorizeUrl(config.signup ? signupUrl.toString() : taggedAuthUrl);
|
|
1048
|
-
openTrackedLink(urlToOpen, "oauth", {
|
|
1049
|
-
auto: true,
|
|
1050
|
-
skipUtm: true
|
|
1051
|
-
});
|
|
1052
|
-
const loginSpinner = getUI().spinner();
|
|
1053
|
-
loginSpinner.start("Waiting for authorization...");
|
|
1054
|
-
try {
|
|
1055
|
-
const token = await exchangeCodeForToken(await Promise.race([
|
|
1056
|
-
waitForCallback(),
|
|
1057
|
-
getUI().waitForManualAuthCode(),
|
|
1058
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(AUTHORIZATION_TIMEOUT_MESSAGE)), OAUTH_TIMEOUT_MS))
|
|
1059
|
-
]), codeVerifier, callbackUrl, config.baseUrl);
|
|
1060
|
-
server.close();
|
|
1061
|
-
getUI().setLoginUrl(null);
|
|
1062
|
-
getUI().setAuthorizeUrl(null);
|
|
1063
|
-
loginSpinner.stop("Authorization complete!");
|
|
1064
|
-
reportNarrowedGrant(config.scopes, token.scope);
|
|
1065
|
-
return token;
|
|
1066
|
-
} catch (e) {
|
|
1067
|
-
const error = e instanceof Error ? e : /* @__PURE__ */ new Error("Unknown error");
|
|
1068
|
-
const timedOut = isAuthorizationTimeout(error);
|
|
1069
|
-
const flowError = error instanceof OAuthError ? error : null;
|
|
1070
|
-
loginSpinner.stop(timedOut ? "Session timed out." : "Authorization failed.");
|
|
1071
|
-
server.close();
|
|
1072
|
-
logToFile("[oauth] flow failed:", error);
|
|
1073
|
-
if (flowError?.description) logToFile(`[oauth] server error_description: ${flowError.description}`);
|
|
1074
|
-
const accessDenied = flowError ? flowError.code === "access_denied" : error.message.includes("access_denied");
|
|
1075
|
-
if (timedOut) getUI().showSessionTimeout();
|
|
1076
|
-
else if (accessDenied) getUI().log.info(`Authorization was cancelled.\n\nYou denied access to PostHog. To use the wizard, you need to authorize access to your PostHog account.\n\nYou can try again by re-running the wizard.`);
|
|
1077
|
-
else getUI().log.error(buildOAuthFailureMessage({
|
|
1078
|
-
error,
|
|
1079
|
-
requestedScopes: config.scopes,
|
|
1080
|
-
clientId,
|
|
1081
|
-
oauthUrl,
|
|
1082
|
-
isDevStack: resolveBaseUrl(config.baseUrl) !== void 0
|
|
1083
|
-
}));
|
|
1084
|
-
const oauthErrorCode = flowError ? flowError.code : error.message.startsWith("OAuth error: ") ? error.message.slice(13) : timedOut ? "timeout" : "unknown";
|
|
1085
|
-
analytics.captureException(error, {
|
|
1086
|
-
step: "oauth_flow",
|
|
1087
|
-
oauth_error_code: oauthErrorCode,
|
|
1088
|
-
oauth_error_description: flowError?.description,
|
|
1089
|
-
client_id: clientId,
|
|
1090
|
-
requested_scopes: config.scopes.join(" "),
|
|
1091
|
-
$exception_fingerprint: `wizard_oauth_${oauthErrorCode}`
|
|
1092
|
-
});
|
|
1093
|
-
await abort();
|
|
1094
|
-
throw error;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
if (!lastProcessInfo) throw new Error("No OAuth callback ports configured");
|
|
1098
|
-
await getUI().showPortConflict(lastProcessInfo);
|
|
1099
|
-
shouldRetry = true;
|
|
1100
|
-
} while (shouldRetry);
|
|
1101
|
-
throw new Error("OAuth port retry loop exited unexpectedly");
|
|
859
|
+
function getProvisioningScopesForProgram(programId) {
|
|
860
|
+
const additions = programId && PROGRAM_SCOPE_ADDITIONS[programId] || [];
|
|
861
|
+
if (additions.length === 0) return WIZARD_PROVISIONING_SCOPES;
|
|
862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
863
|
+
const merged = [];
|
|
864
|
+
for (const s of [...WIZARD_PROVISIONING_SCOPES, ...additions]) {
|
|
865
|
+
if (seen.has(s)) continue;
|
|
866
|
+
seen.add(s);
|
|
867
|
+
merged.push(s);
|
|
868
|
+
}
|
|
869
|
+
return merged;
|
|
1102
870
|
}
|
|
1103
871
|
//#endregion
|
|
1104
872
|
//#region src/shared/utils/project-resolution.ts
|
|
@@ -1129,158 +897,27 @@ function resolveGrantedProject(requestedProjectId, scopedTeams) {
|
|
|
1129
897
|
};
|
|
1130
898
|
}
|
|
1131
899
|
//#endregion
|
|
1132
|
-
//#region src/shared/
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Creates a version bucket function for analytics.
|
|
1144
|
-
* Converts versions like "1.2.3" to "1.x" for grouping in analytics.
|
|
1145
|
-
*
|
|
1146
|
-
* @param minMajorVersion - Optional minimum major version threshold.
|
|
1147
|
-
* Versions below this will be bucketed as "<{min}.0.0"
|
|
1148
|
-
*
|
|
1149
|
-
* @example
|
|
1150
|
-
* const getVersionBucket = createVersionBucket(); // no minimum
|
|
1151
|
-
* getVersionBucket("1.2.3") // "1.x"
|
|
1152
|
-
*
|
|
1153
|
-
* const getVersionBucket = createVersionBucket(11);
|
|
1154
|
-
* getVersionBucket("15.3.0") // "15.x"
|
|
1155
|
-
* getVersionBucket("10.0.0") // "<11.0.0"
|
|
1156
|
-
*/
|
|
1157
|
-
function createVersionBucket(minMajorVersion) {
|
|
1158
|
-
return (version) => {
|
|
1159
|
-
if (!version) return "none";
|
|
1160
|
-
if (isNonSemverVersion(version)) return "unknown";
|
|
1161
|
-
try {
|
|
1162
|
-
const minVer = minVersion(version);
|
|
1163
|
-
if (!minVer) return "invalid";
|
|
1164
|
-
const majorVersion = major(minVer);
|
|
1165
|
-
if (minMajorVersion !== void 0 && majorVersion < minMajorVersion) return `<${minMajorVersion}.0.0`;
|
|
1166
|
-
return `${majorVersion}.x`;
|
|
1167
|
-
} catch {
|
|
1168
|
-
return "unknown";
|
|
1169
|
-
}
|
|
900
|
+
//#region src/shared/api-key-login.ts
|
|
901
|
+
async function fetchProjectDataWithApiKey(apiKey, cloudUrl) {
|
|
902
|
+
const projectId = (await fetchUserData(apiKey, cloudUrl)).team?.id;
|
|
903
|
+
if (!projectId) throw new Error("Could not determine project ID from API key. Please ensure your API key has access to a project in this cloud region.");
|
|
904
|
+
const projectData = await fetchProjectData(apiKey, projectId, cloudUrl);
|
|
905
|
+
return {
|
|
906
|
+
api_token: projectData.api_token,
|
|
907
|
+
id: projectId,
|
|
908
|
+
project: projectData
|
|
1170
909
|
};
|
|
1171
910
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
/** @deprecated Use wizardAbort() directly for new code. */
|
|
1175
|
-
async function abort(message, status) {
|
|
1176
|
-
return wizardAbort({
|
|
1177
|
-
message,
|
|
1178
|
-
exitCode: status
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
const FREEMAIL_DOMAINS = new Set([
|
|
1182
|
-
"gmail.com",
|
|
1183
|
-
"googlemail.com",
|
|
1184
|
-
"hotmail.com",
|
|
1185
|
-
"outlook.com",
|
|
1186
|
-
"yahoo.com",
|
|
1187
|
-
"icloud.com",
|
|
1188
|
-
"me.com",
|
|
1189
|
-
"mail.com",
|
|
1190
|
-
"protonmail.com",
|
|
1191
|
-
"proton.me",
|
|
1192
|
-
"live.com",
|
|
1193
|
-
"aol.com",
|
|
1194
|
-
"yandex.com",
|
|
1195
|
-
"zoho.com",
|
|
1196
|
-
"gmx.com",
|
|
1197
|
-
"fastmail.com"
|
|
1198
|
-
]);
|
|
1199
|
-
function parseGitRemote() {
|
|
1200
|
-
try {
|
|
1201
|
-
const match = childProcess.execSync("git remote get-url origin", { stdio: [
|
|
1202
|
-
"ignore",
|
|
1203
|
-
"pipe",
|
|
1204
|
-
"ignore"
|
|
1205
|
-
] }).toString().trim().match(/[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
1206
|
-
if (match) return {
|
|
1207
|
-
org: match[1],
|
|
1208
|
-
repo: match[2]
|
|
1209
|
-
};
|
|
1210
|
-
} catch {}
|
|
1211
|
-
return null;
|
|
1212
|
-
}
|
|
1213
|
-
function detectOrgAndProject(email) {
|
|
1214
|
-
const remote = parseGitRemote();
|
|
1215
|
-
const projectName = remote?.repo || basename(process.cwd()) || void 0;
|
|
1216
|
-
let orgName;
|
|
1217
|
-
if (remote?.org) orgName = remote.org;
|
|
1218
|
-
else {
|
|
1219
|
-
const domain = email.split("@")[1]?.toLowerCase();
|
|
1220
|
-
if (domain && !FREEMAIL_DOMAINS.has(domain)) orgName = domain.split(".")[0];
|
|
1221
|
-
}
|
|
911
|
+
async function fetchProjectDataById(apiKey, projectId, cloudUrl) {
|
|
912
|
+
const projectData = await fetchProjectData(apiKey, projectId, cloudUrl);
|
|
1222
913
|
return {
|
|
1223
|
-
|
|
1224
|
-
|
|
914
|
+
api_token: projectData.api_token,
|
|
915
|
+
id: projectId,
|
|
916
|
+
project: projectData
|
|
1225
917
|
};
|
|
1226
918
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
* Only use where package.json is required (e.g., package install, overrides).
|
|
1230
|
-
* For detection/version-checks, use tryGetPackageJson() instead.
|
|
1231
|
-
*/
|
|
1232
|
-
async function getPackageDotJson({ installDir }) {
|
|
1233
|
-
const pkgPath = join$1(installDir, "package.json");
|
|
1234
|
-
let raw;
|
|
1235
|
-
try {
|
|
1236
|
-
raw = await fs$1.promises.readFile(pkgPath, "utf8");
|
|
1237
|
-
} catch {
|
|
1238
|
-
getUI().log.error("Could not find package.json. Make sure to run the wizard in the root of your app!");
|
|
1239
|
-
await abort();
|
|
1240
|
-
return {};
|
|
1241
|
-
}
|
|
1242
|
-
try {
|
|
1243
|
-
return JSON.parse(raw) ?? {};
|
|
1244
|
-
} catch {
|
|
1245
|
-
getUI().log.error(`Unable to parse your package.json. Make sure it has a valid format!`);
|
|
1246
|
-
await abort();
|
|
1247
|
-
return {};
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Try to get package.json, returning null if it doesn't exist.
|
|
1252
|
-
* Use this for detection purposes where missing package.json is expected (e.g., Python projects).
|
|
1253
|
-
*/
|
|
1254
|
-
async function tryGetPackageJson({ installDir }) {
|
|
1255
|
-
try {
|
|
1256
|
-
const packageJsonFileContents = await fs$1.promises.readFile(join$1(installDir, "package.json"), "utf8");
|
|
1257
|
-
return JSON.parse(packageJsonFileContents);
|
|
1258
|
-
} catch {
|
|
1259
|
-
return null;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
async function updatePackageDotJson(packageDotJson, { installDir }) {
|
|
1263
|
-
const pkgPath = join$1(installDir, "package.json");
|
|
1264
|
-
const serialized = JSON.stringify(packageDotJson, null, 2);
|
|
1265
|
-
try {
|
|
1266
|
-
await fs$1.promises.writeFile(pkgPath, serialized, {
|
|
1267
|
-
encoding: "utf8",
|
|
1268
|
-
flag: "w"
|
|
1269
|
-
});
|
|
1270
|
-
return;
|
|
1271
|
-
} catch {
|
|
1272
|
-
getUI().log.error(`Unable to update your package.json.`);
|
|
1273
|
-
await abort();
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
function isUsingTypeScript({ installDir }) {
|
|
1277
|
-
try {
|
|
1278
|
-
fs$1.accessSync(join$1(installDir, "tsconfig.json"));
|
|
1279
|
-
return true;
|
|
1280
|
-
} catch {
|
|
1281
|
-
return false;
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/tui/auth/project-data.ts
|
|
1284
921
|
/**
|
|
1285
922
|
* Get project data for the wizard via OAuth or CI API key.
|
|
1286
923
|
*/
|
|
@@ -1350,24 +987,6 @@ ${cloudUrl}/settings/project#variables`);
|
|
|
1350
987
|
missingScopes: missingScopes ?? []
|
|
1351
988
|
};
|
|
1352
989
|
}
|
|
1353
|
-
async function fetchProjectDataWithApiKey(apiKey, cloudUrl) {
|
|
1354
|
-
const projectId = (await fetchUserData(apiKey, cloudUrl)).team?.id;
|
|
1355
|
-
if (!projectId) throw new Error("Could not determine project ID from API key. Please ensure your API key has access to a project in this cloud region.");
|
|
1356
|
-
const projectData = await fetchProjectData(apiKey, projectId, cloudUrl);
|
|
1357
|
-
return {
|
|
1358
|
-
api_token: projectData.api_token,
|
|
1359
|
-
id: projectId,
|
|
1360
|
-
project: projectData
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
async function fetchProjectDataById(apiKey, projectId, cloudUrl) {
|
|
1364
|
-
const projectData = await fetchProjectData(apiKey, projectId, cloudUrl);
|
|
1365
|
-
return {
|
|
1366
|
-
api_token: projectData.api_token,
|
|
1367
|
-
id: projectId,
|
|
1368
|
-
project: projectData
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
990
|
async function askForWizardLogin(options) {
|
|
1372
991
|
if (options.signup) return askForProvisioningSignup(options.email, options.region, options.baseUrl, options.localMcp, options.programId);
|
|
1373
992
|
const requestedScopes = [...getOAuthScopesForProgram(options.programId)];
|
|
@@ -1507,6 +1126,6 @@ async function askForProvisioningSignup(email, region, baseUrl, localMcp, progra
|
|
|
1507
1126
|
}
|
|
1508
1127
|
}
|
|
1509
1128
|
//#endregion
|
|
1510
|
-
export {
|
|
1129
|
+
export { OAuthError as a, refreshAccessToken as i, extractOAuthCode as n, missingOAuthScopes as r, getOrAskForProjectData as t };
|
|
1511
1130
|
|
|
1512
|
-
//# sourceMappingURL=
|
|
1131
|
+
//# sourceMappingURL=project-data-BO9IXn9y.js.map
|