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.
Files changed (41) hide show
  1. package/README.md +51 -7
  2. package/dist/bin/approved-claims-sync.js +93 -1
  3. package/dist/bin/claim-push-bg.js +115 -1
  4. package/dist/bin/directory.js +93 -1
  5. package/dist/bin/founder-note-sync.js +161 -0
  6. package/dist/bin/founder-verdict-sync.js +201 -0
  7. package/dist/bin/jpi-bounties.js +348 -14
  8. package/dist/bin/jpi-chat-read.js +104 -3
  9. package/dist/bin/jpi-chat.js +119 -9
  10. package/dist/bin/jpi-claim.js +311 -128
  11. package/dist/bin/jpi-contribute.js +93 -1
  12. package/dist/bin/jpi-devs.js +96 -2
  13. package/dist/bin/jpi-dispatch.js +1572 -863
  14. package/dist/bin/jpi-hub.js +114 -8
  15. package/dist/bin/jpi-inbox.js +115 -7
  16. package/dist/bin/jpi-init.js +93 -1
  17. package/dist/bin/jpi-intro.js +100 -1
  18. package/dist/bin/jpi-jobs.js +102 -2
  19. package/dist/bin/jpi-link.js +115 -3
  20. package/dist/bin/jpi-login.js +166 -29
  21. package/dist/bin/jpi-mcp-chat.js +140 -9
  22. package/dist/bin/jpi-mcp.js +332 -140
  23. package/dist/bin/jpi-post.js +93 -1
  24. package/dist/bin/jpi-project.js +93 -1
  25. package/dist/bin/jpi-protocol.js +27 -4
  26. package/dist/bin/jpi-refresh.js +128 -5
  27. package/dist/bin/jpi-run.js +399 -122
  28. package/dist/bin/jpi-sync.js +131 -5
  29. package/dist/bin/jpi-trajectory.js +115 -4
  30. package/dist/bin/jpi-update.js +27 -4
  31. package/dist/bin/peer-connect-prompt.js +121 -7
  32. package/dist/bin/pulse-prompt.js +93 -1
  33. package/dist/bin/recall-check.js +278 -0
  34. package/dist/src/api-base.js +156 -0
  35. package/dist/src/chat-client.js +93 -1
  36. package/dist/src/intro.js +93 -1
  37. package/dist/src/link.js +108 -3
  38. package/dist/src/protocol.js +27 -4
  39. package/dist/src/repo-policy.js +0 -36
  40. package/dist/src/trajectory.js +109 -3
  41. 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 `/api/index` and `/api/lead` (also accepts legacy `JPI_API_URL`) |
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 = "https://terminalhire.com";
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 = "https://terminalhire.com";
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) {
@@ -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 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
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
+ };