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.
Files changed (45) hide show
  1. package/dist/chunk-25OXNFDV.js +133 -0
  2. package/dist/{chunk-R2DJ7IJ5.js → chunk-7POLZOFY.js} +44 -375
  3. package/dist/chunk-ABRYBO4T.js +275 -0
  4. package/dist/{chunk-6X6PPQNI.js → chunk-APKDTEVV.js} +280 -446
  5. package/dist/{chunk-5MV5SSRM.js → chunk-CKQPUHM2.js} +5 -3
  6. package/dist/chunk-DOHWNPAZ.js +60 -0
  7. package/dist/chunk-EE7W3GLA.js +55 -0
  8. package/dist/chunk-FCA3RRNF.js +60 -0
  9. package/dist/{chunk-KX5Y3GSX.js → chunk-IR2DI4Y5.js} +5 -3
  10. package/dist/chunk-K3M6OSUW.js +77 -0
  11. package/dist/{chunk-WPJHDRTP.js → chunk-KJG2CSYD.js} +12 -57
  12. package/dist/chunk-LOVLZNLX.js +111 -0
  13. package/dist/chunk-OSL4J7GF.js +32 -0
  14. package/dist/{chunk-QU2CKXM4.js → chunk-Q6EZF37H.js} +91 -199
  15. package/dist/chunk-QESNTTVY.js +50 -0
  16. package/dist/{chunk-NA3TZ4JN.js → chunk-RR6VDWSI.js} +8 -6
  17. package/dist/chunk-SKOLE6US.js +126 -0
  18. package/dist/chunk-TKX6PSWZ.js +344 -0
  19. package/dist/chunk-ZRGZBAVS.js +98 -0
  20. package/dist/cli.js +359 -305
  21. package/dist/{index-projection-6XGESSHV.js → index-projection-UNZG5UJZ.js} +5 -3
  22. package/dist/journal-5IW3TZ5F.js +13 -0
  23. package/dist/{managed-write-preflight-JPOBVVSU.js → managed-write-preflight-L7IRQ76C.js} +7 -4
  24. package/dist/mcp-auth-env-X5MI5IX6.js +25 -0
  25. package/dist/migrate-RO4BJP5T.js +22 -0
  26. package/dist/mirror-H5BTBAON.js +151 -0
  27. package/dist/ready-S7F53Q2D.js +9 -0
  28. package/dist/runtime-VMFYW2K3.js +20 -0
  29. package/dist/skillwiki-mcp.js +14 -7
  30. package/dist/sources-AOYP6ROO.js +10 -0
  31. package/dist/upload-HTE62TOV.js +14 -0
  32. package/dist/watcher-MGHQD3UI.js +526 -0
  33. package/docs/clip-outbox.md +119 -0
  34. package/package.json +5 -1
  35. package/skills/.claude-plugin/plugin.json +1 -1
  36. package/skills/.codex-plugin/plugin.json +1 -1
  37. package/skills/.cursor-plugin/plugin.json +1 -1
  38. package/skills/package.json +1 -1
  39. package/skills/skills/using-skillwiki/SKILL.md +1 -1
  40. package/skills/skills/wiki-init/SKILL.md +3 -0
  41. package/skills/using-skillwiki/SKILL.md +1 -1
  42. package/skills/wiki-init/SKILL.md +3 -0
  43. package/templates/launchd/com.karlchow.skillwiki-clip-outbox.plist +12 -13
  44. package/templates/launchd/com.karlchow.skillwiki-mirror-sse.plist +20 -0
  45. 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-R2DJ7IJ5.js";
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-WPJHDRTP.js";
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-R2DJ7IJ5.js";
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
- extractFrontmatter,
8
- ok,
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 as readFile2 } from "fs/promises";
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 as dirname2, posix, relative, resolve, sep } from "path";
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 = dirname2(absolutePath);
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 readFile2(page.absPath, "utf8");
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 readFile2(readme.absPath, "utf8");
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
+ };