docnest 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -89,6 +89,8 @@ startServer(3000)
89
89
  如果宿主项目需要生成静态站点,可使用 `docnest/attachments` 提供的
90
90
  `collectDocumentAttachmentPaths` 和 `copyDocumentAttachments`。它们只复制
91
91
  Markdown 或 HTML 中明确引用的本地附件,不按文件扩展名设置业务白名单;外链、文档链接、隐藏路径、路径穿越和代码块中的伪引用会被忽略,缺失的附件默认会让构建失败。
92
+ 常见凭据文件(如 `.env`、私钥和证书容器)属于公共安全阻断项,即使被误写进文档链接也不会进入静态产物。
93
+ 静态生成 HTML 时,可再调用 `rewriteDocumentAttachmentUrls`,将正文中的附件链接统一改写到 `/doc-asset/` 端点。
92
94
 
93
95
  ## 开发
94
96
 
@@ -5,7 +5,13 @@ export type DocumentAttachmentOptions = {
5
5
  */
6
6
  failOnMissing?: boolean;
7
7
  };
8
+ export type DocumentAttachmentUrlOptions = {
9
+ assetPaths?: readonly string[];
10
+ assetPrefix?: string;
11
+ };
8
12
  /** Collect every existing, explicitly referenced local document attachment. */
9
13
  export declare function collectDocumentAttachmentPaths(docsRoot: string, options?: DocumentAttachmentOptions): string[];
10
14
  /** Copy collected attachments while preserving their paths relative to docsRoot. */
11
15
  export declare function copyDocumentAttachments(sourceRoot: string, destinationRoot: string, options?: DocumentAttachmentOptions): string[];
16
+ /** Rewrite rendered local attachment links to the static document asset endpoint. */
17
+ export declare function rewriteDocumentAttachmentUrls(html: string, documentPath: string, options?: DocumentAttachmentUrlOptions): string;
@@ -5,9 +5,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.collectDocumentAttachmentPaths = collectDocumentAttachmentPaths;
7
7
  exports.copyDocumentAttachments = copyDocumentAttachments;
8
+ exports.rewriteDocumentAttachmentUrls = rewriteDocumentAttachmentUrls;
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  const MARKDOWN_SOURCE_EXTENSIONS = new Set(['.md', '.markdown']);
12
+ const SENSITIVE_ATTACHMENT_EXTENSIONS = new Set([
13
+ '.env',
14
+ '.pem',
15
+ '.key',
16
+ '.p12',
17
+ '.pfx',
18
+ '.jks',
19
+ '.keystore',
20
+ '.secret',
21
+ '.secrets',
22
+ ]);
23
+ const SENSITIVE_ATTACHMENT_NAMES = new Set([
24
+ 'id_rsa',
25
+ 'id_dsa',
26
+ 'id_ecdsa',
27
+ 'id_ed25519',
28
+ ]);
11
29
  const MARKDOWN_LINK_PATTERN = /!?\[[^\]]*\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)/g;
12
30
  const MARKDOWN_REFERENCE_PATTERN = /^\s{0,3}\[[^\]]+\]:\s*(?:<([^>\n]+)>|(\S+))/gm;
13
31
  const HTML_ASSET_PATTERN = /\b(?:src|href)=["']([^"']+)["']/gi;
@@ -33,6 +51,12 @@ function isLocalRelativeTarget(target) {
33
51
  function stripTargetSuffix(target) {
34
52
  return target.split(/[?#]/, 1)[0];
35
53
  }
54
+ function isSensitiveAttachment(target) {
55
+ const baseName = node_path_1.default.basename(target).toLowerCase();
56
+ return SENSITIVE_ATTACHMENT_EXTENSIONS.has(node_path_1.default.extname(baseName))
57
+ || SENSITIVE_ATTACHMENT_NAMES.has(baseName)
58
+ || baseName.startsWith('.env.');
59
+ }
36
60
  function extractTargets(markdown) {
37
61
  const source = markdown.replace(FENCED_CODE_PATTERN, '');
38
62
  const targets = [];
@@ -65,6 +89,8 @@ function resolveAttachmentPath(docsRoot, realDocsRoot, markdownPath, rawTarget,
65
89
  const extension = node_path_1.default.extname(decodedTarget).toLowerCase();
66
90
  if (MARKDOWN_SOURCE_EXTENSIONS.has(extension))
67
91
  return null;
92
+ if (isSensitiveAttachment(decodedTarget))
93
+ return null;
68
94
  const resolved = node_path_1.default.resolve(docsRoot, node_path_1.default.dirname(markdownPath), decodedTarget);
69
95
  const relative = node_path_1.default.relative(docsRoot, resolved);
70
96
  if (!relative || node_path_1.default.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${node_path_1.default.sep}`)) {
@@ -121,3 +147,36 @@ function copyDocumentAttachments(sourceRoot, destinationRoot, options = {}) {
121
147
  }
122
148
  return attachmentPaths;
123
149
  }
150
+ function encodeAssetPath(assetPath) {
151
+ return assetPath.split('/').map((segment) => encodeURIComponent(segment)).join('/');
152
+ }
153
+ /** Rewrite rendered local attachment links to the static document asset endpoint. */
154
+ function rewriteDocumentAttachmentUrls(html, documentPath, options = {}) {
155
+ const assetPrefix = (options.assetPrefix || '/doc-asset').replace(/\/+$/, '');
156
+ const assetPaths = options.assetPaths ? new Set(options.assetPaths) : null;
157
+ const documentDir = node_path_1.default.posix.dirname(documentPath.replace(/\\/g, '/'));
158
+ const attributePattern = /(<(?:a|audio|embed|iframe|img|object|source|video)\b[^>]*\s(?:href|src)=["'])([^"']+)(["'])/gi;
159
+ return html.replace(attributePattern, (match, prefix, rawTarget, suffix) => {
160
+ if (!rawTarget || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/|data:)/i.test(rawTarget))
161
+ return match;
162
+ const targetWithoutSuffix = stripTargetSuffix(rawTarget);
163
+ let decodedTarget;
164
+ try {
165
+ decodedTarget = decodeURIComponent(targetWithoutSuffix);
166
+ }
167
+ catch {
168
+ return match;
169
+ }
170
+ if (!decodedTarget || MARKDOWN_SOURCE_EXTENSIONS.has(node_path_1.default.extname(decodedTarget).toLowerCase())) {
171
+ return match;
172
+ }
173
+ const resolved = node_path_1.default.posix.normalize(node_path_1.default.posix.join(documentDir, decodedTarget));
174
+ if (!resolved || resolved === '..' || resolved.startsWith('../') || resolved.startsWith('.'))
175
+ return match;
176
+ if (assetPaths && !assetPaths.has(resolved))
177
+ return match;
178
+ const suffixStart = targetWithoutSuffix.length;
179
+ const queryAndHash = rawTarget.slice(suffixStart);
180
+ return `${prefix}${assetPrefix}/${encodeAssetPath(resolved)}${queryAndHash}${suffix}`;
181
+ });
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",