skillwiki 0.10.110 → 0.10.112
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/chunk-25OXNFDV.js +133 -0
- package/dist/{chunk-R2DJ7IJ5.js → chunk-7POLZOFY.js} +44 -375
- package/dist/chunk-ABRYBO4T.js +275 -0
- package/dist/{chunk-6X6PPQNI.js → chunk-APKDTEVV.js} +280 -446
- package/dist/{chunk-5MV5SSRM.js → chunk-CKQPUHM2.js} +5 -3
- package/dist/chunk-DOHWNPAZ.js +60 -0
- package/dist/chunk-EE7W3GLA.js +55 -0
- package/dist/chunk-FCA3RRNF.js +60 -0
- package/dist/{chunk-KX5Y3GSX.js → chunk-IR2DI4Y5.js} +5 -3
- package/dist/chunk-K3M6OSUW.js +77 -0
- package/dist/{chunk-WPJHDRTP.js → chunk-KJG2CSYD.js} +12 -57
- package/dist/chunk-LOVLZNLX.js +111 -0
- package/dist/chunk-OSL4J7GF.js +32 -0
- package/dist/{chunk-QU2CKXM4.js → chunk-Q6EZF37H.js} +91 -199
- package/dist/chunk-QESNTTVY.js +50 -0
- package/dist/{chunk-NA3TZ4JN.js → chunk-RR6VDWSI.js} +8 -6
- package/dist/chunk-SKOLE6US.js +126 -0
- package/dist/chunk-TKX6PSWZ.js +344 -0
- package/dist/chunk-ZRGZBAVS.js +98 -0
- package/dist/cli.js +359 -305
- package/dist/{index-projection-6XGESSHV.js → index-projection-UNZG5UJZ.js} +5 -3
- package/dist/journal-5IW3TZ5F.js +13 -0
- package/dist/{managed-write-preflight-JPOBVVSU.js → managed-write-preflight-L7IRQ76C.js} +7 -4
- package/dist/mcp-auth-env-X5MI5IX6.js +25 -0
- package/dist/migrate-RO4BJP5T.js +22 -0
- package/dist/mirror-H5BTBAON.js +151 -0
- package/dist/ready-S7F53Q2D.js +9 -0
- package/dist/runtime-VMFYW2K3.js +20 -0
- package/dist/skillwiki-mcp.js +14 -7
- package/dist/sources-AOYP6ROO.js +10 -0
- package/dist/upload-HTE62TOV.js +14 -0
- package/dist/watcher-MGHQD3UI.js +526 -0
- package/docs/clip-outbox.md +119 -0
- package/package.json +5 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/.cursor-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/skills/wiki-init/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/wiki-init/SKILL.md +3 -0
- package/templates/launchd/com.karlchow.skillwiki-clip-outbox.plist +12 -13
- package/templates/launchd/com.karlchow.skillwiki-mirror-sse.plist +20 -0
- package/dist/sources-CLOF5VAT.js +0 -9
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
err,
|
|
4
3
|
extractFrontmatter,
|
|
5
|
-
ok,
|
|
6
4
|
readPage,
|
|
7
5
|
scanSensitiveContent,
|
|
8
6
|
splitFrontmatter
|
|
9
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-TKX6PSWZ.js";
|
|
8
|
+
import {
|
|
9
|
+
err,
|
|
10
|
+
ok
|
|
11
|
+
} from "./chunk-7POLZOFY.js";
|
|
10
12
|
|
|
11
13
|
// src/utils/vault-path-safety.ts
|
|
12
14
|
import { lstatSync, realpathSync } from "fs";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseDotenvFile,
|
|
4
|
+
resolveSkillwikiHome
|
|
5
|
+
} from "./chunk-SKOLE6US.js";
|
|
6
|
+
|
|
7
|
+
// src/utils/mcp-auth-env.ts
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
var MCP_URL_ENV = "SKILLWIKI_MCP_URL";
|
|
10
|
+
var MCP_TOKEN_ENV = "SKILLWIKI_MCP_TOKEN";
|
|
11
|
+
var MCP_HOST_ID_ENV = "SKILLWIKI_HOST_ID";
|
|
12
|
+
function nonEmptyEnvValue(value) {
|
|
13
|
+
if (typeof value !== "string") return void 0;
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
16
|
+
}
|
|
17
|
+
function mcpAuthFromEnv(env) {
|
|
18
|
+
return {
|
|
19
|
+
url: nonEmptyEnvValue(env[MCP_URL_ENV]),
|
|
20
|
+
token: nonEmptyEnvValue(env[MCP_TOKEN_ENV]),
|
|
21
|
+
hostId: nonEmptyEnvValue(env[MCP_HOST_ID_ENV])
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function resolveMcpAuthEnv(input) {
|
|
25
|
+
const processAuth = mcpAuthFromEnv(input.env ?? process.env);
|
|
26
|
+
const dotenv = await parseDotenvFile(join(resolveSkillwikiHome(input.home, input.env), ".skillwiki", ".env"));
|
|
27
|
+
const fileAuth = mcpAuthFromEnv(dotenv);
|
|
28
|
+
return {
|
|
29
|
+
url: processAuth.url ?? fileAuth.url,
|
|
30
|
+
token: processAuth.token ?? fileAuth.token,
|
|
31
|
+
hostId: processAuth.hostId ?? fileAuth.hostId
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function mergeMcpAuthIntoEnv(home, env) {
|
|
35
|
+
const auth = await resolveMcpAuthEnv({ home, env });
|
|
36
|
+
const merged = { ...env };
|
|
37
|
+
if (auth.url && !nonEmptyEnvValue(env[MCP_URL_ENV])) merged[MCP_URL_ENV] = auth.url;
|
|
38
|
+
if (auth.token && !nonEmptyEnvValue(env[MCP_TOKEN_ENV])) merged[MCP_TOKEN_ENV] = auth.token;
|
|
39
|
+
if (auth.hostId && !nonEmptyEnvValue(env[MCP_HOST_ID_ENV])) merged[MCP_HOST_ID_ENV] = auth.hostId;
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
function redactMcpSecret(text, secret) {
|
|
43
|
+
if (!secret || secret.length === 0 || !text.includes(secret)) return text;
|
|
44
|
+
return text.split(secret).join("[REDACTED]");
|
|
45
|
+
}
|
|
46
|
+
function isMcpOnlyLeaf(input) {
|
|
47
|
+
return !input.resolvedPath && Boolean(input.token) && !input.vaultSyncInstalled;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
MCP_URL_ENV,
|
|
52
|
+
MCP_TOKEN_ENV,
|
|
53
|
+
MCP_HOST_ID_ENV,
|
|
54
|
+
nonEmptyEnvValue,
|
|
55
|
+
mcpAuthFromEnv,
|
|
56
|
+
resolveMcpAuthEnv,
|
|
57
|
+
mergeMcpAuthIntoEnv,
|
|
58
|
+
redactMcpSecret,
|
|
59
|
+
isMcpOnlyLeaf
|
|
60
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
err,
|
|
4
|
+
ok
|
|
5
|
+
} from "./chunk-7POLZOFY.js";
|
|
6
|
+
|
|
7
|
+
// src/utils/atomic-write.ts
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { open, readFile, rename, unlink } from "fs/promises";
|
|
10
|
+
import { basename, dirname, join } from "path";
|
|
11
|
+
async function readExisting(path) {
|
|
12
|
+
try {
|
|
13
|
+
return await readFile(path, "utf8");
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code === "ENOENT") return null;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function atomicWriteText(path, text) {
|
|
20
|
+
let existing;
|
|
21
|
+
try {
|
|
22
|
+
existing = await readExisting(path);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
|
|
25
|
+
}
|
|
26
|
+
if (existing === text) return ok({ changed: false, existed: true });
|
|
27
|
+
const tmp = join(
|
|
28
|
+
dirname(path),
|
|
29
|
+
`.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
|
|
30
|
+
);
|
|
31
|
+
try {
|
|
32
|
+
const handle = await open(tmp, "wx");
|
|
33
|
+
try {
|
|
34
|
+
await handle.writeFile(text, "utf8");
|
|
35
|
+
try {
|
|
36
|
+
await handle.sync();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
await handle.close();
|
|
41
|
+
}
|
|
42
|
+
await rename(tmp, path);
|
|
43
|
+
return ok({ changed: true, existed: existing !== null });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
try {
|
|
46
|
+
await unlink(tmp);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
atomicWriteText
|
|
55
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteText
|
|
4
|
+
} from "./chunk-EE7W3GLA.js";
|
|
5
|
+
|
|
6
|
+
// src/clip-outbox/journal.ts
|
|
7
|
+
import { mkdir, readFile, open } from "fs/promises";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
async function readJournal(stateRoot) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = await readFile(join(stateRoot, "journal.json"), "utf8");
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (e.code === "ENOENT") return [];
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
const key = z.string().regex(/^raw\/(articles|assets)\/[A-Za-z0-9._/-]+$/).refine((s) => s.split("/").every((p) => p && p !== "." && p !== ".."));
|
|
19
|
+
const schema = z.object({
|
|
20
|
+
captureId: z.string().regex(/^[a-f0-9]{16,32}$/),
|
|
21
|
+
snapshotId: z.string().regex(/^[a-f0-9]{16}$/).optional(),
|
|
22
|
+
source: z.string().optional(),
|
|
23
|
+
sourceSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
24
|
+
articleKey: key.refine((s) => s.startsWith("raw/articles/")),
|
|
25
|
+
assetKeys: z.array(key.refine((s) => s.startsWith("raw/assets/"))),
|
|
26
|
+
sha256: z.record(z.string().regex(/^[a-f0-9]{64}$/)),
|
|
27
|
+
sizes: z.record(z.number().int().nonnegative()).optional(),
|
|
28
|
+
receipts: z.record(z.enum(["created", "idempotent"])).optional(),
|
|
29
|
+
state: z.enum(["discovered", "preparing", "sealed", "assets-verified", "article-verified", "notify-pending", "central-visible", "mirrored", "conflict", "held"]),
|
|
30
|
+
reason: z.string().optional(),
|
|
31
|
+
attempts: z.number().int().nonnegative().optional(),
|
|
32
|
+
updatedAt: z.string().optional(),
|
|
33
|
+
retryAt: z.number().finite().optional()
|
|
34
|
+
}).refine((e) => !e.sourceSha256 || e.snapshotId && [e.articleKey, ...e.assetKeys].every((k) => e.sha256[k] && e.sizes?.[k] !== void 0));
|
|
35
|
+
const result = z.array(schema).safeParse(JSON.parse(raw));
|
|
36
|
+
if (!result.success) throw new Error("invalid clip journal; retain and inspect it before restarting");
|
|
37
|
+
return result.data;
|
|
38
|
+
}
|
|
39
|
+
async function writeJournal(stateRoot, entries) {
|
|
40
|
+
await mkdir(stateRoot, { recursive: true, mode: 448 });
|
|
41
|
+
const result = await atomicWriteText(join(stateRoot, "journal.json"), JSON.stringify(entries, null, 2));
|
|
42
|
+
if (!result.ok) throw new Error("journal write failed");
|
|
43
|
+
for (const path of [stateRoot, dirname(stateRoot)]) {
|
|
44
|
+
const dir = await open(path, "r");
|
|
45
|
+
try {
|
|
46
|
+
await dir.sync();
|
|
47
|
+
} finally {
|
|
48
|
+
await dir.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function upsertJournal(entries, next) {
|
|
53
|
+
return [...entries.filter((e) => e.captureId !== next.captureId), next];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
readJournal,
|
|
58
|
+
writeJournal,
|
|
59
|
+
upsertJournal
|
|
60
|
+
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ROOT_INDEX_SECTION_ORDER,
|
|
4
|
-
atomicWriteText,
|
|
5
4
|
buildRootIndexUniverse
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KJG2CSYD.js";
|
|
6
|
+
import {
|
|
7
|
+
atomicWriteText
|
|
8
|
+
} from "./chunk-EE7W3GLA.js";
|
|
7
9
|
import {
|
|
8
10
|
err,
|
|
9
11
|
ok
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-7POLZOFY.js";
|
|
11
13
|
|
|
12
14
|
// src/utils/index-projection.ts
|
|
13
15
|
import { readFile } from "fs/promises";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/clip-outbox/files.ts
|
|
4
|
+
import { constants } from "fs";
|
|
5
|
+
import { lstat, mkdir, open, realpath } from "fs/promises";
|
|
6
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
7
|
+
async function readRegular(root, path, maxBytes) {
|
|
8
|
+
const rel = relative(root, path);
|
|
9
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) throw new Error("unsafe input path");
|
|
10
|
+
let current = resolve(root);
|
|
11
|
+
for (const part of ["", ...rel.split(sep)]) {
|
|
12
|
+
current = resolve(current, part);
|
|
13
|
+
const st = await lstat(current);
|
|
14
|
+
if (st.isSymbolicLink()) throw new Error("symlink input refused");
|
|
15
|
+
if (current !== path && !st.isDirectory()) throw new Error("invalid input parent");
|
|
16
|
+
}
|
|
17
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
18
|
+
try {
|
|
19
|
+
const before = await handle.stat();
|
|
20
|
+
if (!before.isFile() || before.size > maxBytes) throw new Error("input type or size refused");
|
|
21
|
+
const bytes = Buffer.alloc(before.size + 1);
|
|
22
|
+
let bytesRead = 0;
|
|
23
|
+
while (bytesRead < bytes.length) {
|
|
24
|
+
const chunk = await handle.read(bytes, bytesRead, bytes.length - bytesRead, bytesRead);
|
|
25
|
+
if (chunk.bytesRead === 0) break;
|
|
26
|
+
bytesRead += chunk.bytesRead;
|
|
27
|
+
}
|
|
28
|
+
const after = await handle.stat();
|
|
29
|
+
if (await realpath(path) !== path) throw new Error("input path changed during read");
|
|
30
|
+
const leaf = await lstat(path);
|
|
31
|
+
if (leaf.ino !== before.ino || leaf.dev !== before.dev) throw new Error("input identity changed during read");
|
|
32
|
+
if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) throw new Error("input changed during read");
|
|
33
|
+
return bytes.subarray(0, bytesRead);
|
|
34
|
+
} finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function writeRegularExclusive(root, rel, body) {
|
|
39
|
+
const dest = resolve(root, ...rel.split("/"));
|
|
40
|
+
const checked = relative(root, dest);
|
|
41
|
+
if (!checked || checked.startsWith(`..${sep}`) || checked === ".." || isAbsolute(checked)) throw new Error("unsafe dest path");
|
|
42
|
+
let current = resolve(root);
|
|
43
|
+
const parts = checked.split(sep);
|
|
44
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
45
|
+
current = resolve(current, parts[i]);
|
|
46
|
+
try {
|
|
47
|
+
const st = await lstat(current);
|
|
48
|
+
if (st.isSymbolicLink()) throw new Error("symlink dest refused");
|
|
49
|
+
if (!st.isDirectory()) throw new Error("invalid dest parent");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== "ENOENT") throw error;
|
|
52
|
+
await mkdir(current);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const leaf = resolve(current, parts[parts.length - 1]);
|
|
56
|
+
if (dirname(leaf) !== current) throw new Error("unsafe dest path");
|
|
57
|
+
try {
|
|
58
|
+
const handle = await open(leaf, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 420);
|
|
59
|
+
try {
|
|
60
|
+
await handle.writeFile(body);
|
|
61
|
+
} finally {
|
|
62
|
+
await handle.close();
|
|
63
|
+
}
|
|
64
|
+
return "created";
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code !== "EEXIST") throw error;
|
|
67
|
+
const st = await lstat(leaf);
|
|
68
|
+
if (st.isSymbolicLink() || !st.isFile() || st.size !== body.length) return "conflict";
|
|
69
|
+
const existing = await readRegular(root, leaf, body.length);
|
|
70
|
+
return existing.equals(body) ? "idempotent" : "conflict";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
readRegular,
|
|
76
|
+
writeRegularExclusive
|
|
77
|
+
};
|
|
@@ -1,67 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
extractFrontmatter,
|
|
4
|
+
scanSensitiveContent,
|
|
5
|
+
scanVault
|
|
6
|
+
} from "./chunk-TKX6PSWZ.js";
|
|
2
7
|
import {
|
|
3
8
|
MetaSchema,
|
|
4
9
|
TypedKnowledgeSchema,
|
|
5
10
|
detectSchema,
|
|
6
11
|
err,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
scanSensitiveContent,
|
|
10
|
-
scanVault
|
|
11
|
-
} from "./chunk-R2DJ7IJ5.js";
|
|
12
|
-
|
|
13
|
-
// src/utils/atomic-write.ts
|
|
14
|
-
import { randomBytes } from "crypto";
|
|
15
|
-
import { open, readFile, rename, unlink } from "fs/promises";
|
|
16
|
-
import { basename, dirname, join } from "path";
|
|
17
|
-
async function readExisting(path) {
|
|
18
|
-
try {
|
|
19
|
-
return await readFile(path, "utf8");
|
|
20
|
-
} catch (error) {
|
|
21
|
-
if (error.code === "ENOENT") return null;
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async function atomicWriteText(path, text) {
|
|
26
|
-
let existing;
|
|
27
|
-
try {
|
|
28
|
-
existing = await readExisting(path);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
|
|
31
|
-
}
|
|
32
|
-
if (existing === text) return ok({ changed: false, existed: true });
|
|
33
|
-
const tmp = join(
|
|
34
|
-
dirname(path),
|
|
35
|
-
`.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
|
|
36
|
-
);
|
|
37
|
-
try {
|
|
38
|
-
const handle = await open(tmp, "wx");
|
|
39
|
-
try {
|
|
40
|
-
await handle.writeFile(text, "utf8");
|
|
41
|
-
try {
|
|
42
|
-
await handle.sync();
|
|
43
|
-
} catch {
|
|
44
|
-
}
|
|
45
|
-
} finally {
|
|
46
|
-
await handle.close();
|
|
47
|
-
}
|
|
48
|
-
await rename(tmp, path);
|
|
49
|
-
return ok({ changed: true, existed: existing !== null });
|
|
50
|
-
} catch (error) {
|
|
51
|
-
try {
|
|
52
|
-
await unlink(tmp);
|
|
53
|
-
} catch {
|
|
54
|
-
}
|
|
55
|
-
return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
12
|
+
ok
|
|
13
|
+
} from "./chunk-7POLZOFY.js";
|
|
58
14
|
|
|
59
15
|
// src/utils/index-universe.ts
|
|
60
|
-
import { readFile
|
|
16
|
+
import { readFile } from "fs/promises";
|
|
61
17
|
|
|
62
18
|
// src/utils/typed-page.ts
|
|
63
19
|
import { lstatSync, realpathSync } from "fs";
|
|
64
|
-
import { dirname
|
|
20
|
+
import { dirname, posix, relative, resolve, sep } from "path";
|
|
65
21
|
var TYPE_DIRECTORY = {
|
|
66
22
|
entity: "entities",
|
|
67
23
|
concept: "concepts",
|
|
@@ -86,7 +42,7 @@ function assertTargetInsideVault(vault, target) {
|
|
|
86
42
|
return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
|
|
87
43
|
}
|
|
88
44
|
const absolutePath = resolve(vaultReal, target);
|
|
89
|
-
const parent =
|
|
45
|
+
const parent = dirname(absolutePath);
|
|
90
46
|
let parentReal;
|
|
91
47
|
try {
|
|
92
48
|
parentReal = realpathSync(parent);
|
|
@@ -201,7 +157,7 @@ async function buildRootIndexUniverse(input) {
|
|
|
201
157
|
for (const page of typedPages) {
|
|
202
158
|
let text;
|
|
203
159
|
try {
|
|
204
|
-
text = await
|
|
160
|
+
text = await readFile(page.absPath, "utf8");
|
|
205
161
|
} catch {
|
|
206
162
|
continue;
|
|
207
163
|
}
|
|
@@ -247,7 +203,7 @@ async function buildRootIndexUniverse(input) {
|
|
|
247
203
|
for (const readme of projectReadmes) {
|
|
248
204
|
let text;
|
|
249
205
|
try {
|
|
250
|
-
text = await
|
|
206
|
+
text = await readFile(readme.absPath, "utf8");
|
|
251
207
|
} catch {
|
|
252
208
|
continue;
|
|
253
209
|
}
|
|
@@ -286,7 +242,6 @@ async function buildRootIndexUniverse(input) {
|
|
|
286
242
|
}
|
|
287
243
|
|
|
288
244
|
export {
|
|
289
|
-
atomicWriteText,
|
|
290
245
|
assertTargetInsideVault,
|
|
291
246
|
prepareTypedPage,
|
|
292
247
|
ROOT_INDEX_SECTION_ORDER,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ExitCode,
|
|
4
|
+
err,
|
|
5
|
+
ipv4InCidr,
|
|
6
|
+
isBlockedHost,
|
|
7
|
+
ok
|
|
8
|
+
} from "./chunk-7POLZOFY.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/fetch-guard.ts
|
|
11
|
+
var REDACT_PARAMS = /* @__PURE__ */ new Set(["api_key", "token", "key", "auth", "password", "secret", "access_token"]);
|
|
12
|
+
var PATH_TOKEN_RE = /[A-Fa-f0-9]{32,}|[A-Za-z0-9_\-]{40,}/g;
|
|
13
|
+
function runFetchGuard(input) {
|
|
14
|
+
return Promise.resolve(runFetchGuardSync(input));
|
|
15
|
+
}
|
|
16
|
+
function runFetchGuardSync(input) {
|
|
17
|
+
let u;
|
|
18
|
+
try {
|
|
19
|
+
u = new URL(input.url);
|
|
20
|
+
} catch {
|
|
21
|
+
return { exitCode: ExitCode.MALFORMED_URL, result: err("MALFORMED_URL", { url: input.url }) };
|
|
22
|
+
}
|
|
23
|
+
const sanitized = sanitizeUrl(u);
|
|
24
|
+
if (u.protocol !== "https:") {
|
|
25
|
+
return {
|
|
26
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
27
|
+
result: err("SCHEME_REJECTED", { sanitized_url: sanitized, scheme: u.protocol })
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (isBlockedHost(u.hostname)) {
|
|
31
|
+
return {
|
|
32
|
+
exitCode: ExitCode.HOST_BLOCKED,
|
|
33
|
+
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
37
|
+
}
|
|
38
|
+
function sanitizeUrl(u) {
|
|
39
|
+
const clone = new URL(u.toString());
|
|
40
|
+
if (clone.username || clone.password) {
|
|
41
|
+
clone.username = "";
|
|
42
|
+
clone.password = "";
|
|
43
|
+
}
|
|
44
|
+
for (const k of Array.from(clone.searchParams.keys())) {
|
|
45
|
+
if (REDACT_PARAMS.has(k.toLowerCase())) clone.searchParams.set(k, "REDACTED");
|
|
46
|
+
}
|
|
47
|
+
let s = clone.toString();
|
|
48
|
+
s = s.replace(PATH_TOKEN_RE, "REDACTED");
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/clip-outbox/ssrf.ts
|
|
53
|
+
import { isIP } from "net";
|
|
54
|
+
import { lookup as dnsLookup } from "dns/promises";
|
|
55
|
+
var extraBlockedV4 = [
|
|
56
|
+
["0.0.0.0", 8],
|
|
57
|
+
["100.64.0.0", 10],
|
|
58
|
+
["192.0.2.0", 24],
|
|
59
|
+
["198.18.0.0", 15],
|
|
60
|
+
["198.51.100.0", 24],
|
|
61
|
+
["203.0.113.0", 24],
|
|
62
|
+
["224.0.0.0", 4],
|
|
63
|
+
["240.0.0.0", 4]
|
|
64
|
+
];
|
|
65
|
+
function isBlockedAddress(address) {
|
|
66
|
+
if (isBlockedHost(address)) return true;
|
|
67
|
+
const version = isIP(address);
|
|
68
|
+
if (version === 4) {
|
|
69
|
+
return extraBlockedV4.some(([base, prefix]) => ipv4InCidr(address, base, prefix));
|
|
70
|
+
}
|
|
71
|
+
if (version === 6) {
|
|
72
|
+
const lower = address.toLowerCase();
|
|
73
|
+
return lower === "::" || lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80:");
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
async function assertPublicHttpsUrl(url, lookup = defaultLookup) {
|
|
78
|
+
const guard = runFetchGuardSync({ url });
|
|
79
|
+
if (!guard.result.ok) {
|
|
80
|
+
return { ok: false, reason: guard.result.error };
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = new URL(url);
|
|
85
|
+
} catch {
|
|
86
|
+
return { ok: false, reason: "MALFORMED_URL" };
|
|
87
|
+
}
|
|
88
|
+
if (isBlockedAddress(parsed.hostname)) {
|
|
89
|
+
return { ok: false, reason: "HOST_BLOCKED" };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const addresses = await lookup(parsed.hostname);
|
|
93
|
+
if (addresses.length === 0) return { ok: false, reason: "DNS_EMPTY" };
|
|
94
|
+
if (addresses.some(isBlockedAddress)) return { ok: false, reason: "HOST_BLOCKED" };
|
|
95
|
+
} catch {
|
|
96
|
+
return { ok: false, reason: "DNS_FAILED" };
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, href: parsed.href };
|
|
99
|
+
}
|
|
100
|
+
async function defaultLookup(hostname) {
|
|
101
|
+
if (isIP(hostname)) return [hostname];
|
|
102
|
+
const v4 = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
103
|
+
return v4.map((row) => row.address);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
runFetchGuard,
|
|
108
|
+
runFetchGuardSync,
|
|
109
|
+
assertPublicHttpsUrl,
|
|
110
|
+
defaultLookup
|
|
111
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/clip-outbox/ready.ts
|
|
4
|
+
function buildReadyHint(vaultId, files) {
|
|
5
|
+
const assets = files.filter((f) => f.path.startsWith("raw/assets/"));
|
|
6
|
+
const articles = files.filter((f) => f.path.startsWith("raw/articles/"));
|
|
7
|
+
return { vault_id: vaultId, files: [...assets, ...articles] };
|
|
8
|
+
}
|
|
9
|
+
async function postReadyHint(opts) {
|
|
10
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
11
|
+
const body = JSON.stringify(opts.hint);
|
|
12
|
+
if (Buffer.byteLength(body) > 32768) return { ok: false, status: 413 };
|
|
13
|
+
const res = await fetchImpl(opts.url, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${opts.token}`,
|
|
17
|
+
"Content-Type": "application/json"
|
|
18
|
+
},
|
|
19
|
+
body,
|
|
20
|
+
signal: AbortSignal.timeout(1e4),
|
|
21
|
+
redirect: "error"
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) return { ok: false, status: res.status };
|
|
24
|
+
const result = await res.json();
|
|
25
|
+
const paths = result.paths;
|
|
26
|
+
return result.ok === true && Array.isArray(paths) && opts.hint.files.every((f) => paths.includes(f.path)) ? { ok: true } : { ok: false, status: 503 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
buildReadyHint,
|
|
31
|
+
postReadyHint
|
|
32
|
+
};
|