@v7-cms/proxy 0.0.0-alpha.9 → 0.0.0-stage

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 (3) hide show
  1. package/README.md +3 -0
  2. package/package.json +4 -44
  3. package/dist/cli.mjs +0 -317
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Temporary Holding Version
2
+
3
+ This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
package/package.json CHANGED
@@ -1,46 +1,6 @@
1
1
  {
2
2
  "name": "@v7-cms/proxy",
3
- "version": "0.0.0-alpha.9",
4
- "type": "module",
5
- "exports": {
6
- ".": "./src/server.ts",
7
- "./http": "./src/http.ts"
8
- },
9
- "bin": {
10
- "v7-cms-proxy": "./dist/cli.mjs"
11
- },
12
- "scripts": {
13
- "check": "tsc --noEmit",
14
- "start": "node dist/cli.mjs",
15
- "build": "node build.mjs"
16
- },
17
- "devDependencies": {
18
- "esbuild": "0.28.2"
19
- },
20
- "files": [
21
- "dist",
22
- "README.md"
23
- ],
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/phishinqi/v7-cms.git",
28
- "directory": "packages/proxy"
29
- },
30
- "homepage": "https://github.com/phishinqi/v7-cms#readme",
31
- "bugs": {
32
- "url": "https://github.com/phishinqi/v7-cms/issues"
33
- },
34
- "publishConfig": {
35
- "access": "public",
36
- "registry": "https://registry.npmjs.org"
37
- },
38
- "description": "Local write proxy for v7-cms, for browsers without the File System Access API.",
39
- "keywords": [
40
- "cms",
41
- "git",
42
- "proxy",
43
- "local",
44
- "v7-cms"
45
- ]
46
- }
3
+ "version": "0.0.0-stage",
4
+ "stub": true,
5
+ "description": "Temporary package placeholder for staged publishing"
6
+ }
package/dist/cli.mjs DELETED
@@ -1,317 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { resolve as resolve2 } from "node:path";
5
-
6
- // src/http.ts
7
- import { createServer } from "node:http";
8
- import { randomBytes } from "node:crypto";
9
-
10
- // src/server.ts
11
- import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
12
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
- var json = (body, status = 200) => ({ status, body });
14
- var LocalProxy = class {
15
- root;
16
- token;
17
- mediaFolder;
18
- constructor(options) {
19
- this.root = resolve(options.root);
20
- this.token = options.token ?? null;
21
- this.mediaFolder = options.mediaFolder ?? "public/images/uploads";
22
- }
23
- async handle(request) {
24
- if (request.method === "OPTIONS") return { status: 204, body: null };
25
- if (request.path === "/api/health") {
26
- return json({ repo: basename(this.root), version: 1 });
27
- }
28
- if (this.token && request.token !== this.token) {
29
- return json({ error: "Missing or wrong proxy token." }, 401);
30
- }
31
- try {
32
- switch (request.path) {
33
- case "/api/v1":
34
- return await this.content(request);
35
- case "/api/media":
36
- return await this.media(request);
37
- default:
38
- return json({ error: "Not found" }, 404);
39
- }
40
- } catch (error) {
41
- const message = error.message;
42
- const status = /outside the repository|escapes/.test(message) ? 403 : 500;
43
- return json({ error: message }, status);
44
- }
45
- }
46
- /**
47
- * One call carries one storage operation. The shape mirrors the GitHub adapter's capabilities
48
- * so the editor cannot tell the two apart.
49
- */
50
- async content(request) {
51
- const body = request.body ?? {};
52
- switch (body["action"]) {
53
- case "info":
54
- return json({ repo: basename(this.root), root: this.root, version: 1 });
55
- case "listDir":
56
- return json({ entries: await this.listDir(String(body["path"] ?? "")) });
57
- case "readFile":
58
- return json({ file: await this.readFile(String(body["path"])) });
59
- case "exists":
60
- return json({ exists: await this.exists(String(body["path"])) });
61
- case "writeFile":
62
- return json({ result: await this.writeFile(body) });
63
- case "deleteFile":
64
- await this.deleteFile(String(body["path"]));
65
- return json({ ok: true });
66
- default:
67
- return json({ error: `Unknown action "${String(body["action"])}"` }, 400);
68
- }
69
- }
70
- async listDir(path) {
71
- const full = await this.resolve(path);
72
- const entries = await readdir(full, { withFileTypes: true }).catch(() => []);
73
- return entries.filter((entry) => !entry.name.startsWith(".")).map((entry) => ({
74
- name: entry.name,
75
- type: entry.isDirectory() ? "dir" : "file"
76
- })).sort((a, b) => a.name.localeCompare(b.name));
77
- }
78
- async readFile(path) {
79
- const full = await this.resolve(path);
80
- const info = await stat(full).catch(() => null);
81
- if (!info?.isFile()) throw new Error(`Not found: ${path}`);
82
- const buffer = await readFile(full);
83
- const isText = /\.(md|mdx|json|ya?ml|txt|css|js|ts|astro)$/i.test(path);
84
- return {
85
- text: isText ? buffer.toString("utf8") : void 0,
86
- base64: isText ? void 0 : buffer.toString("base64"),
87
- size: info.size
88
- };
89
- }
90
- async exists(path) {
91
- const full = await this.resolve(path);
92
- return stat(full).then(
93
- (info) => info.isFile(),
94
- () => false
95
- );
96
- }
97
- async writeFile(body) {
98
- const path = String(body["path"]);
99
- const full = await this.resolve(path, { forWrite: true });
100
- await mkdir(dirname(full), { recursive: true });
101
- const contents = typeof body["base64"] === "string" ? Buffer.from(body["base64"], "base64") : String(body["text"] ?? "");
102
- await writeFile(full, contents);
103
- const info = await stat(full);
104
- return { size: info.size, mtime: info.mtimeMs };
105
- }
106
- async deleteFile(path) {
107
- const full = await this.resolve(path, { forWrite: true });
108
- const info = await stat(full).catch(() => null);
109
- if (!info) throw new Error(`Not found: ${path}`);
110
- await rm(full);
111
- }
112
- async media(request) {
113
- if (request.method === "GET") {
114
- const folder = await this.resolve(this.mediaFolder);
115
- const names = await readdir(folder).catch(() => []);
116
- return json({
117
- assets: names.map((name2) => ({
118
- path: `${this.mediaFolder}/${name2}`,
119
- url: `/${this.mediaFolder}/${name2}`,
120
- name: name2
121
- })),
122
- cursor: null
123
- });
124
- }
125
- const body = request.body ?? {};
126
- const name = String(body["name"] ?? "");
127
- if (!name || /[\\/]/.test(name)) return json({ error: "Invalid file name" }, 400);
128
- const path = `${this.mediaFolder}/${name}`;
129
- await this.writeFile({ path, base64: body["base64"] });
130
- return json({ path, url: `/${path}`, name }, 201);
131
- }
132
- /**
133
- * Resolve a repository-relative path to an absolute one, refusing anything that leaves the root.
134
- * `forWrite` also allows the file itself not to exist yet, while still checking its parent.
135
- */
136
- async resolve(path, options = {}) {
137
- if (isAbsolute(path)) throw new Error(`${path} is outside the repository.`);
138
- const full = resolve(this.root, path);
139
- const rel = relative(this.root, full);
140
- if (rel.startsWith("..") || isAbsolute(rel)) {
141
- throw new Error(`${path} is outside the repository.`);
142
- }
143
- if (!options.forWrite) {
144
- const real = await realpath(full).catch(() => null);
145
- if (real) {
146
- const realRel = relative(await realpath(this.root), real);
147
- if (realRel.startsWith("..") || isAbsolute(realRel)) {
148
- throw new Error(`${path} escapes the repository.`);
149
- }
150
- return real;
151
- }
152
- }
153
- return full;
154
- }
155
- };
156
- var basename = (path) => path.split(sep).filter(Boolean).pop() ?? path;
157
- var TOKEN_HEADER = "x-v7-cms-token";
158
-
159
- // src/http.ts
160
- async function startProxy(options) {
161
- const token = options.token ?? randomBytes(24).toString("hex");
162
- const host = options.host ?? "127.0.0.1";
163
- const proxy = new LocalProxy({
164
- root: options.root,
165
- token,
166
- ...options.mediaFolder ? { mediaFolder: options.mediaFolder } : {}
167
- });
168
- const allowed = new Set(options.allowedOrigins ?? []);
169
- const server = createServer((request, response) => {
170
- void handle(request, response, proxy, allowed);
171
- });
172
- await new Promise((resolve3, reject) => {
173
- server.once("error", reject);
174
- server.listen(options.port ?? 0, host, resolve3);
175
- });
176
- const address = server.address();
177
- const port = typeof address === "object" && address ? address.port : options.port ?? 0;
178
- return {
179
- server,
180
- port,
181
- token,
182
- url: `http://${host}:${port}`,
183
- close: () => new Promise(
184
- (resolve3, reject) => server.close((error) => error ? reject(error) : resolve3())
185
- )
186
- };
187
- }
188
- async function handle(request, response, proxy, allowed) {
189
- const origin = request.headers.origin;
190
- if (origin) {
191
- const permitted = allowed.has(origin) || /^http:\/\/(localhost|127\.0\.0\.1|\[::1\]):\d+$/.test(origin);
192
- if (!permitted) {
193
- response.writeHead(403, { "Content-Type": "application/json" });
194
- response.end(JSON.stringify({ error: "Origin not allowed" }));
195
- return;
196
- }
197
- response.setHeader("Access-Control-Allow-Origin", origin);
198
- response.setHeader("Vary", "Origin");
199
- response.setHeader(
200
- "Access-Control-Allow-Headers",
201
- `Content-Type, ${TOKEN_HEADER}, Authorization`
202
- );
203
- response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
204
- }
205
- const url = new URL(request.url ?? "/", "http://localhost");
206
- const body = await readBody(request);
207
- const header = request.headers[TOKEN_HEADER];
208
- const bearer = request.headers.authorization?.replace(/^Bearer /, "");
209
- const result = await proxy.handle({
210
- method: request.method ?? "GET",
211
- path: url.pathname,
212
- query: url.search.replace(/^\?/, ""),
213
- body,
214
- ...typeof header === "string" ? { token: header } : typeof bearer === "string" ? { token: bearer } : {}
215
- });
216
- response.writeHead(result.status, {
217
- "Content-Type": "application/json",
218
- "Cache-Control": "no-store"
219
- });
220
- response.end(JSON.stringify(result.body ?? null));
221
- }
222
- var MAX_BODY = 32 * 1024 * 1024;
223
- function readBody(request) {
224
- if (request.method === "GET" || request.method === "OPTIONS") return Promise.resolve(void 0);
225
- return new Promise((resolve3) => {
226
- const chunks = [];
227
- let size = 0;
228
- request.on("data", (chunk) => {
229
- size += chunk.length;
230
- if (size > MAX_BODY) {
231
- request.destroy();
232
- resolve3(void 0);
233
- return;
234
- }
235
- chunks.push(chunk);
236
- });
237
- request.on("end", () => {
238
- const text = Buffer.concat(chunks).toString("utf8");
239
- if (!text) return resolve3(void 0);
240
- try {
241
- resolve3(JSON.parse(text));
242
- } catch {
243
- resolve3(void 0);
244
- }
245
- });
246
- request.on("error", () => resolve3(void 0));
247
- });
248
- }
249
-
250
- // src/cli.ts
251
- async function main(argv = process.argv.slice(2)) {
252
- const args = /* @__PURE__ */ new Map();
253
- for (let index = 0; index < argv.length; index += 1) {
254
- const token2 = argv[index];
255
- if (!token2.startsWith("--")) continue;
256
- const next = argv[index + 1];
257
- if (next && !next.startsWith("--")) {
258
- args.set(token2, next);
259
- index += 1;
260
- } else {
261
- args.set(token2, true);
262
- }
263
- }
264
- if (args.has("--help") || args.has("-h")) {
265
- process.stdout.write(
266
- [
267
- "v7-cms local proxy",
268
- "",
269
- "Usage: v7-cms-proxy [--root <dir>] [--port <n>] [--host <addr>] [--token <token>]",
270
- "",
271
- " --root Repository root the editor may read and write. Default: the current directory.",
272
- " --port Port to listen on. Default: 5177.",
273
- " --host Interface to bind. Default: 127.0.0.1. Do not expose this to a network.",
274
- " --token Fixed token. Default: a new one each run.",
275
- ""
276
- ].join("\n")
277
- );
278
- return;
279
- }
280
- const root = resolve2(
281
- typeof args.get("--root") === "string" ? args.get("--root") : "."
282
- );
283
- const port = Number(typeof args.get("--port") === "string" ? args.get("--port") : 5177);
284
- const host = typeof args.get("--host") === "string" ? args.get("--host") : "127.0.0.1";
285
- const token = typeof args.get("--token") === "string" ? args.get("--token") : void 0;
286
- if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
287
- process.stderr.write(
288
- `Refusing to bind ${host}: this proxy can write files and must stay on loopback.
289
- `
290
- );
291
- process.exitCode = 1;
292
- return;
293
- }
294
- const proxy = await startProxy({ root, port, host, ...token ? { token } : {} });
295
- process.stdout.write(
296
- [
297
- "",
298
- `v7-cms local proxy listening on ${proxy.url}`,
299
- ` root: ${root}`,
300
- ` token: ${proxy.token}`,
301
- "",
302
- "Paste the URL and token into the editor. Press Ctrl+C to stop.",
303
- ""
304
- ].join("\n")
305
- );
306
- const stop = () => {
307
- void proxy.close().then(() => process.exit(0));
308
- };
309
- process.on("SIGINT", stop);
310
- process.on("SIGTERM", stop);
311
- }
312
- if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").split("/").pop())) {
313
- await main();
314
- }
315
- export {
316
- main
317
- };