@bunshin-cc/bunshin 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/v2/gitremote.ts
4
- import fs from "node:fs";
4
+ import fs3 from "node:fs";
5
5
  import os from "node:os";
6
- import path from "node:path";
6
+ import path2 from "node:path";
7
7
  import readline from "node:readline";
8
8
  import { spawnSync } from "node:child_process";
9
9
 
10
+ // src/trust.ts
11
+ import fs2 from "node:fs";
12
+ import path from "node:path";
13
+
10
14
  // src/util.ts
15
+ import fs from "node:fs";
16
+ function readJson(file, fallback) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(file, "utf8"));
19
+ } catch {
20
+ return fallback;
21
+ }
22
+ }
11
23
  var STOPWORDS = new Set(
12
24
  "a an the and or but if then else of to in on at for from by with without as is are was were be been being do does did done have has had can could should would will shall may might must this that these those it its i we you they he she them us our your my me not no yes so just also very really please now new use using make get set add fix run change update create write need want like about into over under out up down how what when where why which who whom there here some any all each every more most less least than too own same other".split(" ")
13
25
  );
@@ -15,6 +27,50 @@ function homeDir() {
15
27
  return process.env.HOME || process.env.USERPROFILE || "/";
16
28
  }
17
29
 
30
+ // src/trust.ts
31
+ var HOSTED_API = "https://api.bunshin.cc";
32
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
33
+ var configDir = () => path.join(homeDir(), ".config/bunshin");
34
+ var trustFile = () => path.join(configDir(), "trusted-stores.json");
35
+ var credentialDir = () => path.join(configDir(), "credentials");
36
+ function secureUrl(raw) {
37
+ try {
38
+ const url = new URL(raw ?? "");
39
+ return url.protocol === "https:" || url.protocol === "http:" && LOOPBACK.has(url.hostname);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ function urlOrigin(raw) {
45
+ try {
46
+ return new URL(raw ?? "").origin;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function trustedStores() {
52
+ const list = readJson(trustFile(), {}).stores;
53
+ return Array.isArray(list) ? list.filter((x) => typeof x === "string") : [];
54
+ }
55
+ function trustedOrigins() {
56
+ const out = /* @__PURE__ */ new Set([HOSTED_API]);
57
+ for (const root of trustedStores()) {
58
+ const url = readJson(path.join(root, "config.json"), {}).apiUrl;
59
+ if (url) out.add(url);
60
+ }
61
+ for (const url of Object.keys(readJson(path.join(configDir(), "git-credentials.json"), {}))) out.add(url);
62
+ let files = [];
63
+ try {
64
+ files = fs2.readdirSync(credentialDir()).filter((f) => f.endsWith(".json"));
65
+ } catch {
66
+ }
67
+ for (const f of files) {
68
+ const url = readJson(path.join(credentialDir(), f), {}).apiUrl;
69
+ if (url) out.add(url);
70
+ }
71
+ return new Set([...out].map(urlOrigin).filter((x) => !!x));
72
+ }
73
+
18
74
  // src/v2/gitremote.ts
19
75
  var ZERO_SHA = "0".repeat(40);
20
76
  var REMOTE_PATH = "/v1/git/repos/";
@@ -25,27 +81,30 @@ function parseRemoteUrl(url) {
25
81
  const apiUrl = raw.slice(0, i);
26
82
  const repo = decodeURIComponent(raw.slice(i + REMOTE_PATH.length));
27
83
  if (!/^https?:\/\//.test(apiUrl) || repo.split("/").length !== 2) throw new Error(`not a Bunshin repository address: ${url}`);
84
+ if (!secureUrl(apiUrl)) throw new Error(`refusing ${apiUrl}: Bunshin repositories need https (http only on localhost)`);
28
85
  return { apiUrl, repo };
29
86
  }
30
- var gitCredentialFile = () => path.join(homeDir(), ".config/bunshin/git-credentials.json");
87
+ var gitCredentialFile = () => path2.join(homeDir(), ".config/bunshin/git-credentials.json");
31
88
  function resolveGitKey(apiUrl) {
32
89
  const base = apiUrl.replace(/\/$/, "");
33
- if (process.env.BUNSHIN_API_KEY) return process.env.BUNSHIN_API_KEY;
90
+ if (!secureUrl(base)) return void 0;
91
+ const origin = urlOrigin(base);
92
+ if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
34
93
  try {
35
- const all = JSON.parse(fs.readFileSync(gitCredentialFile(), "utf8"));
94
+ const all = JSON.parse(fs3.readFileSync(gitCredentialFile(), "utf8"));
36
95
  if (all[base]) return all[base];
37
96
  } catch {
38
97
  }
39
- const dir = path.join(homeDir(), ".config/bunshin/credentials");
98
+ const dir = path2.join(homeDir(), ".config/bunshin/credentials");
40
99
  let files = [];
41
100
  try {
42
- files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
101
+ files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
43
102
  } catch {
44
103
  return void 0;
45
104
  }
46
105
  for (const f of files.sort()) {
47
106
  try {
48
- const c = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
107
+ const c = JSON.parse(fs3.readFileSync(path2.join(dir, f), "utf8"));
49
108
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
50
109
  } catch {
51
110
  }
@@ -171,7 +230,7 @@ async function fetchObjects(client, repo, wanted, log = () => {
171
230
  const have = presentObjects([...new Set(meta.flatMap((b) => b.tips))]);
172
231
  const needed = meta.filter((b) => b.tips.some((t) => !have.has(t)));
173
232
  if (!needed.length) throw new Error("the server has no bundle with the missing objects");
174
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-git-"));
233
+ const tmp = fs3.mkdtempSync(path2.join(os.tmpdir(), "bunshin-git-"));
175
234
  try {
176
235
  let after = needed[0].seq - 1;
177
236
  const want = new Set(needed.map((b) => b.seq));
@@ -181,10 +240,10 @@ async function fetchObjects(client, repo, wanted, log = () => {
181
240
  for (const b of page.bundles) {
182
241
  after = b.seq;
183
242
  if (!want.has(b.seq) || !b.data) continue;
184
- const file = path.join(tmp, `${b.seq}.bundle`);
185
- fs.writeFileSync(file, Buffer.from(b.data, "base64"));
243
+ const file = path2.join(tmp, `${b.seq}.bundle`);
244
+ fs3.writeFileSync(file, Buffer.from(b.data, "base64"));
186
245
  git(["bundle", "unbundle", file]);
187
- fs.rmSync(file, { force: true });
246
+ fs3.rmSync(file, { force: true });
188
247
  applied++;
189
248
  }
190
249
  if (!page.more || !page.bundles.length) break;
@@ -192,7 +251,7 @@ async function fetchObjects(client, repo, wanted, log = () => {
192
251
  log(`bunshin: applied ${applied} bundle${applied === 1 ? "" : "s"}
193
252
  `);
194
253
  } finally {
195
- fs.rmSync(tmp, { recursive: true, force: true });
254
+ fs3.rmSync(tmp, { recursive: true, force: true });
196
255
  }
197
256
  const still = wanted.filter((s) => !presentObjects([s]).has(s));
198
257
  if (still.length) throw new Error(`objects still missing after fetch: ${still.slice(0, 3).join(", ")}`);
@@ -246,7 +305,7 @@ async function pushRefs(client, repo, specs) {
246
305
  const sends = updates.filter((u) => u.new !== ZERO_SHA);
247
306
  let bundle = null;
248
307
  if (sends.length) {
249
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-git-"));
308
+ const tmp = fs3.mkdtempSync(path2.join(os.tmpdir(), "bunshin-git-"));
250
309
  const tmpRefs = [];
251
310
  try {
252
311
  sends.forEach((u, i) => {
@@ -255,9 +314,9 @@ async function pushRefs(client, repo, specs) {
255
314
  tmpRefs.push(ref);
256
315
  });
257
316
  const known = presentObjects([...new Set(remote.values())]);
258
- const file = path.join(tmp, "push.bundle");
317
+ const file = path2.join(tmp, "push.bundle");
259
318
  const r = git(["bundle", "create", "--quiet", file, ...tmpRefs, ...[...known].map((k) => `^${k}`)], { allowFail: true });
260
- if (r.ok) bundle = new Uint8Array(fs.readFileSync(file));
319
+ if (r.ok) bundle = new Uint8Array(fs3.readFileSync(file));
261
320
  else if (!/empty bundle/i.test(r.err)) throw new Error(r.err.trim() || "git bundle create failed");
262
321
  else {
263
322
  for (const u of sends) {
@@ -267,7 +326,7 @@ async function pushRefs(client, repo, specs) {
267
326
  }
268
327
  } finally {
269
328
  for (const ref of tmpRefs) git(["update-ref", "-d", ref], { allowFail: true });
270
- fs.rmSync(tmp, { recursive: true, force: true });
329
+ fs3.rmSync(tmp, { recursive: true, force: true });
271
330
  }
272
331
  }
273
332
  if (updates.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {