@lizard-build/cli 4.0.60 → 4.0.62
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/commands/git-hosting.js +142 -5
- package/dist/commands/git-hosting.js.map +1 -1
- package/dist/lib/lizard-git.d.ts +5 -0
- package/dist/lib/lizard-git.js +28 -5
- package/dist/lib/lizard-git.js.map +1 -1
- package/dist/lib/skills-data.generated.js +1 -1
- package/dist/lib/skills-data.generated.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/skill-data/core/SKILL.md +1 -1
- package/src/commands/git-hosting.ts +134 -4
- package/src/lib/lizard-git.ts +26 -5
- package/src/lib/updater.ts +1 -1
- package/test/unit/lizard-git.test.ts +18 -1
package/src/lib/lizard-git.ts
CHANGED
|
@@ -11,8 +11,23 @@ import { apiErrorFrom, clientHeaders, getRequestToken, APIError } from "./api.js
|
|
|
11
11
|
|
|
12
12
|
export const DEFAULT_GIT_URL = "https://git.lizard.build";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Where Lizard Git is. LIZARD_GIT_URL must be https (or http on this machine): the Lizard
|
|
16
|
+
* sign-in goes there with every request.
|
|
17
|
+
*/
|
|
14
18
|
export function gitURL(): string {
|
|
15
|
-
|
|
19
|
+
const u = (process.env.LIZARD_GIT_URL || DEFAULT_GIT_URL).replace(/\/+$/, "");
|
|
20
|
+
let parsed: URL;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(u);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new APIError(400, `LIZARD_GIT_URL is not a URL: ${u}`);
|
|
25
|
+
}
|
|
26
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
27
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
|
|
28
|
+
throw new APIError(400, `LIZARD_GIT_URL must be https:// (your Lizard sign-in is sent there), got ${u}`);
|
|
29
|
+
}
|
|
30
|
+
return u;
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
/** The credential Lizard Git gets: LIZARD_GIT_TOKEN, else the CLI's sign-in. */
|
|
@@ -61,7 +76,12 @@ export function qs(path: string, values: Record<string, string | number | boolea
|
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
export const seg = encodeURIComponent;
|
|
64
|
-
|
|
79
|
+
/** A branch or file path, each part escaped; . and .. parts refused (they'd reach other URLs). */
|
|
80
|
+
export const segs = (p: string) => {
|
|
81
|
+
const parts = p.split("/");
|
|
82
|
+
if (parts.some((x) => x === "" || x === "." || x === "..")) throw new APIError(400, `bad path: ${p}`);
|
|
83
|
+
return parts.map(seg).join("/");
|
|
84
|
+
};
|
|
65
85
|
|
|
66
86
|
let me: { login: string } | null = null;
|
|
67
87
|
/** Your Lizard Git login. */
|
|
@@ -74,7 +94,7 @@ export async function myLogin(): Promise<string> {
|
|
|
74
94
|
export function repoFromRemote(url: string, base = gitURL()): string | null {
|
|
75
95
|
const host = new URL(base).host;
|
|
76
96
|
const m = url.trim().match(/^(?:https?:\/\/)(?:[^@/]+@)?([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
77
|
-
if (!m || m[1] !== host) return null;
|
|
97
|
+
if (!m || m[1] !== host || [m[2], m[3]].some((x) => x === "." || x === "..")) return null;
|
|
78
98
|
return `${m[2]}/${m[3]}`;
|
|
79
99
|
}
|
|
80
100
|
|
|
@@ -98,8 +118,9 @@ function originRepo(): string | null {
|
|
|
98
118
|
export async function resolveRepo(flag?: string): Promise<string> {
|
|
99
119
|
if (flag) {
|
|
100
120
|
const f = flag.replace(/\.git$/, "");
|
|
101
|
-
|
|
102
|
-
if (/^[^/\s]+$/.test(f)) return
|
|
121
|
+
const ok = (x: string) => /^[A-Za-z0-9._-]+$/.test(x) && x !== "." && x !== "..";
|
|
122
|
+
if (/^[^/\s]+\/[^/\s]+$/.test(f) && f.split("/").every(ok)) return f;
|
|
123
|
+
if (ok(f)) return `${await myLogin()}/${f}`;
|
|
103
124
|
throw new APIError(400, `--repo is owner/name or name, got "${flag}"`);
|
|
104
125
|
}
|
|
105
126
|
const r = originRepo();
|
package/src/lib/updater.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
export const CURRENT_VERSION = "4.0.
|
|
8
|
+
export const CURRENT_VERSION = "4.0.62";
|
|
9
9
|
const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
|
|
10
10
|
const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
|
|
11
11
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import { repoFromRemote, qs, gitURL, helperCommand, resolveRepo, lg } from "../../src/lib/lizard-git.js";
|
|
2
|
+
import { repoFromRemote, qs, gitURL, helperCommand, resolveRepo, lg, segs } from "../../src/lib/lizard-git.js";
|
|
3
3
|
|
|
4
4
|
afterEach(() => {
|
|
5
5
|
vi.unstubAllGlobals();
|
|
@@ -46,3 +46,20 @@ describe("lizard git helpers", () => {
|
|
|
46
46
|
await expect(lg.get("/api/repos/a/b")).rejects.toMatchObject({ status: 404, message: "no such repository" });
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
describe("lizard git safety", () => {
|
|
51
|
+
it("refuses a LIZARD_GIT_URL that would send the sign-in in clear text", () => {
|
|
52
|
+
vi.stubEnv("LIZARD_GIT_URL", "http://evil.example");
|
|
53
|
+
expect(() => gitURL()).toThrow(/https/);
|
|
54
|
+
vi.stubEnv("LIZARD_GIT_URL", "http://127.0.0.1:8080");
|
|
55
|
+
expect(gitURL()).toBe("http://127.0.0.1:8080");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("refuses . and .. in repository names and paths", async () => {
|
|
59
|
+
await expect(resolveRepo("../x")).rejects.toThrow();
|
|
60
|
+
await expect(resolveRepo("a/..")).rejects.toThrow();
|
|
61
|
+
expect(() => segs("../../api/user/tokens")).toThrow(/bad path/);
|
|
62
|
+
expect(segs("src/a b.js")).toBe("src/a%20b.js");
|
|
63
|
+
expect(repoFromRemote("https://git.lizard.build/../x.git")).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|