@tenphi/akno-core 0.1.1 → 0.3.0

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 (101) hide show
  1. package/config/default.jsonc +22 -5
  2. package/dist/config/load.js +17 -0
  3. package/dist/config/load.js.map +1 -1
  4. package/dist/config/schema.d.ts +25 -1
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/config/schema.js +37 -2
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/doctor.d.ts +13 -0
  9. package/dist/doctor.d.ts.map +1 -1
  10. package/dist/doctor.js +18 -0
  11. package/dist/doctor.js.map +1 -1
  12. package/dist/index/derive.d.ts.map +1 -1
  13. package/dist/index/derive.js +26 -3
  14. package/dist/index/derive.js.map +1 -1
  15. package/dist/index/indexer.d.ts +12 -6
  16. package/dist/index/indexer.d.ts.map +1 -1
  17. package/dist/index/indexer.js +41 -24
  18. package/dist/index/indexer.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/ingest/fetch.d.ts +16 -13
  23. package/dist/ingest/fetch.d.ts.map +1 -1
  24. package/dist/ingest/fetch.js +176 -52
  25. package/dist/ingest/fetch.js.map +1 -1
  26. package/dist/kb/frontmatter.d.ts +15 -0
  27. package/dist/kb/frontmatter.d.ts.map +1 -1
  28. package/dist/kb/frontmatter.js +105 -9
  29. package/dist/kb/frontmatter.js.map +1 -1
  30. package/dist/maintenance/adopt.d.ts.map +1 -1
  31. package/dist/maintenance/adopt.js +2 -1
  32. package/dist/maintenance/adopt.js.map +1 -1
  33. package/dist/maintenance/brain-migration.d.ts +20 -0
  34. package/dist/maintenance/brain-migration.d.ts.map +1 -0
  35. package/dist/maintenance/brain-migration.js +234 -0
  36. package/dist/maintenance/brain-migration.js.map +1 -0
  37. package/dist/maintenance/curate.js +3 -3
  38. package/dist/maintenance/curate.js.map +1 -1
  39. package/dist/maintenance/dream.d.ts.map +1 -1
  40. package/dist/maintenance/dream.js +27 -17
  41. package/dist/maintenance/dream.js.map +1 -1
  42. package/dist/maintenance/managed-item-sources.d.ts +0 -1
  43. package/dist/maintenance/managed-item-sources.d.ts.map +1 -1
  44. package/dist/maintenance/managed-item-sources.js +0 -1
  45. package/dist/maintenance/managed-item-sources.js.map +1 -1
  46. package/dist/maintenance/managed-items.d.ts +1 -7
  47. package/dist/maintenance/managed-items.d.ts.map +1 -1
  48. package/dist/maintenance/managed-items.js +15 -23
  49. package/dist/maintenance/managed-items.js.map +1 -1
  50. package/dist/open.d.ts +3 -0
  51. package/dist/open.d.ts.map +1 -1
  52. package/dist/open.js +5 -0
  53. package/dist/open.js.map +1 -1
  54. package/dist/ops/forget.d.ts.map +1 -1
  55. package/dist/ops/forget.js +3 -0
  56. package/dist/ops/forget.js.map +1 -1
  57. package/dist/ops/ingest.d.ts.map +1 -1
  58. package/dist/ops/ingest.js +11 -5
  59. package/dist/ops/ingest.js.map +1 -1
  60. package/dist/ops/remember.d.ts.map +1 -1
  61. package/dist/ops/remember.js +47 -11
  62. package/dist/ops/remember.js.map +1 -1
  63. package/dist/ops/retain.d.ts +4 -0
  64. package/dist/ops/retain.d.ts.map +1 -0
  65. package/dist/ops/retain.js +743 -0
  66. package/dist/ops/retain.js.map +1 -0
  67. package/dist/ops/undo.d.ts +1 -1
  68. package/dist/ops/undo.d.ts.map +1 -1
  69. package/dist/ops/undo.js +20 -1
  70. package/dist/ops/undo.js.map +1 -1
  71. package/dist/ops/write.d.ts.map +1 -1
  72. package/dist/ops/write.js +11 -7
  73. package/dist/ops/write.js.map +1 -1
  74. package/dist/store/db.d.ts.map +1 -1
  75. package/dist/store/db.js +12 -1
  76. package/dist/store/db.js.map +1 -1
  77. package/dist/store/migrations.d.ts +8 -6
  78. package/dist/store/migrations.d.ts.map +1 -1
  79. package/dist/store/migrations.js +60 -8
  80. package/dist/store/migrations.js.map +1 -1
  81. package/dist/write/journal.d.ts +4 -8
  82. package/dist/write/journal.d.ts.map +1 -1
  83. package/dist/write/journal.js +215 -55
  84. package/dist/write/journal.js.map +1 -1
  85. package/dist/write/managed-memory.d.ts +45 -0
  86. package/dist/write/managed-memory.d.ts.map +1 -0
  87. package/dist/write/managed-memory.js +462 -0
  88. package/dist/write/managed-memory.js.map +1 -0
  89. package/dist/write/placement.d.ts +3 -2
  90. package/dist/write/placement.d.ts.map +1 -1
  91. package/dist/write/placement.js +3 -5
  92. package/dist/write/placement.js.map +1 -1
  93. package/dist/write/retain-supports.d.ts +6 -0
  94. package/dist/write/retain-supports.d.ts.map +1 -0
  95. package/dist/write/retain-supports.js +48 -0
  96. package/dist/write/retain-supports.js.map +1 -0
  97. package/dist/write/retain.d.ts +8 -2
  98. package/dist/write/retain.d.ts.map +1 -1
  99. package/dist/write/retain.js +30 -9
  100. package/dist/write/retain.js.map +1 -1
  101. package/package.json +2 -2
