terminalhire 0.42.18 → 0.42.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/bin/founder-verdict-sync.js +1 -1
- package/dist/bin/jpi-bounties.js +252 -108
- package/dist/bin/jpi-chat-read.js +152 -76
- package/dist/bin/jpi-chat.js +194 -107
- package/dist/bin/jpi-claim.js +597 -121
- package/dist/bin/jpi-contribute.js +157 -102
- package/dist/bin/jpi-decline.js +580 -0
- package/dist/bin/jpi-dispatch.js +1217 -530
- package/dist/bin/jpi-hub.js +153 -73
- package/dist/bin/jpi-inbox.js +194 -107
- package/dist/bin/jpi-intro.js +185 -128
- package/dist/bin/jpi-jobs.js +30 -8
- package/dist/bin/jpi-link.js +119 -118
- package/dist/bin/jpi-mcp-chat.js +153 -76
- package/dist/bin/jpi-mcp.js +766 -229
- package/dist/bin/jpi-refresh.js +9 -0
- package/dist/bin/jpi-run.js +323 -43
- package/dist/bin/jpi-statusline.js +1 -0
- package/dist/bin/jpi-trajectory.js +177 -118
- package/dist/bin/jpi.js +127 -11
- package/dist/bin/pulse-prompt.js +128 -107
- package/dist/src/api-base.js +1 -0
- package/dist/src/chat-client.js +117 -53
- package/dist/src/intro.js +123 -65
- package/dist/src/link.js +110 -110
- package/dist/src/state-dir-pin.js +115 -0
- package/dist/src/trajectory.js +108 -50
- package/dist/src/web-session.js +72 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -310,6 +310,13 @@ node test/zero-egress.test.js
|
|
|
310
310
|
uses that one answer — so reads and writes can no longer disagree about which
|
|
311
311
|
environment you are on.
|
|
312
312
|
|
|
313
|
+
The credential store follows that answer. Pointed at `dev.terminalhire.com` by any
|
|
314
|
+
spelling (`thdev`, the variable, an MCP server entry), the CLI reads and writes
|
|
315
|
+
`~/.terminalhire-dev` unless `TERMINALHIRE_DIR` names a store (`src/state-dir-pin.ts`,
|
|
316
|
+
called first thing in `bin/jpi-dispatch.js`, before any subcommand loads). Production and loopback keep
|
|
317
|
+
`~/.terminalhire`. Before this, only `thdev` switched stores, and every other
|
|
318
|
+
spelling presented the production session to dev.
|
|
319
|
+
|
|
313
320
|
The override is an **allowlist**, not a free-form URL, because this same base decides
|
|
314
321
|
where your push token is presented. Only these are accepted:
|
|
315
322
|
|
|
@@ -182,7 +182,7 @@ async function syncFounderVerdicts({
|
|
|
182
182
|
const applied = [];
|
|
183
183
|
for (const t of plan) {
|
|
184
184
|
try {
|
|
185
|
-
claimsModule.updateClaim(t.id, { state: t.to });
|
|
185
|
+
claimsModule.updateClaim(t.id, { state: t.to, posterVerdict: t.verdict });
|
|
186
186
|
applied.push(t);
|
|
187
187
|
const line = buildVerdictNotice(t);
|
|
188
188
|
if (line) log(line);
|
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -10550,6 +10550,187 @@ var init_state_dir = __esm({
|
|
|
10550
10550
|
}
|
|
10551
10551
|
});
|
|
10552
10552
|
|
|
10553
|
+
// src/api-base.ts
|
|
10554
|
+
var api_base_exports = {};
|
|
10555
|
+
__export(api_base_exports, {
|
|
10556
|
+
ApiBaseError: () => ApiBaseError,
|
|
10557
|
+
DEV_API_BASE: () => DEV_API_BASE,
|
|
10558
|
+
DEV_STATE_DIR_NAME: () => DEV_STATE_DIR_NAME,
|
|
10559
|
+
PROD_API_BASE: () => PROD_API_BASE,
|
|
10560
|
+
__resetDevMarkerLatchForTests: () => __resetDevMarkerLatchForTests,
|
|
10561
|
+
formatDevMarker: () => formatDevMarker,
|
|
10562
|
+
isLoopbackOrigin: () => isLoopbackOrigin,
|
|
10563
|
+
isNonProdApiBase: () => isNonProdApiBase,
|
|
10564
|
+
pinToDevApiBase: () => pinToDevApiBase,
|
|
10565
|
+
printDevMarkerIfNeeded: () => printDevMarkerIfNeeded,
|
|
10566
|
+
resolveApiBase: () => resolveApiBase,
|
|
10567
|
+
resolveOAuthBase: () => resolveOAuthBase,
|
|
10568
|
+
warnSharedCredentialsIfNonProd: () => warnSharedCredentialsIfNonProd
|
|
10569
|
+
});
|
|
10570
|
+
import { homedir as homedir2 } from "os";
|
|
10571
|
+
import { join as join3 } from "path";
|
|
10572
|
+
function sanitizeOverrideForError(raw) {
|
|
10573
|
+
try {
|
|
10574
|
+
const url = new URL(raw);
|
|
10575
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
10576
|
+
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
10577
|
+
}
|
|
10578
|
+
if (url.username !== "" || url.password !== "") {
|
|
10579
|
+
return `${url.protocol}//***@${url.host}`;
|
|
10580
|
+
}
|
|
10581
|
+
return url.origin;
|
|
10582
|
+
} catch {
|
|
10583
|
+
return "(unparseable override)";
|
|
10584
|
+
}
|
|
10585
|
+
}
|
|
10586
|
+
function isLoopbackOrigin(origin) {
|
|
10587
|
+
try {
|
|
10588
|
+
const host = new URL(origin).hostname;
|
|
10589
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
10590
|
+
} catch {
|
|
10591
|
+
return false;
|
|
10592
|
+
}
|
|
10593
|
+
}
|
|
10594
|
+
function localApiAllowed(env) {
|
|
10595
|
+
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
10596
|
+
}
|
|
10597
|
+
function pinToDevApiBase(env = process.env) {
|
|
10598
|
+
env["TERMINALHIRE_API_URL"] = DEV_API_BASE;
|
|
10599
|
+
env["TERMINALHIRE_DIR"] = env["TERMINALHIRE_DIR"] || join3(homedir2(), DEV_STATE_DIR_NAME);
|
|
10600
|
+
return DEV_API_BASE;
|
|
10601
|
+
}
|
|
10602
|
+
function resolveApiBase(env = process.env) {
|
|
10603
|
+
for (const key of ENV_KEYS) {
|
|
10604
|
+
const raw = env[key];
|
|
10605
|
+
if (typeof raw !== "string") continue;
|
|
10606
|
+
const trimmed = raw.trim();
|
|
10607
|
+
if (trimmed === "") continue;
|
|
10608
|
+
const normalized = normalizeOverride(trimmed);
|
|
10609
|
+
if (normalized === null) {
|
|
10610
|
+
throw new ApiBaseError(
|
|
10611
|
+
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
10612
|
+
);
|
|
10613
|
+
}
|
|
10614
|
+
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
10615
|
+
throw new ApiBaseError(
|
|
10616
|
+
`terminalhire: ${key}=${normalized} is a loopback origin. Set ${ALLOW_LOCAL_API_KEY}=1 to talk to a local web app on purpose. Refusing so stored credentials cannot be exfiltrated to localhost by a poisoned override.`
|
|
10617
|
+
);
|
|
10618
|
+
}
|
|
10619
|
+
return normalized;
|
|
10620
|
+
}
|
|
10621
|
+
return PROD_API_BASE;
|
|
10622
|
+
}
|
|
10623
|
+
function resolveOAuthBase(env = process.env) {
|
|
10624
|
+
const base = resolveApiBase(env);
|
|
10625
|
+
if (OAUTH_ALLOWED_ORIGINS.includes(base)) return base;
|
|
10626
|
+
if (env[ALLOW_LOCAL_OAUTH_KEY] === "1") return base;
|
|
10627
|
+
throw new ApiBaseError(
|
|
10628
|
+
`terminalhire: the API base is ${base}, which is not a trusted origin for a browser sign-in. Point the CLI at ${DEV_API_BASE} for an end-to-end login, or set ${ALLOW_LOCAL_OAUTH_KEY}=1 (with ${ALLOW_LOCAL_API_KEY}=1) if you are running the web app locally on purpose. Refusing to open production sign-in while the API is local.`
|
|
10629
|
+
);
|
|
10630
|
+
}
|
|
10631
|
+
function resolveApiBaseOrProd(env = process.env) {
|
|
10632
|
+
try {
|
|
10633
|
+
return resolveApiBase(env);
|
|
10634
|
+
} catch {
|
|
10635
|
+
return PROD_API_BASE;
|
|
10636
|
+
}
|
|
10637
|
+
}
|
|
10638
|
+
function normalizeOverride(raw) {
|
|
10639
|
+
let url;
|
|
10640
|
+
try {
|
|
10641
|
+
url = new URL(raw);
|
|
10642
|
+
} catch {
|
|
10643
|
+
return null;
|
|
10644
|
+
}
|
|
10645
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
10646
|
+
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
10647
|
+
if (expectedProtocol === void 0) return null;
|
|
10648
|
+
if (url.protocol !== expectedProtocol) return null;
|
|
10649
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
10650
|
+
return null;
|
|
10651
|
+
}
|
|
10652
|
+
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
10653
|
+
if (rewrite !== void 0) return rewrite;
|
|
10654
|
+
return url.origin;
|
|
10655
|
+
}
|
|
10656
|
+
function isNonProdApiBase(base = resolveApiBaseOrProd()) {
|
|
10657
|
+
return base !== PROD_API_BASE;
|
|
10658
|
+
}
|
|
10659
|
+
function formatDevMarker(base = resolveApiBaseOrProd()) {
|
|
10660
|
+
if (!isNonProdApiBase(base)) return null;
|
|
10661
|
+
let host = base;
|
|
10662
|
+
try {
|
|
10663
|
+
host = new URL(base).host;
|
|
10664
|
+
} catch {
|
|
10665
|
+
}
|
|
10666
|
+
return `[dev \u2192 ${host}]`;
|
|
10667
|
+
}
|
|
10668
|
+
function printDevMarkerIfNeeded(stream = process.stderr) {
|
|
10669
|
+
if (markerPrinted) return;
|
|
10670
|
+
const marker = formatDevMarker();
|
|
10671
|
+
if (marker === null) return;
|
|
10672
|
+
markerPrinted = true;
|
|
10673
|
+
try {
|
|
10674
|
+
stream.write(`${marker}
|
|
10675
|
+
`);
|
|
10676
|
+
} catch {
|
|
10677
|
+
}
|
|
10678
|
+
}
|
|
10679
|
+
function __resetDevMarkerLatchForTests() {
|
|
10680
|
+
markerPrinted = false;
|
|
10681
|
+
}
|
|
10682
|
+
function warnSharedCredentialsIfNonProd(base, stream = process.stderr) {
|
|
10683
|
+
if (!isNonProdApiBase(base)) return;
|
|
10684
|
+
if (usingSeparateStateDir()) return;
|
|
10685
|
+
try {
|
|
10686
|
+
stream.write(
|
|
10687
|
+
"terminalhire: non-prod API base \u2014 using the same local session/push credentials as prod; do not mix environments casually.\n"
|
|
10688
|
+
);
|
|
10689
|
+
} catch {
|
|
10690
|
+
}
|
|
10691
|
+
}
|
|
10692
|
+
function usingSeparateStateDir(env = process.env) {
|
|
10693
|
+
const dir = env["TERMINALHIRE_DIR"];
|
|
10694
|
+
if (dir === void 0 || dir === "") return false;
|
|
10695
|
+
return dir.endsWith(DEV_STATE_DIR_NAME);
|
|
10696
|
+
}
|
|
10697
|
+
var PROD_API_BASE, DEV_API_BASE, DEV_STATE_DIR_NAME, ApiBaseError, ALLOWED_HOSTS, OAUTH_ALLOWED_ORIGINS, ALLOW_LOCAL_OAUTH_KEY, ALLOW_LOCAL_API_KEY, ALLOWED_DESCRIPTION, CANONICAL_REWRITES, ENV_KEYS, markerPrinted;
|
|
10698
|
+
var init_api_base = __esm({
|
|
10699
|
+
"src/api-base.ts"() {
|
|
10700
|
+
"use strict";
|
|
10701
|
+
PROD_API_BASE = "https://terminalhire.com";
|
|
10702
|
+
DEV_API_BASE = "https://dev.terminalhire.com";
|
|
10703
|
+
DEV_STATE_DIR_NAME = ".terminalhire-dev";
|
|
10704
|
+
ApiBaseError = class extends Error {
|
|
10705
|
+
constructor(message) {
|
|
10706
|
+
super(message);
|
|
10707
|
+
this.name = "ApiBaseError";
|
|
10708
|
+
}
|
|
10709
|
+
};
|
|
10710
|
+
ALLOWED_HOSTS = {
|
|
10711
|
+
"terminalhire.com": "https:",
|
|
10712
|
+
"www.terminalhire.com": "https:",
|
|
10713
|
+
"dev.terminalhire.com": "https:",
|
|
10714
|
+
localhost: "http:",
|
|
10715
|
+
"127.0.0.1": "http:"
|
|
10716
|
+
};
|
|
10717
|
+
OAUTH_ALLOWED_ORIGINS = [PROD_API_BASE, DEV_API_BASE];
|
|
10718
|
+
ALLOW_LOCAL_OAUTH_KEY = "TERMINALHIRE_ALLOW_LOCAL_OAUTH";
|
|
10719
|
+
ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
10720
|
+
ALLOWED_DESCRIPTION = [
|
|
10721
|
+
PROD_API_BASE,
|
|
10722
|
+
DEV_API_BASE,
|
|
10723
|
+
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
10724
|
+
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
10725
|
+
].join(", ");
|
|
10726
|
+
CANONICAL_REWRITES = {
|
|
10727
|
+
"www.terminalhire.com": PROD_API_BASE
|
|
10728
|
+
};
|
|
10729
|
+
ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
10730
|
+
markerPrinted = false;
|
|
10731
|
+
}
|
|
10732
|
+
});
|
|
10733
|
+
|
|
10553
10734
|
// bin/founder-pin.js
|
|
10554
10735
|
function isPinnedFounderBounty(j) {
|
|
10555
10736
|
return j?.bounty?.bountySource === "founder" && j?.bounty?.claimable === true;
|
|
@@ -11785,12 +11966,16 @@ var init_founder_paid_badge = __esm({
|
|
|
11785
11966
|
var web_session_exports = {};
|
|
11786
11967
|
__export(web_session_exports, {
|
|
11787
11968
|
clearWebSessionFile: () => clearWebSessionFile,
|
|
11969
|
+
hostLabel: () => hostLabel,
|
|
11970
|
+
missingSessionLines: () => missingSessionLines,
|
|
11788
11971
|
readWebSessionCookie: () => readWebSessionCookie,
|
|
11789
11972
|
readWebSessionFile: () => readWebSessionFile,
|
|
11790
11973
|
readWebSessionRecord: () => readWebSessionRecord,
|
|
11974
|
+
refusedSessionLines: () => refusedSessionLines,
|
|
11791
11975
|
webSessionCookieForHost: () => webSessionCookieForHost,
|
|
11792
11976
|
webSessionFilePath: () => webSessionFilePath,
|
|
11793
11977
|
webSessionForHost: () => webSessionForHost,
|
|
11978
|
+
withSessionDeps: () => withSessionDeps,
|
|
11794
11979
|
writeWebSessionFile: () => writeWebSessionFile
|
|
11795
11980
|
});
|
|
11796
11981
|
import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
@@ -11830,6 +12015,7 @@ function readWebSessionFile() {
|
|
|
11830
12015
|
return readWebSessionRecord()?.token ?? null;
|
|
11831
12016
|
}
|
|
11832
12017
|
function webSessionForHost(apiBase) {
|
|
12018
|
+
assertBase(apiBase, "webSessionForHost");
|
|
11833
12019
|
const record = readWebSessionRecord();
|
|
11834
12020
|
if (!record) return { cookie: null, mismatch: null };
|
|
11835
12021
|
if (record.host !== null && record.host !== apiBase) {
|
|
@@ -11843,11 +12029,50 @@ function webSessionCookieForHost(apiBase) {
|
|
|
11843
12029
|
const env = process.env["TERMINALHIRE_WEB_SESSION"];
|
|
11844
12030
|
return { cookie: typeof env === "string" && env.length > 0 ? env : null, mismatch: null };
|
|
11845
12031
|
}
|
|
11846
|
-
function
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
12032
|
+
function assertBase(apiBase, fn) {
|
|
12033
|
+
if (typeof apiBase !== "string" || apiBase.length === 0) {
|
|
12034
|
+
throw new TypeError(
|
|
12035
|
+
`${fn}(apiBase) requires the destination base as a non-empty string; received ${apiBase === "" ? "''" : String(apiBase)}. This is a wiring bug in the calling command, not a developer misconfiguration: pass the same resolveApiBase() value the request is sent to, so the session's host affinity can be checked (TERM-991).`
|
|
12036
|
+
);
|
|
12037
|
+
}
|
|
12038
|
+
}
|
|
12039
|
+
function hostLabel(base) {
|
|
12040
|
+
return String(base ?? "").replace(/^https?:\/\//, "");
|
|
12041
|
+
}
|
|
12042
|
+
function missingSessionLines(mismatch) {
|
|
12043
|
+
if (!mismatch) {
|
|
12044
|
+
return [
|
|
12045
|
+
"No linked web session found on this machine.",
|
|
12046
|
+
"Run `terminalhire link` to connect this terminal to your account, then re-run."
|
|
12047
|
+
];
|
|
12048
|
+
}
|
|
12049
|
+
const linked = hostLabel(mismatch.linkedHost);
|
|
12050
|
+
const current = hostLabel(mismatch.currentHost);
|
|
12051
|
+
const reach = mismatch.linkedHost === PROD_API_BASE ? `unset TERMINALHIRE_API_URL to reach ${linked}` : isLoopbackOrigin(mismatch.linkedHost) ? `set TERMINALHIRE_ALLOW_LOCAL_API=1 TERMINALHIRE_API_URL=${mismatch.linkedHost} to reach ${linked}` : `set TERMINALHIRE_API_URL=${mismatch.linkedHost} to reach ${linked}`;
|
|
12052
|
+
const link = isLoopbackOrigin(mismatch.currentHost) ? "`TERMINALHIRE_ALLOW_LOCAL_OAUTH=1 terminalhire link`" : "`terminalhire link`";
|
|
12053
|
+
return [
|
|
12054
|
+
`This terminal is on ${current}, but your linked session belongs to ${linked}. Nothing was sent.`,
|
|
12055
|
+
`Either ${reach}, or run ${link} to link this terminal to ${current} instead.`
|
|
12056
|
+
];
|
|
12057
|
+
}
|
|
12058
|
+
function withSessionDeps(defaults, overrides) {
|
|
12059
|
+
const merged = { ...defaults, ...overrides };
|
|
12060
|
+
if (overrides?.sessionCookie && !overrides.sessionMismatch) merged.sessionMismatch = () => null;
|
|
12061
|
+
return merged;
|
|
12062
|
+
}
|
|
12063
|
+
function refusedSessionLines(apiBase) {
|
|
12064
|
+
return [
|
|
12065
|
+
`${hostLabel(apiBase)} refused your linked session.`,
|
|
12066
|
+
"Run `terminalhire link` to link this terminal again, then re-run."
|
|
12067
|
+
];
|
|
12068
|
+
}
|
|
12069
|
+
function readWebSessionCookie(apiBase) {
|
|
12070
|
+
if (typeof apiBase !== "string" || apiBase.length === 0) {
|
|
12071
|
+
throw new TypeError(
|
|
12072
|
+
`readWebSessionCookie(apiBase) requires the destination base as a non-empty string; received ${apiBase === "" ? "''" : String(apiBase)}. This is a wiring bug in the calling command, not a developer misconfiguration: pass the same resolveApiBase() value the request is sent to, so the session's host affinity can be checked (TERM-991).`
|
|
12073
|
+
);
|
|
12074
|
+
}
|
|
12075
|
+
return webSessionCookieForHost(apiBase).cookie;
|
|
11851
12076
|
}
|
|
11852
12077
|
function writeWebSessionFile(token, host) {
|
|
11853
12078
|
ensureStateDirForSecret(terminalhireDir());
|
|
@@ -11868,6 +12093,7 @@ function clearWebSessionFile() {
|
|
|
11868
12093
|
var init_web_session = __esm({
|
|
11869
12094
|
"src/web-session.ts"() {
|
|
11870
12095
|
"use strict";
|
|
12096
|
+
init_api_base();
|
|
11871
12097
|
init_state_dir();
|
|
11872
12098
|
}
|
|
11873
12099
|
});
|
|
@@ -11976,15 +12202,27 @@ async function sendDecline(bountyId, reason) {
|
|
|
11976
12202
|
console.error("Not one of the answers \u2014 nothing sent.");
|
|
11977
12203
|
return false;
|
|
11978
12204
|
}
|
|
11979
|
-
|
|
11980
|
-
|
|
12205
|
+
let apiUrl;
|
|
12206
|
+
try {
|
|
12207
|
+
const { resolveApiBase: resolveApiBase2 } = await Promise.resolve().then(() => (init_api_base(), api_base_exports));
|
|
12208
|
+
apiUrl = resolveApiBase2();
|
|
12209
|
+
} catch (err) {
|
|
12210
|
+
console.log(`
|
|
12211
|
+
${err instanceof Error ? err.message : String(err)}`);
|
|
12212
|
+
return false;
|
|
12213
|
+
}
|
|
12214
|
+
const { missingSessionLines: missingSessionLines2, webSessionCookieForHost: webSessionCookieForHost2 } = await Promise.resolve().then(() => (init_web_session(), web_session_exports));
|
|
12215
|
+
const { cookie, mismatch } = webSessionCookieForHost2(apiUrl);
|
|
11981
12216
|
if (!cookie) {
|
|
11982
|
-
console.log(
|
|
12217
|
+
if (mismatch) console.log(`
|
|
12218
|
+
${missingSessionLines2(mismatch).join("\n")}`);
|
|
12219
|
+
else
|
|
12220
|
+
console.log("\nRun `terminalhire link` first \u2014 a poster needs to know the answer is real.");
|
|
11983
12221
|
return false;
|
|
11984
12222
|
}
|
|
11985
12223
|
if (!await ensureDeclineConsent(bountyId, reason)) return false;
|
|
11986
12224
|
try {
|
|
11987
|
-
const res = await fetch(`${
|
|
12225
|
+
const res = await fetch(`${apiUrl}/api/posting/decline`, {
|
|
11988
12226
|
method: "POST",
|
|
11989
12227
|
headers: {
|
|
11990
12228
|
"content-type": "application/json",
|
|
@@ -12013,11 +12251,10 @@ async function runDeclinePrompt(bountyId, { ask = prompt } = {}) {
|
|
|
12013
12251
|
if (!reason) return false;
|
|
12014
12252
|
return sendDecline(bountyId, reason);
|
|
12015
12253
|
}
|
|
12016
|
-
var
|
|
12254
|
+
var GH_SESSION_COOKIE, DECLINE_CHOICES;
|
|
12017
12255
|
var init_jpi_decline = __esm({
|
|
12018
12256
|
"bin/jpi-decline.js"() {
|
|
12019
12257
|
"use strict";
|
|
12020
|
-
API_URL = process.env["TERMINALHIRE_API_URL"] || "https://terminalhire.com";
|
|
12021
12258
|
GH_SESSION_COOKIE = "__jpi_gh_session";
|
|
12022
12259
|
DECLINE_CHOICES = [
|
|
12023
12260
|
{ key: "1", reason: "price_low", label: "the price is too low for the work" },
|
|
@@ -12249,106 +12486,13 @@ function linkTitle(title, url) {
|
|
|
12249
12486
|
return href ? `${safeTitle} (${href})` : safeTitle;
|
|
12250
12487
|
}
|
|
12251
12488
|
|
|
12252
|
-
// src/api-base.ts
|
|
12253
|
-
import { homedir as homedir2 } from "os";
|
|
12254
|
-
import { join as join3 } from "path";
|
|
12255
|
-
var PROD_API_BASE = "https://terminalhire.com";
|
|
12256
|
-
var DEV_API_BASE = "https://dev.terminalhire.com";
|
|
12257
|
-
var ApiBaseError = class extends Error {
|
|
12258
|
-
constructor(message) {
|
|
12259
|
-
super(message);
|
|
12260
|
-
this.name = "ApiBaseError";
|
|
12261
|
-
}
|
|
12262
|
-
};
|
|
12263
|
-
var ALLOWED_HOSTS = {
|
|
12264
|
-
"terminalhire.com": "https:",
|
|
12265
|
-
"www.terminalhire.com": "https:",
|
|
12266
|
-
"dev.terminalhire.com": "https:",
|
|
12267
|
-
localhost: "http:",
|
|
12268
|
-
"127.0.0.1": "http:"
|
|
12269
|
-
};
|
|
12270
|
-
var ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
12271
|
-
var ALLOWED_DESCRIPTION = [
|
|
12272
|
-
PROD_API_BASE,
|
|
12273
|
-
DEV_API_BASE,
|
|
12274
|
-
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
12275
|
-
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
12276
|
-
].join(", ");
|
|
12277
|
-
var CANONICAL_REWRITES = {
|
|
12278
|
-
"www.terminalhire.com": PROD_API_BASE
|
|
12279
|
-
};
|
|
12280
|
-
var ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
12281
|
-
function sanitizeOverrideForError(raw) {
|
|
12282
|
-
try {
|
|
12283
|
-
const url = new URL(raw);
|
|
12284
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
12285
|
-
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
12286
|
-
}
|
|
12287
|
-
if (url.username !== "" || url.password !== "") {
|
|
12288
|
-
return `${url.protocol}//***@${url.host}`;
|
|
12289
|
-
}
|
|
12290
|
-
return url.origin;
|
|
12291
|
-
} catch {
|
|
12292
|
-
return "(unparseable override)";
|
|
12293
|
-
}
|
|
12294
|
-
}
|
|
12295
|
-
function isLoopbackOrigin(origin) {
|
|
12296
|
-
try {
|
|
12297
|
-
const host = new URL(origin).hostname;
|
|
12298
|
-
return host === "localhost" || host === "127.0.0.1";
|
|
12299
|
-
} catch {
|
|
12300
|
-
return false;
|
|
12301
|
-
}
|
|
12302
|
-
}
|
|
12303
|
-
function localApiAllowed(env) {
|
|
12304
|
-
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
12305
|
-
}
|
|
12306
|
-
function resolveApiBase(env = process.env) {
|
|
12307
|
-
for (const key of ENV_KEYS) {
|
|
12308
|
-
const raw = env[key];
|
|
12309
|
-
if (typeof raw !== "string") continue;
|
|
12310
|
-
const trimmed = raw.trim();
|
|
12311
|
-
if (trimmed === "") continue;
|
|
12312
|
-
const normalized = normalizeOverride(trimmed);
|
|
12313
|
-
if (normalized === null) {
|
|
12314
|
-
throw new ApiBaseError(
|
|
12315
|
-
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
12316
|
-
);
|
|
12317
|
-
}
|
|
12318
|
-
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
12319
|
-
throw new ApiBaseError(
|
|
12320
|
-
`terminalhire: ${key}=${normalized} is a loopback origin. Set ${ALLOW_LOCAL_API_KEY}=1 to talk to a local web app on purpose. Refusing so stored credentials cannot be exfiltrated to localhost by a poisoned override.`
|
|
12321
|
-
);
|
|
12322
|
-
}
|
|
12323
|
-
return normalized;
|
|
12324
|
-
}
|
|
12325
|
-
return PROD_API_BASE;
|
|
12326
|
-
}
|
|
12327
|
-
function normalizeOverride(raw) {
|
|
12328
|
-
let url;
|
|
12329
|
-
try {
|
|
12330
|
-
url = new URL(raw);
|
|
12331
|
-
} catch {
|
|
12332
|
-
return null;
|
|
12333
|
-
}
|
|
12334
|
-
if (url.username !== "" || url.password !== "") return null;
|
|
12335
|
-
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
12336
|
-
if (expectedProtocol === void 0) return null;
|
|
12337
|
-
if (url.protocol !== expectedProtocol) return null;
|
|
12338
|
-
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
12339
|
-
return null;
|
|
12340
|
-
}
|
|
12341
|
-
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
12342
|
-
if (rewrite !== void 0) return rewrite;
|
|
12343
|
-
return url.origin;
|
|
12344
|
-
}
|
|
12345
|
-
|
|
12346
12489
|
// bin/jpi-bounties.js
|
|
12490
|
+
init_api_base();
|
|
12347
12491
|
init_founder_pin();
|
|
12348
12492
|
var TERMINALHIRE_DIR7 = process.env.TERMINALHIRE_DIR || join13(homedir10(), ".terminalhire");
|
|
12349
12493
|
var INDEX_CACHE_FILE2 = join13(TERMINALHIRE_DIR7, "index-cache.json");
|
|
12350
12494
|
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
12351
|
-
var
|
|
12495
|
+
var API_URL = resolveApiBase();
|
|
12352
12496
|
var RANK_MODE = process.env["TERMINALHIRE_BOUNTY_RANK"] ?? "winnability";
|
|
12353
12497
|
var CONTINUITY_RANK_DISABLED = process.env["TERMINALHIRE_NO_CONTINUITY_RANK"] === "1";
|
|
12354
12498
|
var DEFAULT_LIMIT = 15;
|
|
@@ -12369,7 +12513,7 @@ function readIndexCache() {
|
|
|
12369
12513
|
}
|
|
12370
12514
|
async function fetchIndex(quiet = false) {
|
|
12371
12515
|
const { index, reason, source, cachedAt } = await revalidateIndex({
|
|
12372
|
-
apiUrl:
|
|
12516
|
+
apiUrl: API_URL,
|
|
12373
12517
|
fetchImpl: (url, init) => fetch(url, init),
|
|
12374
12518
|
readEntry: readCacheEntry,
|
|
12375
12519
|
writeEntry: updateIndexCache
|
|
@@ -12520,7 +12664,7 @@ function classifyEmptyStatus({ preFilterCount, postFilterCount, priced }) {
|
|
|
12520
12664
|
return "empty";
|
|
12521
12665
|
}
|
|
12522
12666
|
async function getBounties({ quiet = false, offline = false, priced = PRICED_ONLY } = {}) {
|
|
12523
|
-
if (!quiet) console.log(`Fetching bounty index from ${
|
|
12667
|
+
if (!quiet) console.log(`Fetching bounty index from ${API_URL}/api/index...`);
|
|
12524
12668
|
const index = offline ? readIndexCache() : await fetchIndex(quiet);
|
|
12525
12669
|
if (offline && !index) return { status: "no-cache" };
|
|
12526
12670
|
let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
|