terminalhire 0.42.7 → 0.42.9
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 +51 -7
- package/dist/bin/approved-claims-sync.js +93 -1
- package/dist/bin/claim-push-bg.js +115 -1
- package/dist/bin/directory.js +93 -1
- package/dist/bin/founder-note-sync.js +161 -0
- package/dist/bin/founder-verdict-sync.js +201 -0
- package/dist/bin/jpi-bounties.js +348 -14
- package/dist/bin/jpi-chat-read.js +104 -3
- package/dist/bin/jpi-chat.js +119 -9
- package/dist/bin/jpi-claim.js +311 -128
- package/dist/bin/jpi-contribute.js +93 -1
- package/dist/bin/jpi-devs.js +96 -2
- package/dist/bin/jpi-dispatch.js +1572 -863
- package/dist/bin/jpi-hub.js +114 -8
- package/dist/bin/jpi-inbox.js +115 -7
- package/dist/bin/jpi-init.js +93 -1
- package/dist/bin/jpi-intro.js +100 -1
- package/dist/bin/jpi-jobs.js +102 -2
- package/dist/bin/jpi-link.js +115 -3
- package/dist/bin/jpi-login.js +166 -29
- package/dist/bin/jpi-mcp-chat.js +140 -9
- package/dist/bin/jpi-mcp.js +332 -140
- package/dist/bin/jpi-post.js +93 -1
- package/dist/bin/jpi-project.js +93 -1
- package/dist/bin/jpi-protocol.js +27 -4
- package/dist/bin/jpi-refresh.js +128 -5
- package/dist/bin/jpi-run.js +399 -122
- package/dist/bin/jpi-sync.js +131 -5
- package/dist/bin/jpi-trajectory.js +115 -4
- package/dist/bin/jpi-update.js +27 -4
- package/dist/bin/peer-connect-prompt.js +121 -7
- package/dist/bin/pulse-prompt.js +93 -1
- package/dist/bin/recall-check.js +278 -0
- package/dist/src/api-base.js +156 -0
- package/dist/src/chat-client.js +93 -1
- package/dist/src/intro.js +93 -1
- package/dist/src/link.js +108 -3
- package/dist/src/protocol.js +27 -4
- package/dist/src/repo-policy.js +0 -36
- package/dist/src/trajectory.js +109 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -296,13 +296,57 @@ node test/zero-egress.test.js
|
|
|
296
296
|
|
|
297
297
|
## Environment variables
|
|
298
298
|
|
|
299
|
-
| Variable | Default | Description
|
|
300
|
-
| -------------------------- | ------------------------------------ |
|
|
301
|
-
| `TERMINALHIRE_API_URL` | `https://terminalhire.com` | Base URL for
|
|
302
|
-
| `GITHUB_CLIENT_ID` | _(required for real OAuth)_ | GitHub OAuth App client ID (device flow)
|
|
303
|
-
| `GITHUB_CLIENT_SECRET` | _(optional)_ | GitHub OAuth App client secret (public apps omit this)
|
|
304
|
-
| `GITHUB_DEVICE_CLIENT_ID` | _(falls back to `GITHUB_CLIENT_ID`)_ | Device-flow specific client ID if different
|
|
305
|
-
| `TERMINALHIRE_GITHUB_MOCK` | `0` | Set to `1` to skip real OAuth and use the fixture (dev/CI); also accepts legacy `JPI_GITHUB_MOCK`
|
|
299
|
+
| Variable | Default | Description |
|
|
300
|
+
| -------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
301
|
+
| `TERMINALHIRE_API_URL` | `https://terminalhire.com` | Base URL for **every** server call — reads, login/OAuth and background syncs alike (also accepts legacy `JPI_API_URL`). **Allowlisted hosts only** — see below |
|
|
302
|
+
| `GITHUB_CLIENT_ID` | _(required for real OAuth)_ | GitHub OAuth App client ID (device flow) |
|
|
303
|
+
| `GITHUB_CLIENT_SECRET` | _(optional)_ | GitHub OAuth App client secret (public apps omit this) |
|
|
304
|
+
| `GITHUB_DEVICE_CLIENT_ID` | _(falls back to `GITHUB_CLIENT_ID`)_ | Device-flow specific client ID if different |
|
|
305
|
+
| `TERMINALHIRE_GITHUB_MOCK` | `0` | Set to `1` to skip real OAuth and use the fixture (dev/CI); also accepts legacy `JPI_GITHUB_MOCK` |
|
|
306
|
+
|
|
307
|
+
### Pointing the CLI at a non-production server
|
|
308
|
+
|
|
309
|
+
`TERMINALHIRE_API_URL` is resolved once, in `src/api-base.ts`, and every call site
|
|
310
|
+
uses that one answer — so reads and writes can no longer disagree about which
|
|
311
|
+
environment you are on.
|
|
312
|
+
|
|
313
|
+
The override is an **allowlist**, not a free-form URL, because this same base decides
|
|
314
|
+
where your push token is presented. Only these are accepted:
|
|
315
|
+
|
|
316
|
+
| Host | Scheme | Extra flag |
|
|
317
|
+
| -------------------------------------------- | ------- | -------------------------------- |
|
|
318
|
+
| `terminalhire.com`, `www.terminalhire.com`\* | `https` | — |
|
|
319
|
+
| `dev.terminalhire.com` | `https` | — |
|
|
320
|
+
| `localhost`, `127.0.0.1` (any port) | `http` | `TERMINALHIRE_ALLOW_LOCAL_API=1` |
|
|
321
|
+
|
|
322
|
+
\* `www` is rewritten to the apex — its 301 strips the session cookie.
|
|
323
|
+
|
|
324
|
+
Anything else — an unknown host, the wrong scheme, embedded credentials, a string
|
|
325
|
+
that will not parse — makes the command **refuse to run**. It does not quietly fall
|
|
326
|
+
back to production: a typo'd override that resolved to prod is how a developer ends up
|
|
327
|
+
pushing claims at the real database while believing they are in a sandbox. It never
|
|
328
|
+
falls back to the host you named either, so a poisoned shell profile cannot redirect
|
|
329
|
+
you. Loopback without the opt-in is refused for the same reason: the URL alone must
|
|
330
|
+
not be enough to make ambient refresh present the persisted prod session to a local
|
|
331
|
+
listener.
|
|
332
|
+
|
|
333
|
+
**Browser sign-in uses a narrower allowlist.** `login`, `link`, `sync` and the claim
|
|
334
|
+
verification flows accept only `https://terminalhire.com` and
|
|
335
|
+
`https://dev.terminalhire.com`. Loopback OAuth requires both
|
|
336
|
+
`TERMINALHIRE_ALLOW_LOCAL_API=1` and `TERMINALHIRE_ALLOW_LOCAL_OAUTH=1`. Point at
|
|
337
|
+
`https://dev.terminalhire.com` for an end-to-end login against the shared-dev stack.
|
|
338
|
+
|
|
339
|
+
Note that `~/.terminalhire` is keyed per machine, not per origin: the web session,
|
|
340
|
+
push token and consent markers are **shared** with production. The credential-bearing
|
|
341
|
+
commands warn about this on stderr when a non-prod base is active.
|
|
342
|
+
|
|
343
|
+
When a non-production base is active, every command prints one line to stderr:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
[dev → dev.terminalhire.com]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
No marker means you are talking to production.
|
|
306
350
|
|
|
307
351
|
## File layout
|
|
308
352
|
|
|
@@ -1,5 +1,97 @@
|
|
|
1
|
+
// src/api-base.ts
|
|
2
|
+
var PROD_API_BASE = "https://terminalhire.com";
|
|
3
|
+
var DEV_API_BASE = "https://dev.terminalhire.com";
|
|
4
|
+
var ApiBaseError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ApiBaseError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var ALLOWED_HOSTS = {
|
|
11
|
+
"terminalhire.com": "https:",
|
|
12
|
+
"www.terminalhire.com": "https:",
|
|
13
|
+
"dev.terminalhire.com": "https:",
|
|
14
|
+
localhost: "http:",
|
|
15
|
+
"127.0.0.1": "http:"
|
|
16
|
+
};
|
|
17
|
+
var ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
18
|
+
var ALLOWED_DESCRIPTION = [
|
|
19
|
+
PROD_API_BASE,
|
|
20
|
+
DEV_API_BASE,
|
|
21
|
+
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
22
|
+
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
23
|
+
].join(", ");
|
|
24
|
+
var CANONICAL_REWRITES = {
|
|
25
|
+
"www.terminalhire.com": PROD_API_BASE
|
|
26
|
+
};
|
|
27
|
+
var ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
28
|
+
function sanitizeOverrideForError(raw) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(raw);
|
|
31
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
32
|
+
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
33
|
+
}
|
|
34
|
+
if (url.username !== "" || url.password !== "") {
|
|
35
|
+
return `${url.protocol}//***@${url.host}`;
|
|
36
|
+
}
|
|
37
|
+
return url.origin;
|
|
38
|
+
} catch {
|
|
39
|
+
return "(unparseable override)";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isLoopbackOrigin(origin) {
|
|
43
|
+
try {
|
|
44
|
+
const host = new URL(origin).hostname;
|
|
45
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function localApiAllowed(env) {
|
|
51
|
+
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
52
|
+
}
|
|
53
|
+
function resolveApiBase(env = process.env) {
|
|
54
|
+
for (const key of ENV_KEYS) {
|
|
55
|
+
const raw = env[key];
|
|
56
|
+
if (typeof raw !== "string") continue;
|
|
57
|
+
const trimmed = raw.trim();
|
|
58
|
+
if (trimmed === "") continue;
|
|
59
|
+
const normalized = normalizeOverride(trimmed);
|
|
60
|
+
if (normalized === null) {
|
|
61
|
+
throw new ApiBaseError(
|
|
62
|
+
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
66
|
+
throw new ApiBaseError(
|
|
67
|
+
`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.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return normalized;
|
|
71
|
+
}
|
|
72
|
+
return PROD_API_BASE;
|
|
73
|
+
}
|
|
74
|
+
function normalizeOverride(raw) {
|
|
75
|
+
let url;
|
|
76
|
+
try {
|
|
77
|
+
url = new URL(raw);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
82
|
+
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
83
|
+
if (expectedProtocol === void 0) return null;
|
|
84
|
+
if (url.protocol !== expectedProtocol) return null;
|
|
85
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
89
|
+
if (rewrite !== void 0) return rewrite;
|
|
90
|
+
return url.origin;
|
|
91
|
+
}
|
|
92
|
+
|
|
1
93
|
// bin/approved-claims-sync.js
|
|
2
|
-
var CLAIM_SYNC_BASE =
|
|
94
|
+
var CLAIM_SYNC_BASE = resolveApiBase();
|
|
3
95
|
function approvalsSyncGate({ autoMarkerExists, tokenFileExists }) {
|
|
4
96
|
if (!autoMarkerExists || !tokenFileExists) return { sync: false, reason: "not-opted-in" };
|
|
5
97
|
return { sync: true, reason: "ok" };
|
|
@@ -509,11 +509,125 @@ function decrypt(blob, key) {
|
|
|
509
509
|
|
|
510
510
|
// bin/claim-push-bg.js
|
|
511
511
|
init_state_dir();
|
|
512
|
+
|
|
513
|
+
// src/api-base.ts
|
|
514
|
+
var PROD_API_BASE = "https://terminalhire.com";
|
|
515
|
+
var DEV_API_BASE = "https://dev.terminalhire.com";
|
|
516
|
+
var ApiBaseError = class extends Error {
|
|
517
|
+
constructor(message) {
|
|
518
|
+
super(message);
|
|
519
|
+
this.name = "ApiBaseError";
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
var ALLOWED_HOSTS = {
|
|
523
|
+
"terminalhire.com": "https:",
|
|
524
|
+
"www.terminalhire.com": "https:",
|
|
525
|
+
"dev.terminalhire.com": "https:",
|
|
526
|
+
localhost: "http:",
|
|
527
|
+
"127.0.0.1": "http:"
|
|
528
|
+
};
|
|
529
|
+
var ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
530
|
+
var ALLOWED_DESCRIPTION = [
|
|
531
|
+
PROD_API_BASE,
|
|
532
|
+
DEV_API_BASE,
|
|
533
|
+
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
534
|
+
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
535
|
+
].join(", ");
|
|
536
|
+
var CANONICAL_REWRITES = {
|
|
537
|
+
"www.terminalhire.com": PROD_API_BASE
|
|
538
|
+
};
|
|
539
|
+
var ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
540
|
+
function sanitizeOverrideForError(raw) {
|
|
541
|
+
try {
|
|
542
|
+
const url = new URL(raw);
|
|
543
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
544
|
+
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
545
|
+
}
|
|
546
|
+
if (url.username !== "" || url.password !== "") {
|
|
547
|
+
return `${url.protocol}//***@${url.host}`;
|
|
548
|
+
}
|
|
549
|
+
return url.origin;
|
|
550
|
+
} catch {
|
|
551
|
+
return "(unparseable override)";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function isLoopbackOrigin(origin) {
|
|
555
|
+
try {
|
|
556
|
+
const host = new URL(origin).hostname;
|
|
557
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
558
|
+
} catch {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function localApiAllowed(env) {
|
|
563
|
+
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
564
|
+
}
|
|
565
|
+
function resolveApiBase(env = process.env) {
|
|
566
|
+
for (const key of ENV_KEYS) {
|
|
567
|
+
const raw = env[key];
|
|
568
|
+
if (typeof raw !== "string") continue;
|
|
569
|
+
const trimmed = raw.trim();
|
|
570
|
+
if (trimmed === "") continue;
|
|
571
|
+
const normalized = normalizeOverride(trimmed);
|
|
572
|
+
if (normalized === null) {
|
|
573
|
+
throw new ApiBaseError(
|
|
574
|
+
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
578
|
+
throw new ApiBaseError(
|
|
579
|
+
`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.`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return normalized;
|
|
583
|
+
}
|
|
584
|
+
return PROD_API_BASE;
|
|
585
|
+
}
|
|
586
|
+
function resolveApiBaseOrProd(env = process.env) {
|
|
587
|
+
try {
|
|
588
|
+
return resolveApiBase(env);
|
|
589
|
+
} catch {
|
|
590
|
+
return PROD_API_BASE;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function normalizeOverride(raw) {
|
|
594
|
+
let url;
|
|
595
|
+
try {
|
|
596
|
+
url = new URL(raw);
|
|
597
|
+
} catch {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
601
|
+
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
602
|
+
if (expectedProtocol === void 0) return null;
|
|
603
|
+
if (url.protocol !== expectedProtocol) return null;
|
|
604
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
608
|
+
if (rewrite !== void 0) return rewrite;
|
|
609
|
+
return url.origin;
|
|
610
|
+
}
|
|
611
|
+
function isNonProdApiBase(base = resolveApiBaseOrProd()) {
|
|
612
|
+
return base !== PROD_API_BASE;
|
|
613
|
+
}
|
|
614
|
+
function warnSharedCredentialsIfNonProd(base, stream = process.stderr) {
|
|
615
|
+
if (!isNonProdApiBase(base)) return;
|
|
616
|
+
try {
|
|
617
|
+
stream.write(
|
|
618
|
+
"terminalhire: non-prod API base \u2014 using the same local session/push credentials as prod; do not mix environments casually.\n"
|
|
619
|
+
);
|
|
620
|
+
} catch {
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// bin/claim-push-bg.js
|
|
512
625
|
var TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
|
|
513
626
|
var CLAIM_PUSH_AUTO_MARKER = join5(TERMINALHIRE_DIR4, "claim-push-auto.json");
|
|
514
627
|
var CLAIM_PUSH_TOKEN_FILE = join5(TERMINALHIRE_DIR4, "claim-push-token.enc");
|
|
515
628
|
var CLAIM_PUSH_MANUAL_MARKER = join5(TERMINALHIRE_DIR4, "claim-push.json");
|
|
516
|
-
var CLAIM_SYNC_BASE =
|
|
629
|
+
var CLAIM_SYNC_BASE = resolveApiBase();
|
|
630
|
+
warnSharedCredentialsIfNonProd(CLAIM_SYNC_BASE);
|
|
517
631
|
var AUTO_CONSENT_VERSION = 3;
|
|
518
632
|
var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
519
633
|
async function writePushTokenEnc(rawToken) {
|
package/dist/bin/directory.js
CHANGED
|
@@ -53,12 +53,104 @@ function ensureStateDir(dir) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// src/api-base.ts
|
|
57
|
+
var PROD_API_BASE = "https://terminalhire.com";
|
|
58
|
+
var DEV_API_BASE = "https://dev.terminalhire.com";
|
|
59
|
+
var ApiBaseError = class extends Error {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "ApiBaseError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var ALLOWED_HOSTS = {
|
|
66
|
+
"terminalhire.com": "https:",
|
|
67
|
+
"www.terminalhire.com": "https:",
|
|
68
|
+
"dev.terminalhire.com": "https:",
|
|
69
|
+
localhost: "http:",
|
|
70
|
+
"127.0.0.1": "http:"
|
|
71
|
+
};
|
|
72
|
+
var ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
73
|
+
var ALLOWED_DESCRIPTION = [
|
|
74
|
+
PROD_API_BASE,
|
|
75
|
+
DEV_API_BASE,
|
|
76
|
+
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
77
|
+
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
78
|
+
].join(", ");
|
|
79
|
+
var CANONICAL_REWRITES = {
|
|
80
|
+
"www.terminalhire.com": PROD_API_BASE
|
|
81
|
+
};
|
|
82
|
+
var ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
83
|
+
function sanitizeOverrideForError(raw) {
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(raw);
|
|
86
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
87
|
+
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
88
|
+
}
|
|
89
|
+
if (url.username !== "" || url.password !== "") {
|
|
90
|
+
return `${url.protocol}//***@${url.host}`;
|
|
91
|
+
}
|
|
92
|
+
return url.origin;
|
|
93
|
+
} catch {
|
|
94
|
+
return "(unparseable override)";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function isLoopbackOrigin(origin) {
|
|
98
|
+
try {
|
|
99
|
+
const host = new URL(origin).hostname;
|
|
100
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function localApiAllowed(env) {
|
|
106
|
+
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
107
|
+
}
|
|
108
|
+
function resolveApiBase(env = process.env) {
|
|
109
|
+
for (const key of ENV_KEYS) {
|
|
110
|
+
const raw = env[key];
|
|
111
|
+
if (typeof raw !== "string") continue;
|
|
112
|
+
const trimmed = raw.trim();
|
|
113
|
+
if (trimmed === "") continue;
|
|
114
|
+
const normalized = normalizeOverride(trimmed);
|
|
115
|
+
if (normalized === null) {
|
|
116
|
+
throw new ApiBaseError(
|
|
117
|
+
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
121
|
+
throw new ApiBaseError(
|
|
122
|
+
`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.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
return PROD_API_BASE;
|
|
128
|
+
}
|
|
129
|
+
function normalizeOverride(raw) {
|
|
130
|
+
let url;
|
|
131
|
+
try {
|
|
132
|
+
url = new URL(raw);
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
137
|
+
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
138
|
+
if (expectedProtocol === void 0) return null;
|
|
139
|
+
if (url.protocol !== expectedProtocol) return null;
|
|
140
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
144
|
+
if (rewrite !== void 0) return rewrite;
|
|
145
|
+
return url.origin;
|
|
146
|
+
}
|
|
147
|
+
|
|
56
148
|
// bin/directory.js
|
|
57
149
|
var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
|
|
58
150
|
var DIRECTORY_CACHE_FILE = join(TERMINALHIRE_DIR, "directory-cache.json");
|
|
59
151
|
var PROJECT_FILE = join(TERMINALHIRE_DIR, "project.json");
|
|
60
152
|
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
61
|
-
var API_URL =
|
|
153
|
+
var API_URL = resolveApiBase();
|
|
62
154
|
function readDirectoryCache() {
|
|
63
155
|
try {
|
|
64
156
|
const entry = JSON.parse(readFileSync(DIRECTORY_CACHE_FILE, "utf8"));
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/api-base.ts
|
|
2
|
+
var PROD_API_BASE = "https://terminalhire.com";
|
|
3
|
+
var DEV_API_BASE = "https://dev.terminalhire.com";
|
|
4
|
+
var ApiBaseError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ApiBaseError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var ALLOWED_HOSTS = {
|
|
11
|
+
"terminalhire.com": "https:",
|
|
12
|
+
"www.terminalhire.com": "https:",
|
|
13
|
+
"dev.terminalhire.com": "https:",
|
|
14
|
+
localhost: "http:",
|
|
15
|
+
"127.0.0.1": "http:"
|
|
16
|
+
};
|
|
17
|
+
var ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
18
|
+
var ALLOWED_DESCRIPTION = [
|
|
19
|
+
PROD_API_BASE,
|
|
20
|
+
DEV_API_BASE,
|
|
21
|
+
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
22
|
+
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
23
|
+
].join(", ");
|
|
24
|
+
var CANONICAL_REWRITES = {
|
|
25
|
+
"www.terminalhire.com": PROD_API_BASE
|
|
26
|
+
};
|
|
27
|
+
var ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
28
|
+
function sanitizeOverrideForError(raw) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(raw);
|
|
31
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
32
|
+
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
33
|
+
}
|
|
34
|
+
if (url.username !== "" || url.password !== "") {
|
|
35
|
+
return `${url.protocol}//***@${url.host}`;
|
|
36
|
+
}
|
|
37
|
+
return url.origin;
|
|
38
|
+
} catch {
|
|
39
|
+
return "(unparseable override)";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isLoopbackOrigin(origin) {
|
|
43
|
+
try {
|
|
44
|
+
const host = new URL(origin).hostname;
|
|
45
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function localApiAllowed(env) {
|
|
51
|
+
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
52
|
+
}
|
|
53
|
+
function resolveApiBase(env = process.env) {
|
|
54
|
+
for (const key of ENV_KEYS) {
|
|
55
|
+
const raw = env[key];
|
|
56
|
+
if (typeof raw !== "string") continue;
|
|
57
|
+
const trimmed = raw.trim();
|
|
58
|
+
if (trimmed === "") continue;
|
|
59
|
+
const normalized = normalizeOverride(trimmed);
|
|
60
|
+
if (normalized === null) {
|
|
61
|
+
throw new ApiBaseError(
|
|
62
|
+
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
66
|
+
throw new ApiBaseError(
|
|
67
|
+
`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.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return normalized;
|
|
71
|
+
}
|
|
72
|
+
return PROD_API_BASE;
|
|
73
|
+
}
|
|
74
|
+
function normalizeOverride(raw) {
|
|
75
|
+
let url;
|
|
76
|
+
try {
|
|
77
|
+
url = new URL(raw);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
82
|
+
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
83
|
+
if (expectedProtocol === void 0) return null;
|
|
84
|
+
if (url.protocol !== expectedProtocol) return null;
|
|
85
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
89
|
+
if (rewrite !== void 0) return rewrite;
|
|
90
|
+
return url.origin;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// bin/founder-note-sync.js
|
|
94
|
+
var CLAIM_SYNC_BASE = resolveApiBase();
|
|
95
|
+
async function fetchFounderNotes(pushToken, fetchImpl = fetch) {
|
|
96
|
+
if (typeof pushToken !== "string" || pushToken.length === 0) return null;
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetchImpl(`${CLAIM_SYNC_BASE}/api/claim/notes`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ pushToken }),
|
|
102
|
+
signal: AbortSignal.timeout(15e3)
|
|
103
|
+
});
|
|
104
|
+
if (!res?.ok) return null;
|
|
105
|
+
const body = await res.json();
|
|
106
|
+
if (!body || !Array.isArray(body.notes)) return null;
|
|
107
|
+
const notes = body.notes.filter(
|
|
108
|
+
(n) => n && typeof n.claimId === "string" && n.claimId !== "" && typeof n.note === "string" && n.note !== "" && typeof n.at === "string" && (n.kind === "founder_note" || n.kind === "feedback")
|
|
109
|
+
);
|
|
110
|
+
return { notes, latestAt: typeof body.latestAt === "string" ? body.latestAt : null };
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function noteKey(note) {
|
|
116
|
+
return `${note.claimId}@${note.at}`;
|
|
117
|
+
}
|
|
118
|
+
function computeFounderNotes(notes, previous) {
|
|
119
|
+
const keys = Array.isArray(notes) ? notes.map(noteKey) : [];
|
|
120
|
+
const prior = previous && Array.isArray(previous.seen) ? new Set(previous.seen) : /* @__PURE__ */ new Set();
|
|
121
|
+
const seen = keys.filter((k) => prior.has(k));
|
|
122
|
+
return { count: keys.length - seen.length, seen };
|
|
123
|
+
}
|
|
124
|
+
function acknowledgeFounderNotes(notes) {
|
|
125
|
+
const keys = Array.isArray(notes) ? notes.map(noteKey) : [];
|
|
126
|
+
return { count: 0, seen: keys };
|
|
127
|
+
}
|
|
128
|
+
async function syncFounderNotes({
|
|
129
|
+
readAutoMarker,
|
|
130
|
+
readPushTokenEnc,
|
|
131
|
+
readPrevious,
|
|
132
|
+
fetchImpl = fetch,
|
|
133
|
+
timeoutMs = 15e3
|
|
134
|
+
} = {}) {
|
|
135
|
+
try {
|
|
136
|
+
const marker = readAutoMarker();
|
|
137
|
+
const token = await readPushTokenEnc();
|
|
138
|
+
if (!marker || !token) return null;
|
|
139
|
+
const answer = await fetchFounderNotes(
|
|
140
|
+
token,
|
|
141
|
+
(url, init) => fetchImpl(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
|
|
142
|
+
);
|
|
143
|
+
if (!answer) return null;
|
|
144
|
+
return computeFounderNotes(answer.notes, readPrevious());
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function formatNote(note) {
|
|
150
|
+
const label = note.kind === "feedback" ? "changes requested" : "note";
|
|
151
|
+
return ` ${note.at.slice(0, 16).replace("T", " ")} [${label}] ${note.claimId}
|
|
152
|
+
${note.note.split("\n").join("\n ")}`;
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
acknowledgeFounderNotes,
|
|
156
|
+
computeFounderNotes,
|
|
157
|
+
fetchFounderNotes,
|
|
158
|
+
formatNote,
|
|
159
|
+
noteKey,
|
|
160
|
+
syncFounderNotes
|
|
161
|
+
};
|