@@ -1,19 +1,10 @@
1
1
  /**
2
2
  * `ingest` pulls documents from a file, a folder, or a URL. This is the URL.
3
3
  *
4
- * Downloading is the one thing `ingest` does that reaches outside the machine, so the
5
- * limits are deliberate rather than defensive-by-habit:
6
- *
7
- * - **http and https only.** `file://` would make `ingest({url})` a way to read any
8
- * path on the machine through an interface that looks like it fetches the web, and
9
- * `data:` would smuggle bytes past the size cap.
10
- * - **Redirects are followed** by `fetch`, which caps the chain itself; a redirect is
11
- * normal and an infinite one is already handled below us.
12
- * - **The size cap applies to what arrives, not to what the server claims.** A
13
- * `Content-Length` is a promise, and the stream is what actually lands on disk.
14
- * - **The filename comes from the response, then the URL, then the content type.** A
15
- * URL path is often `/download` or `/`, which says nothing — and a name adding nothing is
16
- * exactly what naming-from-content replaces.
4
+ * A URL is an instruction to make a network connection, so validation is performed on
5
+ * resolved destinations rather than on spelling alone. Every DNS answer must be public,
6
+ * the request is pinned to one of those validated answers, and every redirect starts the
7
+ * same process again. That closes both redirect-to-private and DNS-rebinding paths.
17
8
  */
