hatchkit 0.1.45 → 0.2.1
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/adopt.d.ts +61 -1
- package/dist/adopt.d.ts.map +1 -1
- package/dist/adopt.js +90 -86
- package/dist/adopt.js.map +1 -1
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +19 -1
- package/dist/completion.js.map +1 -1
- package/dist/config.d.ts +32 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +364 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify.d.ts +5 -0
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +67 -4
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/ghcr.d.ts +1 -0
- package/dist/deploy/ghcr.d.ts.map +1 -1
- package/dist/deploy/ghcr.js +2 -2
- package/dist/deploy/ghcr.js.map +1 -1
- package/dist/deploy/github.d.ts.map +1 -1
- package/dist/deploy/github.js +3 -2
- package/dist/deploy/github.js.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +9 -0
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/dev-setup.d.ts +10 -4
- package/dist/dev-setup.d.ts.map +1 -1
- package/dist/dev-setup.js +166 -57
- package/dist/dev-setup.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +65 -1
- package/dist/doctor.js.map +1 -1
- package/dist/email/index.js +5 -5
- package/dist/email/index.js.map +1 -1
- package/dist/email/setup.d.ts +1 -1
- package/dist/email/setup.d.ts.map +1 -1
- package/dist/email/setup.js +3 -3
- package/dist/email/setup.js.map +1 -1
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +8 -7
- package/dist/explain.js.map +1 -1
- package/dist/index.js +277 -60
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +1 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +2 -0
- package/dist/inventory.js.map +1 -1
- package/dist/onboarding/plan.d.ts +54 -0
- package/dist/onboarding/plan.d.ts.map +1 -0
- package/dist/onboarding/plan.js +143 -0
- package/dist/onboarding/plan.js.map +1 -0
- package/dist/onboarding/review.d.ts +27 -0
- package/dist/onboarding/review.d.ts.map +1 -0
- package/dist/onboarding/review.js +55 -0
- package/dist/onboarding/review.js.map +1 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +107 -89
- package/dist/prompts.js.map +1 -1
- package/dist/provision/index.d.ts +21 -3
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +112 -5
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/plausible.d.ts +10 -0
- package/dist/provision/plausible.d.ts.map +1 -0
- package/dist/provision/plausible.js +103 -0
- package/dist/provision/plausible.js.map +1 -0
- package/dist/provision/search-console.d.ts +17 -0
- package/dist/provision/search-console.d.ts.map +1 -0
- package/dist/provision/search-console.js +142 -0
- package/dist/provision/search-console.js.map +1 -0
- package/dist/scaffold/app.d.ts +1 -0
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +4 -1
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/infra.js +2 -0
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts +4 -2
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +7 -1
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/server-add.d.ts +21 -0
- package/dist/scaffold/server-add.d.ts.map +1 -0
- package/dist/scaffold/server-add.js +273 -0
- package/dist/scaffold/server-add.js.map +1 -0
- package/dist/scaffold/update.d.ts +1 -0
- package/dist/scaffold/update.d.ts.map +1 -1
- package/dist/scaffold/update.js +8 -5
- package/dist/scaffold/update.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +27 -1
- package/dist/status.js.map +1 -1
- package/dist/templates/base/env.example.hbs +3 -0
- package/dist/utils/cloudflare-api.d.ts +5 -0
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +19 -0
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/coolify-api.d.ts +3 -2
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +19 -5
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/flags.d.ts.map +1 -1
- package/dist/utils/flags.js +16 -0
- package/dist/utils/flags.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +3 -0
- package/dist/utils/run-ledger.d.ts.map +1 -1
- package/dist/utils/run-ledger.js.map +1 -1
- package/dist/utils/secrets.d.ts +5 -0
- package/dist/utils/secrets.d.ts.map +1 -1
- package/dist/utils/secrets.js +5 -0
- package/dist/utils/secrets.js.map +1 -1
- package/package.json +24 -3
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
1
3
|
import { Separator, confirm, input, password, select } from "@inquirer/prompts";
|
|
2
4
|
import chalk from "chalk";
|
|
3
5
|
import Conf from "conf";
|
|
4
6
|
import ora from "ora";
|
|
5
7
|
import { verifyCoolify } from "./utils/coolify-api.js";
|
|
6
8
|
import { execOk } from "./utils/exec.js";
|
|
9
|
+
import { pickPort } from "./utils/ports.js";
|
|
7
10
|
import { SECRET_KEYS, clearAllSecrets, deleteSecret, getSecret, setSecret, } from "./utils/secrets.js";
|
|
8
11
|
import { validateRequired, validateUrl } from "./utils/validate.js";
|
|
9
12
|
/** Sanity-check an S3 access/secret pair against shape rules. Returns
|
|
@@ -925,6 +928,55 @@ export async function getOpenpanelConfig() {
|
|
|
925
928
|
return { ...meta, rootClientId, rootClientSecret };
|
|
926
929
|
}
|
|
927
930
|
// ---------------------------------------------------------------------------
|
|
931
|
+
// Provider: Plausible (privacy-friendly web analytics)
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
export async function ensurePlausible() {
|
|
934
|
+
const existing = store.get("providers.plausible");
|
|
935
|
+
const existingKey = await getSecret(SECRET_KEYS.plausibleApiKey);
|
|
936
|
+
if (existing?.status === "configured" && existingKey) {
|
|
937
|
+
return { ...existing, apiKey: existingKey };
|
|
938
|
+
}
|
|
939
|
+
console.log(chalk.yellow("\n Plausible is not configured yet. Let's set it up."));
|
|
940
|
+
const url = (await input({
|
|
941
|
+
message: "Plausible base URL:",
|
|
942
|
+
default: existing?.url ?? "https://plausible.io",
|
|
943
|
+
validate: (v) => validateUrl(v.trim()),
|
|
944
|
+
}))
|
|
945
|
+
.trim()
|
|
946
|
+
.replace(/\/$/, "");
|
|
947
|
+
tokenHint(`${url}/settings`, "Sites API key (can list/create/delete sites; Plausible Cloud requires a Sites API-enabled plan)");
|
|
948
|
+
const apiKey = await confirmPastedSecret("Plausible Sites API key");
|
|
949
|
+
const teamId = (await input({
|
|
950
|
+
message: "Plausible team id (optional):",
|
|
951
|
+
default: existing?.teamId ?? "",
|
|
952
|
+
})).trim();
|
|
953
|
+
const timezone = (await input({
|
|
954
|
+
message: "Default site timezone:",
|
|
955
|
+
default: existing?.timezone ?? "Etc/UTC",
|
|
956
|
+
validate: validateRequired,
|
|
957
|
+
})).trim();
|
|
958
|
+
const meta = {
|
|
959
|
+
status: "configured",
|
|
960
|
+
url,
|
|
961
|
+
teamId: teamId || undefined,
|
|
962
|
+
timezone,
|
|
963
|
+
lastVerified: new Date().toISOString(),
|
|
964
|
+
};
|
|
965
|
+
store.set("providers.plausible", meta);
|
|
966
|
+
await setSecret(SECRET_KEYS.plausibleApiKey, apiKey);
|
|
967
|
+
console.log(chalk.green(" ✓ Plausible configured"));
|
|
968
|
+
return { ...meta, apiKey };
|
|
969
|
+
}
|
|
970
|
+
export async function getPlausibleConfig() {
|
|
971
|
+
const meta = store.get("providers.plausible");
|
|
972
|
+
if (!meta || meta.status !== "configured")
|
|
973
|
+
return null;
|
|
974
|
+
const apiKey = await getSecret(SECRET_KEYS.plausibleApiKey);
|
|
975
|
+
if (!apiKey)
|
|
976
|
+
return null;
|
|
977
|
+
return { ...meta, apiKey };
|
|
978
|
+
}
|
|
979
|
+
// ---------------------------------------------------------------------------
|
|
928
980
|
// Provider: Resend (transactional email SaaS)
|
|
929
981
|
// ---------------------------------------------------------------------------
|
|
930
982
|
export async function ensureResend() {
|
|
@@ -968,6 +1020,279 @@ export async function getResendConfig() {
|
|
|
968
1020
|
return { ...meta, apiKey };
|
|
969
1021
|
}
|
|
970
1022
|
// ---------------------------------------------------------------------------
|
|
1023
|
+
// Provider: Google Search Console
|
|
1024
|
+
// ---------------------------------------------------------------------------
|
|
1025
|
+
const GOOGLE_SEARCH_CONSOLE_SCOPES = [
|
|
1026
|
+
"https://www.googleapis.com/auth/webmasters",
|
|
1027
|
+
"https://www.googleapis.com/auth/siteverification.verify_only",
|
|
1028
|
+
];
|
|
1029
|
+
// Public Desktop OAuth client id for Hatchkit's own Google Cloud project.
|
|
1030
|
+
// Desktop/installed clients are public clients, so this value is safe to ship.
|
|
1031
|
+
// Until the Hatchkit project is verified, dev builds can set
|
|
1032
|
+
// HATCHKIT_GOOGLE_SEARCH_CONSOLE_CLIENT_ID to exercise the packaged PKCE path.
|
|
1033
|
+
const PACKAGED_GOOGLE_SEARCH_CONSOLE_CLIENT_ID = "932614455438-s0ih891al5pkeo4aeafekf01t6pbqd21.apps.googleusercontent.com";
|
|
1034
|
+
function hatchkitGoogleSearchConsoleClientId() {
|
|
1035
|
+
const configured = process.env.HATCHKIT_GOOGLE_SEARCH_CONSOLE_CLIENT_ID?.trim() ||
|
|
1036
|
+
PACKAGED_GOOGLE_SEARCH_CONSOLE_CLIENT_ID.trim();
|
|
1037
|
+
return configured || null;
|
|
1038
|
+
}
|
|
1039
|
+
const GOOGLE_SEARCH_CONSOLE_SCOPE_REQUIREMENTS = [
|
|
1040
|
+
{
|
|
1041
|
+
label: "Search Console read/write",
|
|
1042
|
+
scopes: ["https://www.googleapis.com/auth/webmasters"],
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
label: "Site Verification verify-only",
|
|
1046
|
+
scopes: [
|
|
1047
|
+
"https://www.googleapis.com/auth/siteverification.verify_only",
|
|
1048
|
+
"https://www.googleapis.com/auth/siteverification",
|
|
1049
|
+
],
|
|
1050
|
+
},
|
|
1051
|
+
];
|
|
1052
|
+
function assertGoogleSearchConsoleScopes(scopes) {
|
|
1053
|
+
const granted = new Set(scopes);
|
|
1054
|
+
const missing = GOOGLE_SEARCH_CONSOLE_SCOPE_REQUIREMENTS.filter((req) => !req.scopes.some((scope) => granted.has(scope)));
|
|
1055
|
+
if (missing.length === 0)
|
|
1056
|
+
return;
|
|
1057
|
+
throw new Error("Google OAuth did not grant every scope Hatchkit needs. " +
|
|
1058
|
+
`Missing: ${missing.map((m) => m.label).join(", ")}. ` +
|
|
1059
|
+
"Re-run setup and approve both Search Console and Site Verification access.");
|
|
1060
|
+
}
|
|
1061
|
+
function base64Url(input) {
|
|
1062
|
+
return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1063
|
+
}
|
|
1064
|
+
function createPkcePair() {
|
|
1065
|
+
const verifier = base64Url(randomBytes(64));
|
|
1066
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
1067
|
+
return { verifier, challenge };
|
|
1068
|
+
}
|
|
1069
|
+
async function exchangeGoogleCode(args) {
|
|
1070
|
+
const body = new URLSearchParams({
|
|
1071
|
+
client_id: args.clientId,
|
|
1072
|
+
code: args.code,
|
|
1073
|
+
grant_type: "authorization_code",
|
|
1074
|
+
redirect_uri: args.redirectUri,
|
|
1075
|
+
});
|
|
1076
|
+
if (args.clientSecret)
|
|
1077
|
+
body.set("client_secret", args.clientSecret);
|
|
1078
|
+
if (args.codeVerifier)
|
|
1079
|
+
body.set("code_verifier", args.codeVerifier);
|
|
1080
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
1081
|
+
method: "POST",
|
|
1082
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1083
|
+
body,
|
|
1084
|
+
});
|
|
1085
|
+
const json = (await res.json().catch(() => null));
|
|
1086
|
+
if (!res.ok || !json?.access_token) {
|
|
1087
|
+
const msg = json?.error_description ?? json?.error ?? `HTTP ${res.status}`;
|
|
1088
|
+
throw new Error(`Google OAuth token exchange failed: ${msg}`);
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
access_token: json.access_token,
|
|
1092
|
+
refresh_token: json.refresh_token,
|
|
1093
|
+
scope: json.scope,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
async function runGoogleOAuthLoopback(args) {
|
|
1097
|
+
const port = await pickPort(49152, 65535, new Set());
|
|
1098
|
+
const state = randomBytes(18).toString("hex");
|
|
1099
|
+
const pkce = args.clientSecret ? null : createPkcePair();
|
|
1100
|
+
const redirectUri = `http://127.0.0.1:${port}/oauth/google/callback`;
|
|
1101
|
+
let settled = false;
|
|
1102
|
+
let resolveCode = () => { };
|
|
1103
|
+
let rejectCode = () => { };
|
|
1104
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
1105
|
+
resolveCode = resolve;
|
|
1106
|
+
rejectCode = reject;
|
|
1107
|
+
});
|
|
1108
|
+
const server = createServer((req, res) => {
|
|
1109
|
+
const url = new URL(req.url ?? "/", redirectUri);
|
|
1110
|
+
if (url.pathname !== "/oauth/google/callback") {
|
|
1111
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1112
|
+
res.end("Not found");
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const gotState = url.searchParams.get("state");
|
|
1116
|
+
const code = url.searchParams.get("code");
|
|
1117
|
+
const error = url.searchParams.get("error");
|
|
1118
|
+
if (gotState !== state) {
|
|
1119
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
1120
|
+
res.end("State mismatch. Return to the terminal and retry setup.");
|
|
1121
|
+
if (!settled) {
|
|
1122
|
+
settled = true;
|
|
1123
|
+
rejectCode(new Error("Google OAuth state mismatch."));
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (error || !code) {
|
|
1128
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
1129
|
+
res.end("Google authorization failed. Return to the terminal and retry setup.");
|
|
1130
|
+
if (!settled) {
|
|
1131
|
+
settled = true;
|
|
1132
|
+
rejectCode(new Error(`Google OAuth failed: ${error ?? "missing code"}`));
|
|
1133
|
+
}
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
1137
|
+
res.end("Google Search Console authorization complete. You can close this tab.");
|
|
1138
|
+
if (!settled) {
|
|
1139
|
+
settled = true;
|
|
1140
|
+
resolveCode(code);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
await new Promise((resolve, reject) => {
|
|
1144
|
+
server.once("error", reject);
|
|
1145
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
1146
|
+
});
|
|
1147
|
+
const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
1148
|
+
authUrl.searchParams.set("client_id", args.clientId);
|
|
1149
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
1150
|
+
authUrl.searchParams.set("response_type", "code");
|
|
1151
|
+
authUrl.searchParams.set("scope", GOOGLE_SEARCH_CONSOLE_SCOPES.join(" "));
|
|
1152
|
+
authUrl.searchParams.set("access_type", "offline");
|
|
1153
|
+
authUrl.searchParams.set("prompt", "consent");
|
|
1154
|
+
authUrl.searchParams.set("state", state);
|
|
1155
|
+
if (pkce) {
|
|
1156
|
+
authUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
1157
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1158
|
+
}
|
|
1159
|
+
console.log(chalk.dim("\n Open this URL in your browser, approve access, then return here:"));
|
|
1160
|
+
console.log(chalk.cyan(` ${authUrl.toString()}\n`));
|
|
1161
|
+
try {
|
|
1162
|
+
const code = await codePromise;
|
|
1163
|
+
const token = await exchangeGoogleCode({
|
|
1164
|
+
clientId: args.clientId,
|
|
1165
|
+
clientSecret: args.clientSecret,
|
|
1166
|
+
code,
|
|
1167
|
+
redirectUri,
|
|
1168
|
+
codeVerifier: pkce?.verifier,
|
|
1169
|
+
});
|
|
1170
|
+
if (!token.refresh_token) {
|
|
1171
|
+
throw new Error("Google did not return a refresh token. Re-run setup and keep `prompt=consent`, or revoke the app at https://myaccount.google.com/permissions and try again.");
|
|
1172
|
+
}
|
|
1173
|
+
const scopes = token.scope?.split(/\s+/).filter(Boolean) ?? GOOGLE_SEARCH_CONSOLE_SCOPES;
|
|
1174
|
+
assertGoogleSearchConsoleScopes(scopes);
|
|
1175
|
+
return {
|
|
1176
|
+
refreshToken: token.refresh_token,
|
|
1177
|
+
scopes,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
finally {
|
|
1181
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
export async function refreshGoogleSearchConsoleAccessToken(cfg) {
|
|
1185
|
+
const body = new URLSearchParams({
|
|
1186
|
+
client_id: cfg.clientId,
|
|
1187
|
+
refresh_token: cfg.refreshToken,
|
|
1188
|
+
grant_type: "refresh_token",
|
|
1189
|
+
});
|
|
1190
|
+
if (cfg.clientSecret)
|
|
1191
|
+
body.set("client_secret", cfg.clientSecret);
|
|
1192
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
1193
|
+
method: "POST",
|
|
1194
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1195
|
+
body,
|
|
1196
|
+
});
|
|
1197
|
+
const json = (await res.json().catch(() => null));
|
|
1198
|
+
if (!res.ok || !json?.access_token) {
|
|
1199
|
+
const msg = json?.error_description ?? json?.error ?? `HTTP ${res.status}`;
|
|
1200
|
+
throw new Error(`Google refresh token failed: ${msg}`);
|
|
1201
|
+
}
|
|
1202
|
+
return json.access_token;
|
|
1203
|
+
}
|
|
1204
|
+
export async function ensureGoogleSearchConsole() {
|
|
1205
|
+
const existing = store.get("providers.googleSearchConsole");
|
|
1206
|
+
const existingClientId = await getSecret(SECRET_KEYS.googleSearchConsoleClientId);
|
|
1207
|
+
const existingClientSecret = await getSecret(SECRET_KEYS.googleSearchConsoleClientSecret);
|
|
1208
|
+
const existingRefreshToken = await getSecret(SECRET_KEYS.googleSearchConsoleRefreshToken);
|
|
1209
|
+
const hatchkitClientId = hatchkitGoogleSearchConsoleClientId();
|
|
1210
|
+
const existingMode = existing?.oauthMode ?? (existingClientSecret ? "byo-client" : "hatchkit-pkce");
|
|
1211
|
+
if (existing?.status === "configured" && existingRefreshToken) {
|
|
1212
|
+
if (existingMode === "hatchkit-pkce" && hatchkitClientId) {
|
|
1213
|
+
return {
|
|
1214
|
+
...existing,
|
|
1215
|
+
oauthMode: "hatchkit-pkce",
|
|
1216
|
+
clientId: hatchkitClientId,
|
|
1217
|
+
refreshToken: existingRefreshToken,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
if (existingClientId && existingClientSecret) {
|
|
1221
|
+
return {
|
|
1222
|
+
...existing,
|
|
1223
|
+
oauthMode: "byo-client",
|
|
1224
|
+
clientId: existingClientId,
|
|
1225
|
+
clientSecret: existingClientSecret,
|
|
1226
|
+
refreshToken: existingRefreshToken,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
console.log(chalk.yellow("\n Google Search Console is not configured yet. Let's set it up."));
|
|
1231
|
+
console.log(chalk.dim(" Hatchkit uses Google OAuth for Search Console and Site Verification.\n" +
|
|
1232
|
+
" You sign in with your own Google account; Hatchkit stores only that\n" +
|
|
1233
|
+
" account's refresh token in your OS keychain on this machine.\n"));
|
|
1234
|
+
let clientId;
|
|
1235
|
+
let clientSecret;
|
|
1236
|
+
let oauthMode;
|
|
1237
|
+
if (hatchkitClientId) {
|
|
1238
|
+
clientId = hatchkitClientId;
|
|
1239
|
+
oauthMode = "hatchkit-pkce";
|
|
1240
|
+
console.log(chalk.dim(" Using Hatchkit's shipped Google OAuth client with PKCE. No Google Cloud setup or client secret is needed."));
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
oauthMode = "byo-client";
|
|
1244
|
+
console.log(chalk.dim(" No packaged Hatchkit Google OAuth client id is configured in this build.\n" +
|
|
1245
|
+
" Falling back to the legacy BYO Google Cloud OAuth client setup.\n"));
|
|
1246
|
+
tokenHint("https://console.cloud.google.com/apis/credentials", "OAuth client (Desktop app) with Search Console API + Site Verification API enabled", `Scopes: ${GOOGLE_SEARCH_CONSOLE_SCOPES.join(", ")}`);
|
|
1247
|
+
clientId = (await input({
|
|
1248
|
+
message: "Google OAuth client ID:",
|
|
1249
|
+
default: existingClientId ?? undefined,
|
|
1250
|
+
validate: validateRequired,
|
|
1251
|
+
})).trim();
|
|
1252
|
+
clientSecret = await confirmPastedSecret("Google OAuth client secret");
|
|
1253
|
+
}
|
|
1254
|
+
const oauth = await runGoogleOAuthLoopback({ clientId, clientSecret });
|
|
1255
|
+
const meta = {
|
|
1256
|
+
status: "configured",
|
|
1257
|
+
scopes: oauth.scopes,
|
|
1258
|
+
oauthMode,
|
|
1259
|
+
lastVerified: new Date().toISOString(),
|
|
1260
|
+
};
|
|
1261
|
+
store.set("providers.googleSearchConsole", meta);
|
|
1262
|
+
if (oauthMode === "byo-client") {
|
|
1263
|
+
await setSecret(SECRET_KEYS.googleSearchConsoleClientId, clientId);
|
|
1264
|
+
if (clientSecret)
|
|
1265
|
+
await setSecret(SECRET_KEYS.googleSearchConsoleClientSecret, clientSecret);
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
await deleteSecret(SECRET_KEYS.googleSearchConsoleClientId);
|
|
1269
|
+
await deleteSecret(SECRET_KEYS.googleSearchConsoleClientSecret);
|
|
1270
|
+
}
|
|
1271
|
+
await setSecret(SECRET_KEYS.googleSearchConsoleRefreshToken, oauth.refreshToken);
|
|
1272
|
+
console.log(chalk.green(" ✓ Google Search Console configured"));
|
|
1273
|
+
return { ...meta, clientId, clientSecret, refreshToken: oauth.refreshToken };
|
|
1274
|
+
}
|
|
1275
|
+
export async function getGoogleSearchConsoleConfig() {
|
|
1276
|
+
const meta = store.get("providers.googleSearchConsole");
|
|
1277
|
+
if (!meta || meta.status !== "configured")
|
|
1278
|
+
return null;
|
|
1279
|
+
const oauthMode = meta.oauthMode ?? "byo-client";
|
|
1280
|
+
const clientId = await getSecret(SECRET_KEYS.googleSearchConsoleClientId);
|
|
1281
|
+
const clientSecret = await getSecret(SECRET_KEYS.googleSearchConsoleClientSecret);
|
|
1282
|
+
const refreshToken = await getSecret(SECRET_KEYS.googleSearchConsoleRefreshToken);
|
|
1283
|
+
if (!refreshToken)
|
|
1284
|
+
return null;
|
|
1285
|
+
if (oauthMode === "hatchkit-pkce") {
|
|
1286
|
+
const hatchkitClientId = hatchkitGoogleSearchConsoleClientId();
|
|
1287
|
+
if (!hatchkitClientId)
|
|
1288
|
+
return null;
|
|
1289
|
+
return { ...meta, oauthMode, clientId: hatchkitClientId, refreshToken };
|
|
1290
|
+
}
|
|
1291
|
+
if (!clientId || !clientSecret)
|
|
1292
|
+
return null;
|
|
1293
|
+
return { ...meta, oauthMode: "byo-client", clientId, clientSecret, refreshToken };
|
|
1294
|
+
}
|
|
1295
|
+
// ---------------------------------------------------------------------------
|
|
971
1296
|
// Provider: Stripe (payments)
|
|
972
1297
|
// ---------------------------------------------------------------------------
|
|
973
1298
|
//
|
|
@@ -1250,10 +1575,22 @@ export async function reconfigureProvider(name) {
|
|
|
1250
1575
|
]);
|
|
1251
1576
|
await ensureOpenpanel();
|
|
1252
1577
|
}
|
|
1578
|
+
else if (name === "plausible") {
|
|
1579
|
+
await wipeProvider("providers.plausible", [SECRET_KEYS.plausibleApiKey]);
|
|
1580
|
+
await ensurePlausible();
|
|
1581
|
+
}
|
|
1253
1582
|
else if (name === "resend") {
|
|
1254
1583
|
await wipeProvider("providers.resend", [SECRET_KEYS.resendApiKey]);
|
|
1255
1584
|
await ensureResend();
|
|
1256
1585
|
}
|
|
1586
|
+
else if (name === "search-console") {
|
|
1587
|
+
await wipeProvider("providers.googleSearchConsole", [
|
|
1588
|
+
SECRET_KEYS.googleSearchConsoleClientId,
|
|
1589
|
+
SECRET_KEYS.googleSearchConsoleClientSecret,
|
|
1590
|
+
SECRET_KEYS.googleSearchConsoleRefreshToken,
|
|
1591
|
+
]);
|
|
1592
|
+
await ensureGoogleSearchConsole();
|
|
1593
|
+
}
|
|
1257
1594
|
else if (name === "stripe") {
|
|
1258
1595
|
// NB: per-project Stripe entries (`stripe:project:<name>:*`) are
|
|
1259
1596
|
// intentionally NOT swept here — those belong to individual scaffolded
|
|
@@ -1397,6 +1734,15 @@ function buildSetupGroups() {
|
|
|
1397
1734
|
},
|
|
1398
1735
|
run: () => reconfigureProvider("openpanel"),
|
|
1399
1736
|
},
|
|
1737
|
+
{
|
|
1738
|
+
key: "plausible",
|
|
1739
|
+
label: "Plausible (web analytics)",
|
|
1740
|
+
status: () => {
|
|
1741
|
+
const m = store.get("providers.plausible");
|
|
1742
|
+
return { configured: m?.status === "configured", summary: m?.url };
|
|
1743
|
+
},
|
|
1744
|
+
run: () => reconfigureProvider("plausible"),
|
|
1745
|
+
},
|
|
1400
1746
|
{
|
|
1401
1747
|
key: "resend",
|
|
1402
1748
|
label: "Resend (transactional email)",
|
|
@@ -1406,6 +1752,23 @@ function buildSetupGroups() {
|
|
|
1406
1752
|
},
|
|
1407
1753
|
run: () => reconfigureProvider("resend"),
|
|
1408
1754
|
},
|
|
1755
|
+
{
|
|
1756
|
+
key: "search-console",
|
|
1757
|
+
label: "Google Search Console",
|
|
1758
|
+
status: () => {
|
|
1759
|
+
const m = store.get("providers.googleSearchConsole");
|
|
1760
|
+
return {
|
|
1761
|
+
configured: m?.status === "configured",
|
|
1762
|
+
summary: [
|
|
1763
|
+
m?.oauthMode === "hatchkit-pkce" ? "Hatchkit OAuth" : m ? "BYO OAuth" : null,
|
|
1764
|
+
m?.scopes?.length ? `${m.scopes.length} scopes` : null,
|
|
1765
|
+
]
|
|
1766
|
+
.filter(Boolean)
|
|
1767
|
+
.join(", "),
|
|
1768
|
+
};
|
|
1769
|
+
},
|
|
1770
|
+
run: () => reconfigureProvider("search-console"),
|
|
1771
|
+
},
|
|
1409
1772
|
{
|
|
1410
1773
|
key: "defaultForwardingEmail",
|
|
1411
1774
|
label: "Default forwarding email",
|
|
@@ -1513,7 +1876,7 @@ export async function runOnboarding() {
|
|
|
1513
1876
|
console.log();
|
|
1514
1877
|
}
|
|
1515
1878
|
// Summary — show both what's configured and what's still missing so
|
|
1516
|
-
// the user notices optional-but-important steps (GlitchTip / OpenPanel
|
|
1879
|
+
// the user notices optional-but-important steps (GlitchTip / OpenPanel / Plausible
|
|
1517
1880
|
// / Resend) they may have skipped.
|
|
1518
1881
|
const configured = allSteps.filter((s) => s.status().configured);
|
|
1519
1882
|
const unconfigured = allSteps.filter((s) => !s.status().configured);
|