llms-ink 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -211,3 +211,27 @@ gets its own daily quota (1000 new links by default) and 60 requests a minute;
211
211
  ## License
212
212
 
213
213
  MIT
214
+
215
+ ## Hosted Inspect (experimental, 0.5.0)
216
+
217
+ ```ts
218
+ // Server-side; requires llms-ink >=0.5.0.
219
+ import { LlmsInk } from 'llms-ink';
220
+
221
+ const ink = new LlmsInk({ key: process.env.LLMS_INK_INSPECT_KEY });
222
+ const receipt = await ink.inspect('Install the package and read the documentation.');
223
+ if (receipt.recommendation !== 'continue') {
224
+ throw new Error(`Withhold content: ${receipt.detection}`);
225
+ }
226
+ console.log(receipt.contentDigest);
227
+ ```
228
+
229
+ Requires an inspection-enabled pilot key. This call uploads only the supplied text to llms.ink; existing retrieval calls, loaders, plugins and the local guard do not opt in automatically. Keep the key server-side.
230
+
231
+ The client checks the exact UTF-8 SHA-256 digest, model/policy/preprocessing revisions, coverage and token counts before returning an immutable receipt. A flagged result can use HTTP 200; incomplete 422/503 receipts return `unknown`. HTTP problems and invalid receipts raise an error. Withhold on either `withhold` or any exception. `no_finding` is not a safety guarantee, and receipts are unsigned.
232
+
233
+ Requests are limited to 128 KiB after JSON encoding and responses to 16 KiB. There are no automatic retries: admitted attempts consume pilot allowance even when incomplete. Redirects are refused. Inspection ignores `LLMS_INK_API`; an explicit client base URL may select an HTTPS endpoint or localhost for development. The key follows that explicitly configured endpoint.
234
+
235
+ For JSON, serialize it yourself and pass the resulting string to `inspect()`. The SDK sends `format: "text"`, so its digest covers those exact bytes; it does not promise cross-language JSON canonicalization. See [pilot limits and privacy](https://llms.ink/inspect/).
236
+
237
+ JavaScript uses a 65-second total request deadline; pass `{ signal }` to cancel earlier.
package/dist/index.d.ts CHANGED
@@ -13,7 +13,9 @@
13
13
  *
14
14
  * @module
15
15
  */
16
- export declare const VERSION = "0.4.0";
16
+ import { type InspectionReceipt } from './inspect.ts';
17
+ export type { InspectionReceipt } from './inspect.ts';
18
+ export declare const VERSION = "0.5.0";
17
19
  export declare const DEFAULT_BASE_URL = "https://llms.ink";
18
20
  /** `llms` for the index (llms.txt), `full` for llms-full.txt. */
19
21
  export type Mode = 'llms' | 'full';
@@ -137,7 +139,7 @@ export interface CheckResult {
137
139
  checkedAt: string;
138
140
  }
139
141
  /** Every error code the API answers with (https://llms.ink/docs/#errors). */
140
- export type ErrorCode = 'invalid_request' | 'invalid_url' | 'invalid_key' | 'key_required' | 'site_claimed' | 'not_found' | 'unknown_version' | 'method_not_allowed' | 'payload_too_large' | 'too_large' | 'unsupported_media_type' | 'unprocessable' | 'rate_limited' | 'quota_exceeded' | 'internal' | 'upstream_error' | 'unavailable' | 'timeout' | 'loop_detected';
142
+ export type ErrorCode = 'inspection_not_enabled' | 'inspection_quota' | 'inspection_unavailable' | 'invalid_request' | 'invalid_url' | 'invalid_key' | 'key_required' | 'site_claimed' | 'not_found' | 'unknown_version' | 'method_not_allowed' | 'payload_too_large' | 'too_large' | 'unsupported_media_type' | 'unprocessable' | 'rate_limited' | 'quota_exceeded' | 'internal' | 'upstream_error' | 'unavailable' | 'timeout' | 'loop_detected';
141
143
  /** `invalid_request` only: one invalid parameter. */
142
144
  export interface Issue {
143
145
  path: string;
@@ -268,6 +270,15 @@ export declare class LlmsInk {
268
270
  * any public link.
269
271
  */
270
272
  llms(url: string, options?: LlmsOptions): Promise<Result>;
273
+ /**
274
+ * Explicitly upload supplied text to the hosted Inspect pilot. Requires an enabled key.
275
+ * The receipt binds to these exact UTF-8 bytes; no finding is not a safety guarantee.
276
+ * No retries. 65-second default deadline. LLMS_INK_API is ignored; baseUrl must be explicit.
277
+ * Keep keys server-side. Malformed receipts and HTTP problems throw LlmsInkError.
278
+ */
279
+ inspect(text: string, options?: {
280
+ signal?: AbortSignal;
281
+ }): Promise<InspectionReceipt>;
271
282
  /** Checks a site's published llms.txt against the llmstxt.org format and that its links answer. */
272
283
  check(url: string, options?: CheckOptions): Promise<CheckResult>;
273
284
  /** Parses and checks llms.txt text you already have (`POST /api/v0/check`); nothing is published. */
package/dist/index.js CHANGED
@@ -13,8 +13,9 @@
13
13
  *
14
14
  * @module
15
15
  */
16
+ import { prepareInspection, inspectionJson, inspectionReceipt } from "./inspect.js";
16
17
  import { env } from "./env.js";
17
- export const VERSION = '0.4.0';
18
+ export const VERSION = '0.5.0';
18
19
  export const DEFAULT_BASE_URL = 'https://llms.ink';
19
20
  /**
20
21
  * An API error, or llms.ink couldn't be reached (then `status` and `code` are undefined).
@@ -83,10 +84,12 @@ export class LlmsInk {
83
84
  key;
84
85
  /** Origin of the API, without a trailing slash. */
85
86
  baseUrl;
87
+ #inspectBaseUrl;
86
88
  #fetch;
87
89
  constructor(options = {}) {
88
90
  const key = (options.key ?? env('LLMS_INK_KEY'))?.trim();
89
91
  this.key = key || undefined;
92
+ this.#inspectBaseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
90
93
  this.baseUrl = (options.baseUrl ?? env('LLMS_INK_API') ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
91
94
  if (!options.fetch && typeof globalThis.fetch !== 'function') {
92
95
  throw new TypeError('No global fetch: use Node 18+ or pass options.fetch.');
@@ -102,6 +105,44 @@ export class LlmsInk {
102
105
  const res = await this.#request('/api/v0/llms', llmsParams(url, options), 'application/json', options.signal);
103
106
  return (await this.#json(res));
104
107
  }
108
+ /**
109
+ * Explicitly upload supplied text to the hosted Inspect pilot. Requires an enabled key.
110
+ * The receipt binds to these exact UTF-8 bytes; no finding is not a safety guarantee.
111
+ * No retries. 65-second default deadline. LLMS_INK_API is ignored; baseUrl must be explicit.
112
+ * Keep keys server-side. Malformed receipts and HTTP problems throw LlmsInkError.
113
+ */
114
+ async inspect(text, options = {}) {
115
+ if (!this.key)
116
+ throw new LlmsInkError('Inspect requires an inspection-enabled API key.', { code: 'key_required' });
117
+ const { body, digest } = await prepareInspection(text, this.#inspectBaseUrl);
118
+ const controller = new AbortController();
119
+ const abort = () => controller.abort(options.signal?.reason);
120
+ const timer = setTimeout(() => controller.abort(new DOMException('Inspection timed out.', 'TimeoutError')), 65000);
121
+ if (options.signal?.aborted)
122
+ abort();
123
+ options.signal?.addEventListener('abort', abort, { once: true });
124
+ try {
125
+ const res = await this.#fetch(`${this.#inspectBaseUrl}/api/v0/inspect`, {
126
+ method: 'POST', redirect: 'error', signal: controller.signal,
127
+ headers: { authorization: `Bearer ${this.key}`, 'content-type': 'application/json', accept: 'application/json' }, body,
128
+ });
129
+ const data = await inspectionJson(res);
130
+ if (!res.ok && data && typeof data === 'object' && !('detection' in data)) {
131
+ const p = data;
132
+ throw errorFor(p.detail || p.title || `Inspect returned HTTP ${res.status}.`, { status: res.status, code: p.code, retryAfter: retryAfter(res.headers.get('retry-after')) });
133
+ }
134
+ return inspectionReceipt(data, digest, res.status);
135
+ }
136
+ catch (err) {
137
+ if (err instanceof LlmsInkError || isAbort(err))
138
+ throw err;
139
+ throw new LlmsInkError('Inspection did not return a valid receipt. Withhold the content.', { code: 'inspection_unavailable' });
140
+ }
141
+ finally {
142
+ clearTimeout(timer);
143
+ options.signal?.removeEventListener('abort', abort);
144
+ }
145
+ }
105
146
  /** Checks a site's published llms.txt against the llmstxt.org format and that its links answer. */
106
147
  async check(url, options = {}) {
107
148
  const res = await this.#request('/api/v0/check', { url }, 'application/json', options.signal);
@@ -0,0 +1,21 @@
1
+ /** Hosted Inspect pilot. Only explicit inspect() calls upload content. */
2
+ export interface InspectionReceipt {
3
+ readonly detection: 'no_finding' | 'flagged' | 'unknown';
4
+ readonly coverage: 'complete' | 'partial' | 'none';
5
+ readonly recommendation: 'continue' | 'withhold';
6
+ readonly reason: string;
7
+ readonly modelRevision: string;
8
+ readonly policyRevision: string;
9
+ readonly preprocessingRevision: string;
10
+ readonly contentDigest: string;
11
+ readonly inputTokens: number;
12
+ readonly inspectedTokens: number;
13
+ readonly windows: number;
14
+ readonly enforced: false;
15
+ }
16
+ export declare function prepareInspection(text: string, baseUrl: string): Promise<{
17
+ body: string;
18
+ digest: string;
19
+ }>;
20
+ export declare function inspectionJson(res: Response): Promise<unknown>;
21
+ export declare function inspectionReceipt(value: unknown, digest: string, status: number): InspectionReceipt;
@@ -0,0 +1,67 @@
1
+ export async function prepareInspection(text, baseUrl) {
2
+ const url = new URL(baseUrl);
3
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))))
4
+ throw new TypeError('Inspect requires HTTPS (or localhost for development).');
5
+ if (typeof text !== 'string' || !text.length)
6
+ throw new TypeError('Inspect requires nonempty text.');
7
+ const bytes = new TextEncoder().encode(text);
8
+ if (new TextDecoder('utf-8', { ignoreBOM: true }).decode(bytes) !== text)
9
+ throw new TypeError('Inspect requires valid Unicode.');
10
+ const body = JSON.stringify({ format: 'text', content: text });
11
+ if (new TextEncoder().encode(body).length > 131072)
12
+ throw new TypeError('Inspect request exceeds 128 KiB after JSON encoding.');
13
+ const crypto = globalThis.crypto ?? (await import('node:crypto')).webcrypto;
14
+ const digest = [...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))].map(b => b.toString(16).padStart(2, '0')).join('');
15
+ return { body, digest };
16
+ }
17
+ export async function inspectionJson(res) {
18
+ const reader = res.body?.getReader();
19
+ if (!reader)
20
+ throw new Error('Inspect returned an empty response.');
21
+ const chunks = [];
22
+ let size = 0;
23
+ try {
24
+ while (true) {
25
+ const { value, done } = await reader.read();
26
+ if (done)
27
+ break;
28
+ size += value.length;
29
+ if (size > 16384) {
30
+ await reader.cancel();
31
+ throw new Error('Inspect response exceeds 16 KiB.');
32
+ }
33
+ chunks.push(value);
34
+ }
35
+ }
36
+ finally {
37
+ reader.releaseLock();
38
+ }
39
+ const bytes = new Uint8Array(size);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ bytes.set(chunk, offset);
43
+ offset += chunk.length;
44
+ }
45
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
46
+ }
47
+ export function inspectionReceipt(value, digest, status) {
48
+ const fail = () => { throw new Error('Inspect returned an invalid or unsupported receipt. Withhold the content.'); };
49
+ if (!value || typeof value !== 'object')
50
+ return fail();
51
+ const r = value;
52
+ if (![200, 422, 503].includes(status) || (status !== 200 && r.detection !== 'unknown') || r.contentDigest !== digest || r.modelRevision !== '4a43fec65a670e48835d8e8c85c705b4b0d1c907' || r.policyRevision !== 'llms-ink-inspect-1' || r.preprocessingRevision !== 'utf8-json-stringify-1' || r.enforced !== false)
53
+ return fail();
54
+ if (!Number.isSafeInteger(r.inputTokens) || r.inputTokens < 0 || !Number.isSafeInteger(r.inspectedTokens) || r.inspectedTokens < 0 || r.inspectedTokens > r.inputTokens || !Number.isInteger(r.windows) || r.windows < 0 || r.windows > 43)
55
+ return fail();
56
+ let valid = false;
57
+ if (r.detection === 'no_finding')
58
+ valid = r.coverage === 'complete' && r.recommendation === 'continue' && r.reason === 'no_finding' && r.inputTokens <= 8192 && r.inspectedTokens === r.inputTokens && (r.inputTokens === 0 || r.windows > 0);
59
+ if (r.detection === 'flagged')
60
+ valid = ['complete', 'partial'].includes(r.coverage) && r.recommendation === 'withhold' && ['instruction_pattern', 'model_detection'].includes(r.reason) && r.inputTokens <= 8192 && r.inspectedTokens > 0 && r.windows > 0 && (r.coverage !== 'complete' || r.inspectedTokens === r.inputTokens);
61
+ if (r.detection === 'unknown')
62
+ valid = r.coverage === 'none' && r.recommendation === 'withhold' && r.inspectedTokens === 0 && r.windows === 0 && ['token_limit', 'model_unavailable', 'model_failure', 'inspection_timeout'].includes(r.reason);
63
+ if (!valid)
64
+ return fail();
65
+ const { detection, coverage, recommendation, reason, modelRevision, policyRevision, preprocessingRevision, contentDigest, inputTokens, inspectedTokens, windows, enforced } = r;
66
+ return Object.freeze({ detection, coverage, recommendation, reason, modelRevision, policyRevision, preprocessingRevision, contentDigest, inputTokens, inspectedTokens, windows, enforced });
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llms-ink",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Turn any link — site, docs section, page, GitHub repo, PDF or video — into llms.txt and llms-full.txt. Typed client for llms.ink, plus a Vite plugin, an Astro integration and middleware that serve the files from your site. No dependencies.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -31,9 +31,9 @@
31
31
  "scripts": {
32
32
  "build": "rm -rf dist && tsc -p tsconfig.json",
33
33
  "prepublishOnly": "npm run build && npm test",
34
- "test": "npm run build && node --test test/client.test.mjs test/plugins.test.mjs test/middleware.test.mjs test/traffic.test.mjs",
34
+ "test": "npm run build && node --test test/client.test.mjs test/plugins.test.mjs test/middleware.test.mjs test/traffic.test.mjs test/inspect.test.mjs",
35
35
  "test:live": "npm run build && node --test test/live.test.mjs",
36
- "test:bun": "npm run build && bun test ./test/client.test.mjs ./test/plugins.test.mjs ./test/middleware.test.mjs ./test/traffic.test.mjs",
36
+ "test:bun": "npm run build && bun test ./test/client.test.mjs ./test/plugins.test.mjs ./test/middleware.test.mjs ./test/traffic.test.mjs ./test/inspect.test.mjs",
37
37
  "test:bun:live": "npm run build && bun test --timeout 300000 ./test/live.test.mjs"
38
38
  },
39
39
  "keywords": ["llms.txt", "llms-full.txt", "llm", "ai", "markdown", "docs", "context", "rag", "sdk", "llms-ink", "vite-plugin", "astro-integration", "middleware"],
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^22.20.5",
53
- "astro": "^7.4.1",
53
+ "astro": "^7.4.7",
54
54
  "typescript": "^5.9.3",
55
55
  "vite": "^8.3.8"
56
56
  }