18
9
  export interface Fetched {
19
10
  /** A temp file. The caller ingests it and is responsible for cleaning up. */
@@ -24,12 +15,24 @@ export interface Fetched {
24
15
  bytes: number;
25
16
  finalUrl: string;
26
17
  }
18
+ interface ResolvedAddress {
19
+ address: string;
20
+ family: 4 | 6;
21
+ }
27
22
  export interface FetchOptions {
28
23
  url: string;
29
24
  maxBytes: number;
30
25
  timeoutMs?: number;
26
+ /** Exact origins explicitly trusted to reach non-public destinations. */
27
+ trustedOrigins?: string[];
28
+ /** Test seam; production uses the operating system resolver. */
29
+ resolve?: (hostname: string) => Promise<ResolvedAddress[]>;
30
+ maxRedirects?: number;
31
31
  }
32
32
  export declare function fetchDocument(options: FetchOptions): Promise<Fetched>;
33
33
  /** The temp directory a fetch created. Removed once the bytes have been stored. */
34
34
  export declare function cleanupFetch(fetched: Fetched): Promise<void>;
35
+ /** Exposed for deterministic policy tests without making a network request. */
36
+ export declare function assertSafeUrlDestination(raw: string, options?: Pick<FetchOptions, 'trustedOrigins' | 'resolve'>): Promise<void>;
37
+ export {};
35
38
  //# sourceMappingURL=fetch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/ingest/fetch.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,OAAO;IACtB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CA4D3E;AAED,mFAAmF;AACnF,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/ingest/fetch.ts"],"names":[],"mappings":"AASA;;;;;;;GAOG;AAEH,MAAM,WAAW,OAAO;IACtB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,gEAAgE;IAChE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAKD,wBAAsB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAkE3E;AAED,mFAAmF;AACnF,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;AAED,+EAA+E;AAC/E,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,IAAI,CAAC,YAAY,EAAE,gBAAgB,GAAG,SAAS,CAAM,GAC7D,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -1,88 +1,222 @@
1
+ import dns from 'node:dns/promises';
2
+ import http, {} from 'node:http';
3
+ import https from 'node:https';
4
+ import { BlockList, isIP } from 'node:net';
1
5
  import fsp from 'node:fs/promises';
2
6
  import os from 'node:os';
3
7
  import path from 'node:path';
4
8
  import { AknoError } from '@tenphi/akno-protocol';
9
+ const DENIED_DESTINATIONS = deniedDestinations();
10
+ const REDIRECTS = new Set([301, 302, 303, 307, 308]);
5
11
  export async function fetchDocument(options) {
6
- const url = parseUrl(options.url);
12
+ const timeoutMs = options.timeoutMs ?? 60_000;
13
+ const trusted = new Set((options.trustedOrigins ?? []).map(normalizeTrustedOrigin));
14
+ const resolve = options.resolve ?? resolveHost;
15
+ let url = parseUrl(options.url);
16
+ let redirects = 0;
7
17
  let response;
8
- try {
9
- response = await fetch(url, {
10
- redirect: 'follow',
11
- signal: AbortSignal.timeout(options.timeoutMs ?? 60_000),
12
- headers: { accept: '*/*' },
13
- });
14
- }
15
- catch (err) {
16
- const timedOut = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError');
17
- throw new AknoError('unavailable', timedOut
18
- ? `fetching ${url.href} timed out`
19
- : `could not fetch ${url.href}: ${err instanceof Error ? err.message : String(err)}`);
20
- }
21
- if (!response.ok) {
22
- throw new AknoError('unavailable', `${url.href} returned ${response.status} ${response.statusText}`);
18
+ while (true) {
19
+ const addresses = await permittedAddresses(url, trusted, resolve);
20
+ response = await request(url, addresses, timeoutMs);
21
+ const status = response.statusCode ?? 0;
22
+ if (!REDIRECTS.has(status))
23
+ break;
24
+ const location = header(response.headers, 'location');
25
+ response.resume();
26
+ if (!location)
27
+ throw new AknoError('unavailable', 'URL redirect returned no location');
28
+ if (redirects++ >= (options.maxRedirects ?? 5)) {
29
+ throw new AknoError('unavailable', 'URL redirect limit exceeded');
30
+ }
31
+ url = parseUrl(new URL(location, url).href);
23
32
  }
24
- if (!response.body) {
25
- throw new AknoError('unavailable', `${url.href} returned no body`);
33
+ const status = response.statusCode ?? 0;
34
+ if (status < 200 || status >= 300) {
35
+ response.resume();
36
+ throw new AknoError('unavailable', `URL returned ${status}${response.statusMessage ? ` ${response.statusMessage}` : ''}`);
26
37
  }
27
- const contentType = (response.headers.get('content-type') ?? '').split(';')[0].trim() || null;
28
- const originalName = filenameFor(response, url, contentType);
38
+ const contentType = (header(response.headers, 'content-type') ?? '').split(';')[0].trim() || null;
39
+ const originalName = filenameFor(response.headers, url, contentType);
29
40
  const directory = await fsp.mkdtemp(path.join(os.tmpdir(), 'akno-fetch-'));
30
41
  const target = path.join(directory, originalName);
31
- const handle = await fsp.open(target, 'w');
32
42
  let written = 0;
43
+ let handle = null;
33
44
  try {
34
- for await (const chunk of response.body) {
35
- written += chunk.byteLength;
36
- // Enforced against the bytes that actually arrive: `Content-Length` is a claim,
37
- // and a server willing to lie about the size is exactly the one to guard against.
45
+ handle = await fsp.open(target, 'w');
46
+ for await (const chunk of response) {
47
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
48
+ written += bytes.byteLength;
49
+ // Enforced against the bytes that actually arrive: Content-Length is only a claim.
38
50
  if (written > options.maxBytes) {
39
- await handle.close();
40
- await fsp.rm(directory, { recursive: true, force: true });
41
- throw new AknoError('invalid', `${url.href} is larger than the configured limit of ${Math.round(options.maxBytes / 1_048_576)} MB`);
51
+ throw new AknoError('invalid', `download is larger than the configured limit of ${Math.round(options.maxBytes / 1_048_576)} MB`);
42
52
  }
43
- await handle.write(chunk);
53
+ await handle.write(bytes);
44
54
  }
45
55
  }
56
+ catch (error) {
57
+ await fsp.rm(directory, { recursive: true, force: true });
58
+ throw networkFailure(error);
59
+ }
46
60
  finally {
47
- await handle.close().catch(() => { });
61
+ if (handle)
62
+ await handle.close().catch(() => { });
48
63
  }
49
64
  if (written === 0) {
50
65
  await fsp.rm(directory, { recursive: true, force: true });
51
- throw new AknoError('unavailable', `${url.href} returned an empty body`);
66
+ throw new AknoError('unavailable', 'URL returned an empty body');
52
67
  }
53
- return { path: target, originalName, contentType, bytes: written, finalUrl: response.url || url.href };
68
+ return { path: target, originalName, contentType, bytes: written, finalUrl: url.href };
54
69
  }
55
70
  /** The temp directory a fetch created. Removed once the bytes have been stored. */
56
71
  export async function cleanupFetch(fetched) {
57
72
  await fsp.rm(path.dirname(fetched.path), { recursive: true, force: true });
58
73
  }
74
+ /** Exposed for deterministic policy tests without making a network request. */
75
+ export async function assertSafeUrlDestination(raw, options = {}) {
76
+ const url = parseUrl(raw);
77
+ const trusted = new Set((options.trustedOrigins ?? []).map(normalizeTrustedOrigin));
78
+ await permittedAddresses(url, trusted, options.resolve ?? resolveHost);
79
+ }
80
+ async function permittedAddresses(url, trusted, resolve) {
81
+ const hostname = normalizeHost(url.hostname);
82
+ let addresses;
83
+ try {
84
+ addresses = isIP(hostname)
85
+ ? [{ address: hostname, family: isIP(hostname) }]
86
+ : await resolve(hostname);
87
+ }
88
+ catch {
89
+ throw new AknoError('unavailable', 'could not resolve URL host');
90
+ }
91
+ if (addresses.length === 0)
92
+ throw new AknoError('unavailable', 'URL host resolved to no addresses');
93
+ if (!trusted.has(url.origin) && addresses.some((entry) => !isPublicAddress(entry))) {
94
+ throw new AknoError('forbidden', 'URL destination is not allowed by network policy', {
95
+ reason: 'network_destination_denied',
96
+ });
97
+ }
98
+ return addresses;
99
+ }
100
+ async function resolveHost(hostname) {
101
+ const answers = await dns.lookup(hostname, { all: true, verbatim: true });
102
+ return answers.map((answer) => ({ address: answer.address, family: answer.family }));
103
+ }
104
+ function isPublicAddress(entry) {
105
+ return !DENIED_DESTINATIONS.check(entry.address, entry.family === 4 ? 'ipv4' : 'ipv6');
106
+ }
107
+ function deniedDestinations() {
108
+ const list = new BlockList();
109
+ for (const [network, prefix] of [
110
+ ['0.0.0.0', 8],
111
+ ['10.0.0.0', 8],
112
+ ['100.64.0.0', 10],
113
+ ['127.0.0.0', 8],
114
+ ['169.254.0.0', 16],
115
+ ['172.16.0.0', 12],
116
+ ['192.0.0.0', 24],
117
+ ['192.0.2.0', 24],
118
+ ['192.168.0.0', 16],
119
+ ['192.88.99.0', 24],
120
+ ['198.18.0.0', 15],
121
+ ['198.51.100.0', 24],
122
+ ['203.0.113.0', 24],
123
+ ['224.0.0.0', 4],
124
+ ['240.0.0.0', 4],
125
+ ]) {
126
+ list.addSubnet(network, prefix, 'ipv4');
127
+ }
128
+ for (const [network, prefix] of [
129
+ ['::', 128],
130
+ ['::1', 128],
131
+ ['::', 96],
132
+ ['::ffff:0:0', 96],
133
+ ['::ffff:0:0:0', 96],
134
+ ['64:ff9b::', 96],
135
+ ['64:ff9b:1::', 48],
136
+ ['100::', 64],
137
+ ['100:0:0:1::', 64],
138
+ ['fc00::', 7],
139
+ ['fe80::', 10],
140
+ ['fec0::', 10],
141
+ ['ff00::', 8],
142
+ ['2001::', 23],
143
+ ['2001:db8::', 32],
144
+ ['2002::', 16],
145
+ ['3fff::', 20],
146
+ ['5f00::', 16],
147
+ ]) {
148
+ list.addSubnet(network, prefix, 'ipv6');
149
+ }
150
+ return list;
151
+ }
152
+ function request(url, addresses, timeoutMs) {
153
+ const transport = url.protocol === 'https:' ? https : http;
154
+ const lookup = (_hostname, lookupOptions, callback) => {
155
+ if (lookupOptions.all) {
156
+ callback(null, addresses);
157
+ return;
158
+ }
159
+ const selected = addresses[0];
160
+ callback(null, selected.address, selected.family);
161
+ };
162
+ return new Promise((resolve, reject) => {
163
+ const req = transport.get(url, {
164
+ signal: AbortSignal.timeout(timeoutMs),
165
+ headers: { accept: '*/*' },
166
+ lookup,
167
+ }, resolve);
168
+ req.on('error', (error) => reject(networkFailure(error)));
169
+ });
170
+ }
59
171
  function parseUrl(raw) {
60
172
  let url;
61
173
  try {
62
174
  url = new URL(raw);
63
175
  }
64
176
  catch {
65
- throw new AknoError('invalid', `not a usable URL: ${raw}`);
177
+ throw new AknoError('invalid', 'not a usable URL');
66
178
  }
67
179
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
68
180
  throw new AknoError('invalid', `only http and https URLs are fetched, not ${url.protocol} — pass a local path instead`);
69
181
  }
182
+ if (url.username || url.password) {
183
+ throw new AknoError('invalid', 'credentials are not allowed in an ingest URL');
184
+ }
70
185
  return url;
71
186
  }
72
- /**
73
- * `Content-Disposition`, then the URL's last path segment, then an extension guessed
74
- * from the content type. Every one of these can be useless, which is fine: the page is named
75
- * from the content anyway, and a useless name is what triggers the rename.
76
- */
77
- function filenameFor(response, url, contentType) {
78
- const disposition = response.headers.get('content-disposition');
187
+ function normalizeHost(value) {
188
+ return value
189
+ .replace(/^\[|\]$/g, '')
190
+ .replace(/\.$/, '')
191
+ .toLowerCase();
192
+ }
193
+ function normalizeTrustedOrigin(value) {
194
+ const url = parseUrl(value);
195
+ if (url.pathname !== '/' || url.search || url.hash) {
196
+ throw new AknoError('invalid', 'a trusted URL origin may contain only scheme, host, and port');
197
+ }
198
+ return url.origin;
199
+ }
200
+ function networkFailure(error) {
201
+ if (error instanceof AknoError)
202
+ return error;
203
+ const name = error instanceof Error ? error.name : '';
204
+ return new AknoError('unavailable', name === 'AbortError' || name === 'TimeoutError' ? 'fetching URL timed out' : 'could not fetch URL');
205
+ }
206
+ function header(headers, name) {
207
+ const value = headers[name];
208
+ if (Array.isArray(value))
209
+ return value[0] ?? null;
210
+ return value ?? null;
211
+ }
212
+ /** Content-Disposition, then URL path, then an extension guessed from content type. */
213
+ function filenameFor(headers, url, contentType) {
214
+ const disposition = header(headers, 'content-disposition');
79
215
  const candidates = [];
80
216
  if (disposition) {
81
217
  const star = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(disposition);
82
218
  const plain = /filename="?([^";]+)"?/i.exec(disposition);
83
219
  const raw = star?.[1] ?? plain?.[1];
84
- // `path.basename` is what stops `../../../etc/passwd` being a path: a server chooses
85
- // this header, so it is untrusted input that becomes a filename.
86
220
  if (raw)
87
221
  candidates.push(sanitize(path.basename(safeDecode(raw.trim()))));
88
222
  }
@@ -91,18 +225,10 @@ function filenameFor(response, url, contentType) {
91
225
  for (const candidate of candidates) {
92
226
  if (candidate.length === 0 || candidate === '.' || candidate === '..')
93
227
  continue;
94
- // The extractor dispatches on the extension, so a name without one is unusable even
95
- // though it reads fine. Servers send `filename="report"` often enough that guessing
96
- // from the content type is the difference between working and skipping.
97
228
  return path.extname(candidate) ? candidate : `${candidate}${extension}`;
98
229
  }
99
230
  return `download${extension}`;
100
231
  }
101
- /**
102
- * A server chooses these bytes, so they are untrusted input on their way to becoming a
103
- * filename: separators and control characters are dropped. Spaces survive, because a
104
- * filename with spaces is ordinary and the rename handles presentation anyway.
105
- */
106
232
  function sanitize(name) {
107
233
  return [...name]
108
234
  .filter((char) => char !== '/' && char !== '\\' && char.codePointAt(0) >= 0x20)
@@ -135,8 +261,6 @@ function extensionFor(contentType) {
135
261
  'application/msword': '.doc',
136
262
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
137
263
  };
138
- // No extension at all is honest: the extractor dispatches on it and will say it has
139
- // none, which is better than claiming `.bin` and looking like a decision.
140
264
  return contentType ? (table[contentType] ?? '') : '';
141
265
  }
142
266
  //# sourceMappingURL=fetch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/ingest/fetch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/ingest/fetch.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,21 @@ export interface Frontmatter {
21
21
  /** 1-based line number of the first body line. Line addressing hangs off this. */
22
22
  bodyLine: number;
23
23
  }
24
+ /**
25
+ * Render a generated YAML string without letting its contents become YAML syntax.
26
+ * JSON string syntax is a YAML 1.2 string syntax too, and gives every troublesome
27
+ * scalar — newlines, comments, brackets, dates, booleans and the empty string — one
28
+ * unambiguous representation.
29
+ */
30
+ export declare function serializeYamlString(value: unknown, field?: string): string;
31
+ /** Render an inline YAML sequence whose members are guaranteed to stay strings. */
32
+ export declare function serializeYamlStringArray(value: unknown, field?: string): string;
33
+ /**
34
+ * Append missing strings to one existing top-level sequence while leaving every existing byte alone.
35
+ * The YAML parser supplies the exact sequence boundary; new members are serialized safely and
36
+ * appended in the sequence's existing block or flow style.
37
+ */
38
+ export declare function mergeTopLevelStringArray(content: string, key: string, additions: unknown): string | null;
24
39
  export declare function parseFrontmatter(content: string): Frontmatter;
25
40
  /** `tags: [a, b]`, `tags:\n - a`, and `tags: a` all mean the same thing here. */
26
41
  export declare function readTags(data: Record<string, unknown>): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/kb/frontmatter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,OAAO,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAoC7D;AAQD,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAUhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhG;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,MAAM,GAAG,IAAI,CA0Cf;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAYzE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CA0DjF;AAED,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAU5E"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/kb/frontmatter.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,OAAO,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAsB,GAAG,MAAM,CAQvF;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAsB,GAAG,MAAM,CAQ5F;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAsDxG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAoC7D;AAQD,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAUhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhG;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,MAAM,GAAG,IAAI,CA6Cf;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAYzE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CA+DjF;AAED,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAU5E"}
@@ -1,5 +1,96 @@
1
+ import { AknoError } from '@tenphi/akno-protocol';
1
2
  import YAML, { isMap, isPair, isScalar, isSeq } from 'yaml';
2
3
  const FENCE = /^---\r?\n/;
4
+ /**
5
+ * Render a generated YAML string without letting its contents become YAML syntax.
6
+ * JSON string syntax is a YAML 1.2 string syntax too, and gives every troublesome
7
+ * scalar — newlines, comments, brackets, dates, booleans and the empty string — one
8
+ * unambiguous representation.
9
+ */
10
+ export function serializeYamlString(value, field = 'frontmatter value') {
11
+ if (typeof value !== 'string') {
12
+ throw new AknoError('invalid', `${field} must be a string`, {
13
+ reason: 'invalid_frontmatter_value',
14
+ field,
15
+ });
16
+ }
17
+ return JSON.stringify(value);
18
+ }
19
+ /** Render an inline YAML sequence whose members are guaranteed to stay strings. */
20
+ export function serializeYamlStringArray(value, field = 'frontmatter value') {
21
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
22
+ throw new AknoError('invalid', `${field} must be an array of strings`, {
23
+ reason: 'invalid_frontmatter_value',
24
+ field,
25
+ });
26
+ }
27
+ return `[${value.map((entry) => serializeYamlString(entry, field)).join(', ')}]`;
28
+ }
29
+ /**
30
+ * Append missing strings to one existing top-level sequence while leaving every existing byte alone.
31
+ * The YAML parser supplies the exact sequence boundary; new members are serialized safely and
32
+ * appended in the sequence's existing block or flow style.
33
+ */
34
+ export function mergeTopLevelStringArray(content, key, additions) {
35
+ // Validate even when the key is absent or malformed. Callers should never silently ignore a
36
+ // value they would have been unable to serialize safely.
37
+ serializeYamlStringArray(additions, key);
38
+ const values = additions;
39
+ const fm = parseFrontmatter(content);
40
+ if (!fm.present)
41
+ return null;
42
+ const document = YAML.parseDocument(fm.raw, { keepSourceTokens: true });
43
+ if (document.errors.length > 0 || !isMap(document.contents))
44
+ return null;
45
+ const matches = document.contents.items.filter((item) => isPair(item) && isScalar(item.key) && item.key.value === key);
46
+ if (matches.length !== 1)
47
+ return null;
48
+ const sequence = matches[0].value;
49
+ if (!isSeq(sequence) || !sequence.range)
50
+ return null;
51
+ if (sequence.items.some((item) => !isScalar(item) || typeof item.value !== 'string'))
52
+ return null;
53
+ const existing = sequence.items.map((item) => item.value);
54
+ const missing = [...new Set(values)].filter((value) => !existing.includes(value));
55
+ if (missing.length === 0)
56
+ return content;
57
+ const [start, end] = sequence.range;
58
+ if (start < 0 || end < start || end > fm.raw.length)
59
+ return null;
60
+ const original = fm.raw.slice(start, end);
61
+ let replacement;
62
+ if (sequence.flow) {
63
+ const close = original.lastIndexOf(']');
64
+ if (close < 0)
65
+ return null;
66
+ // A multiline flow sequence keeps its closing bracket on a separate line. Insert before
67
+ // that trailing whitespace; putting the comma immediately before `]` starts a new mapping
68
+ // key on the closing line instead of extending the sequence.
69
+ const beforeClose = original.slice(0, close);
70
+ const trailingWhitespace = /\s*$/.exec(beforeClose)?.[0] ?? '';
71
+ const insertionPoint = beforeClose.length - trailingWhitespace.length;
72
+ replacement =
73
+ beforeClose.slice(0, insertionPoint) +
74
+ `${existing.length > 0 ? ', ' : ''}${missing.map((value) => serializeYamlString(value, key)).join(', ')}` +
75
+ trailingWhitespace +
76
+ original.slice(close);
77
+ }
78
+ else {
79
+ const newline = fm.raw.includes('\r\n') ? '\r\n' : '\n';
80
+ const trailingNewline = original.endsWith(newline) ? newline : '';
81
+ const lineStart = fm.raw.lastIndexOf('\n', start - 1) + 1;
82
+ const indent = fm.raw.slice(lineStart, start);
83
+ if (!/^[ \t]*$/.test(indent))
84
+ return null;
85
+ replacement =
86
+ original.slice(0, original.length - trailingNewline.length) +
87
+ newline +
88
+ missing.map((value) => `${indent}- ${serializeYamlString(value, key)}`).join(newline) +
89
+ trailingNewline;
90
+ }
91
+ const rawStart = content.indexOf('\n') + 1;
92
+ return content.slice(0, rawStart + start) + replacement + content.slice(rawStart + end);
93
+ }
3
94
  export function parseFrontmatter(content) {
4
95
  if (!FENCE.test(content)) {
5
96
  return { data: {}, raw: '', present: false, bodyOffset: 0, bodyLine: 1 };
@@ -84,7 +175,7 @@ export function replaceTopLevelString(content, key, value) {
84
175
  if (start < 0 || end < start || end > fm.raw.length)
85
176
  return null;
86
177
  const rawStart = content.indexOf('\n') + 1;
87
- return content.slice(0, rawStart + start) + JSON.stringify(value) + content.slice(rawStart + end);
178
+ return content.slice(0, rawStart + start) + serializeYamlString(value, key) + content.slice(rawStart + end);
88
179
  }
89
180
  /**
90
181
  * Replace every exact string in one existing nested YAML sequence while preserving all other bytes.
@@ -126,7 +217,10 @@ export function replaceNestedStringArrayValue(content, keys, from, to) {
126
217
  for (const [start, end] of ranges) {
127
218
  if (start < 0 || end < start || end > fm.raw.length)
128
219
  return null;
129
- result = result.slice(0, rawStart + start) + JSON.stringify(to) + result.slice(rawStart + end);
220
+ result =
221
+ result.slice(0, rawStart + start) +
222
+ serializeYamlString(to, keys.join('.')) +
223
+ result.slice(rawStart + end);
130
224
  }
131
225
  return result;
132
226
  }
@@ -145,11 +239,11 @@ export function replaceNestedStringArrayValue(content, keys, from, to) {
145
239
  export function withId(content, id) {
146
240
  const fm = parseFrontmatter(content);
147
241
  if (!fm.present) {
148
- return `---\nid: ${id}\n---\n\n${content}`;
242
+ return `---\nid: ${serializeYamlString(id, 'id')}\n---\n\n${content}`;
149
243
  }
150
244
  if (typeof fm.data.id === 'string' && fm.data.id.length > 0)
151
245
  return content;
152
- return spliceAfterFence(content, [`id: ${id}`]);
246
+ return spliceAfterFence(content, [`id: ${serializeYamlString(id, 'id')}`]);
153
247
  }
154
248
  /**
155
249
  * Inserts lines immediately after the opening fence of an existing block, leaving
@@ -194,12 +288,14 @@ export function withAknoAliases(content, aliases) {
194
288
  const existingValue = record?.aliases;
195
289
  if (existingValue !== undefined && !Array.isArray(existingValue))
196
290
  return null;
197
- const existing = (existingValue ?? []).filter((value) => typeof value === 'string');
291
+ if (existingValue?.some((value) => typeof value !== 'string'))
292
+ return null;
293
+ const existing = (existingValue ?? []);
198
294
  const wanted = [...new Set([...existing, ...additions])];
199
- if (wanted.length === existing.length)
295
+ if (additions.every((value) => existing.includes(value)))
200
296
  return content;
201
297
  const missing = wanted.filter((value) => !existing.includes(value));
202
- const rendered = wanted.map((value) => ` - ${JSON.stringify(value)}`);
298
+ const rendered = wanted.map((value) => ` - ${serializeYamlString(value, 'akno.aliases')}`);
203
299
  if (!fm.present || !record) {
204
300
  return spliceAfterFence(content, ['akno:', ' aliases:', ...rendered]);
205
301
  }
@@ -226,14 +322,14 @@ export function withAknoAliases(content, aliases) {
226
322
  const match = /^ aliases:[ \t]*(.*)$/.exec(lines[aliasesIndex]);
227
323
  const tail = match?.[1]?.trim() ?? '';
228
324
  if (tail.startsWith('[') && tail.endsWith(']')) {
229
- lines[aliasesIndex] = ` aliases: ${JSON.stringify(wanted)}`;
325
+ lines[aliasesIndex] = ` aliases: ${serializeYamlStringArray(wanted, 'akno.aliases')}`;
230
326
  }
231
327
  else if (tail.length === 0) {
232
328
  let insertAt = aliasesIndex + 1;
233
329
  while (insertAt < blockEnd && (/^[ \t]{4,}/.test(lines[insertAt]) || !lines[insertAt].trim())) {
234
330
  insertAt++;
235
331
  }
236
- lines.splice(insertAt, 0, ...missing.map((value) => ` - ${JSON.stringify(value)}`));
332
+ lines.splice(insertAt, 0, ...missing.map((value) => ` - ${serializeYamlString(value, 'akno.aliases')}`));
237
333
  }
238
334
  else {
239
335
  return null;