@agenthoney/analytics 0.0.0-stage → 0.14.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/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/answers.cjs +20034 -0
- package/dist/answers.cjs.map +1 -0
- package/dist/answers.d.ts +148 -0
- package/dist/answers.js +139 -0
- package/dist/answers.js.map +1 -0
- package/dist/chunk-CD4WLJX7.js +48 -0
- package/dist/chunk-CD4WLJX7.js.map +1 -0
- package/dist/chunk-F3PRHEXB.js +20143 -0
- package/dist/chunk-F3PRHEXB.js.map +1 -0
- package/dist/chunk-L22VERBM.js +911 -0
- package/dist/chunk-L22VERBM.js.map +1 -0
- package/dist/chunk-OH4H2B7O.js +150 -0
- package/dist/chunk-OH4H2B7O.js.map +1 -0
- package/dist/chunk-R76CTIBG.js +701 -0
- package/dist/chunk-R76CTIBG.js.map +1 -0
- package/dist/chunk-UG3REZCJ.js +147 -0
- package/dist/chunk-UG3REZCJ.js.map +1 -0
- package/dist/core/breaker.d.ts +33 -0
- package/dist/core/collector.d.ts +51 -0
- package/dist/core/config.d.ts +124 -0
- package/dist/core/encode.d.ts +32 -0
- package/dist/core/queue.d.ts +39 -0
- package/dist/core/record-gate.d.ts +17 -0
- package/dist/core/safe.d.ts +17 -0
- package/dist/core/transport.d.ts +45 -0
- package/dist/express.cjs +21789 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.ts +65 -0
- package/dist/express.js +6 -0
- package/dist/express.js.map +1 -0
- package/dist/index.cjs +22118 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +21186 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.ts +90 -0
- package/dist/next.js +5 -0
- package/dist/next.js.map +1 -0
- package/dist/observe/client-ip.d.ts +109 -0
- package/dist/observe/next-router.d.ts +22 -0
- package/dist/observe/redact.d.ts +58 -0
- package/dist/observe/request.d.ts +75 -0
- package/dist/observe/response.d.ts +24 -0
- package/dist/runtime.d.ts +27 -0
- package/dist/serve/accept.d.ts +7 -0
- package/dist/serve/discovery.d.ts +56 -0
- package/dist/serve/hosted.d.ts +135 -0
- package/dist/serve/source.d.ts +48 -0
- package/dist/serve/tag-asset.generated.d.ts +14 -0
- package/dist/serve/tag.d.ts +131 -0
- package/dist/serve/twin.d.ts +162 -0
- package/dist/web.cjs +21225 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.ts +52 -0
- package/dist/web.js +6 -0
- package/dist/web.js.map +1 -0
- package/install.md +463 -0
- package/package.json +76 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../core/config.js";
|
|
2
|
+
import { type Twin, type TwinOptions } from "./twin.js";
|
|
3
|
+
/**
|
|
4
|
+
* Where a twin comes from — and, more importantly, **what a caller is allowed
|
|
5
|
+
* to wait for.**
|
|
6
|
+
*
|
|
7
|
+
* ── ⚠️ Why this exists at all ────────────────────────────────────────────────
|
|
8
|
+
* Three adapters each called `twin.resolve(path)` and each had to know, without
|
|
9
|
+
* anything in the type saying so, whether the call they were making was allowed
|
|
10
|
+
* to touch the network. They did not know. `./express` called it on **every
|
|
11
|
+
* passing GET** to decide whether to advertise, and deferred `next()` until it
|
|
12
|
+
* settled; `./web` awaited it after the customer's handler had already built
|
|
13
|
+
* the response. With a corpus-backed resolver on a cold process, both of those
|
|
14
|
+
* were a visitor waiting on OUR ingest before their page rendered — which is
|
|
15
|
+
* the one thing this package is built not to do.
|
|
16
|
+
*
|
|
17
|
+
* So the rule is expressed as two methods instead of a comment:
|
|
18
|
+
*
|
|
19
|
+
* - **`lookup` is synchronous and never does I/O.** Anything running on a
|
|
20
|
+
* request that did not ask for markdown uses it. A cold process answers
|
|
21
|
+
* `undefined`, advertises nothing, and is correct.
|
|
22
|
+
* - **`resolve` may await, and only on a request that asked** — a `.md` path
|
|
23
|
+
* or `Accept: text/markdown`. Bounded, and falling through to the
|
|
24
|
+
* customer's own handler on a miss, a throw or the deadline.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ A caller cannot get this wrong by forgetting a comment any more; it gets
|
|
27
|
+
* it wrong by calling the wrong method, which is visible in a diff.
|
|
28
|
+
*/
|
|
29
|
+
export interface TwinSource {
|
|
30
|
+
lookup: (path: string) => Twin | undefined;
|
|
31
|
+
resolve: (path: string) => Twin | null | undefined | Promise<Twin | null | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Load the corpus in the background. **Never awaited on a request path.**
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ Handed to whatever channel the adapter already has — `after` on Next,
|
|
36
|
+
* `waitUntil` on a Worker, fire-and-forget on a long-lived Node process. It
|
|
37
|
+
* is a clock comparison in the common case, so calling it per request is
|
|
38
|
+
* cheap and calling it never is the bug: `resolve` refreshes only while the
|
|
39
|
+
* process is cold, so a corpus that is never refreshed is **frozen at its
|
|
40
|
+
* first load** — new twins never appear and withdrawals never take effect.
|
|
41
|
+
*/
|
|
42
|
+
warm: () => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* ⚠️ Built ONCE per middleware, not per request. The corpus is the point: a
|
|
46
|
+
* source rebuilt per request holds nothing and refetches everything.
|
|
47
|
+
*/
|
|
48
|
+
export declare function twinSourceFor(twin: TwinOptions, config: ResolvedConfig): TwinSource;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠️ GENERATED by `scripts/build-tag-asset.ts` from `packages/tag/dist/t.js`.
|
|
3
|
+
* Do not edit by hand -- `tag-asset.test.ts` regenerates this and fails if the
|
|
4
|
+
* two disagree.
|
|
5
|
+
*
|
|
6
|
+
* The SDK holds the page tag as a STRING, never as an import:
|
|
7
|
+
* `@agenthoney/tag` is private and unpublished, so a surviving import would
|
|
8
|
+
* make the published SDK uninstallable and `check-sdk-artifact.mjs` refuses
|
|
9
|
+
* one anyway.
|
|
10
|
+
*/
|
|
11
|
+
/** The built tag, with its configuration placeholder still in place. */
|
|
12
|
+
export declare const TAG_SOURCE = "\"use strict\";(()=>{var y=\"__AGENTHONEY_TAG_CONFIG__\";function l(){try{let e=JSON.parse(y);return!e||typeof e!=\"object\"||typeof e.key!=\"string\"||!e.key||typeof e.endpoint!=\"string\"||!e.endpoint||e.deny!==void 0&&!Array.isArray(e.deny)?null:e}catch{return null}}function d(e,t){if(!t||!t.length)return!1;for(let n=0;n<t.length;n+=1){let r=t[n];if(typeof r!=\"string\")continue;let i=r.trim();if(i===\"/\")return!0;let o=i.replace(/\\/+$/,\"\");if(!(!o||o.charAt(0)!==\"/\")&&(e===o||e.indexOf(`${o}/`)===0))return!0}return!1}function u(e){try{return e()}catch{return}}var T=new Set([\"SCRIPT\",\"STYLE\",\"NOSCRIPT\",\"TEMPLATE\",\"SVG\",\"CANVAS\",\"IFRAME\",\"OBJECT\",\"EMBED\",\"VIDEO\",\"AUDIO\",\"INPUT\",\"TEXTAREA\",\"SELECT\",\"OPTION\",\"BUTTON\"]),b=[\"main\",\"article\",\"[role=main]\",\"#main\",\"#content\"],v=[[/[^\\s@<>()[\\]]+@[^\\s@<>()[\\]]+\\.[a-z]{2,}/gi,\"[redacted]\"],[/\\b(?:\\d[ -]?){13,19}\\b/g,\"[redacted]\"],[/\\b\\d{9,}\\b/g,\"[redacted]\"]];function C(e){return!!(e.getAttribute(\"aria-hidden\")===\"true\"||e.hasAttribute(\"hidden\")||e.hasAttribute(\"data-agenthoney-private\"))}function E(e){var r;let t=e.cloneNode(!0),n=t.querySelectorAll(\"*\");for(let i=0;i<n.length;i+=1){let o=n[i];if(!o)continue;if(T.has(o.tagName)||C(o)){o.remove();continue}let a=o.attributes;for(let s=a.length-1;s>=0;s-=1){let c=(r=a[s])==null?void 0:r.name;c&&c!==\"href\"&&c!==\"src\"&&c!==\"alt\"&&c!==\"title\"&&o.removeAttribute(c)}}return t}function f(e){let t=e;for(let[n,r]of v)t=t.replace(n,r);return t}function g(e){return u(()=>{var i,o;let t=null;for(let a of b)if(t=e.querySelector(a),t)break;if(t||(t=e.body),!t)return;let n=E(t),r=f((i=n.textContent)!=null?i:\"\").replace(/\\s+/g,\" \").trim();if(!(r.length<200))return{html:f(n.innerHTML),title:f((o=e.title)!=null?o:\"\").slice(0,300),textLength:r.length}})}async function m(e){var t,n;try{let r=(t=globalThis.crypto)==null?void 0:t.subtle;if(!r)return;let i=new TextEncoder().encode(e),o=await r.digest(\"SHA-256\",i),a=new Uint8Array(o),s=\"\";for(let c=0;c<a.length;c+=1)s+=((n=a[c])!=null?n:0).toString(16).padStart(2,\"0\");return s}catch{return}}var A=512*1024;function w(e){try{return new URL(e).origin}catch{return}}async function h(e){try{let t=w(e.config.endpoint);if(!t)return\"failed\";let n=g(e.doc);if(!n)return\"no_content\";let r=await m(n.html);if(!r)return\"no_hash\";let i=`?k=${encodeURIComponent(e.config.key)}&p=${encodeURIComponent(e.path)}&h=${r}`,o=await e.fetchImpl(`${t}/v1/twin-state${i}`,{method:\"GET\",credentials:\"omit\",mode:\"cors\",cache:\"no-store\"});if(!o.ok)return\"failed\";let a=await o.json();if((a==null?void 0:a.want)!==!0)return\"not_wanted\";if(n.html.length>A)return\"too_large\";let s=JSON.stringify({k:e.config.key,path:e.path,contentHash:r,title:n.title,textLength:n.textLength,html:n.html});return(await e.fetchImpl(`${t}/v1/twin-content`,{method:\"POST\",credentials:\"omit\",mode:\"cors\",headers:{\"content-type\":\"text/plain;charset=UTF-8\"},body:s})).ok?\"uploaded\":\"failed\"}catch{return\"failed\"}}var p=\"0.3.0\";function x(e){let t=e.slice(),n=((...r)=>{u(()=>{t.length<128&&t.push(r)})});return n.q=t,Object.defineProperty(n,\"version\",{value:p,enumerable:!0}),Object.defineProperty(n,\"config\",{value:l(),enumerable:!0}),n}u(()=>{var n;if(typeof window==\"undefined\")return;let e=window.agenthoney,t=(n=u(()=>Array.isArray(e==null?void 0:e.q)?e.q:[]))!=null?n:[];e&&e.version===p||(window.agenthoney=x(t),S())});function S(){u(()=>{var a,s,c;let e=l();if(!e||e.harvest!==!0)return;let t=(s=(a=document.querySelector('meta[name=\"robots\"]'))==null?void 0:a.getAttribute(\"content\"))!=null?s:\"\";if(/noindex|none/i.test(t)||d(location.pathname,e.deny)||((c=navigator.connection)==null?void 0:c.saveData)===!0||typeof navigator.hardwareConcurrency==\"number\"&&navigator.hardwareConcurrency<=2)return;let r=()=>{u(()=>{h({doc:document,config:e,fetchImpl:fetch.bind(globalThis),path:location.pathname})})},i=globalThis.requestIdleCallback,o=()=>{i?i(r):setTimeout(r,2e3)};document.readyState===\"complete\"?o():window.addEventListener(\"load\",o,{once:!0,passive:!0})})}})();\n";
|
|
13
|
+
/** Of the UNSUBSTITUTED source, so it identifies the build and not the site. */
|
|
14
|
+
export declare const TAG_SHA256 = "0421439eca918bb309b6fdd572e7db685466fb2ae25a7a93337c52811502e7e6";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { TAG_SHA256 } from "./tag-asset.generated.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serving the page tag from the customer's own origin.
|
|
4
|
+
*
|
|
5
|
+
* ── ⚠️ Served, never injected ────────────────────────────────────────────────
|
|
6
|
+
* The obvious design is to insert a `<script>` into the HTML on the way past.
|
|
7
|
+
* It is prohibited, for three independent reasons:
|
|
8
|
+
*
|
|
9
|
+
* 1. It is **body rewriting**. `res` is a single-read stream; monkey-patching
|
|
10
|
+
* `write`/`end` to splice a tag in adds latency, adds memory, and can
|
|
11
|
+
* corrupt what the customer sends. This middleware has no body path at all
|
|
12
|
+
* today and must not grow one.
|
|
13
|
+
* 2. An **inline script breaks a nonce-based CSP**. That is not hypothetical
|
|
14
|
+
* -- **R6** is written down about Cloudflare's Scrape Shield doing exactly
|
|
15
|
+
* this. An external SAME-ORIGIN script satisfies `script-src 'self'` with
|
|
16
|
+
* no nonce at all.
|
|
17
|
+
* 3. Injection would make the tag impossible to remove without removing the
|
|
18
|
+
* middleware, which is the wrong kill switch for the half that runs in
|
|
19
|
+
* somebody else's browser.
|
|
20
|
+
*
|
|
21
|
+
* So: the adapter serves the file, and the customer writes one `<script async>`
|
|
22
|
+
* themselves. Serving it ourselves is what lets the configuration be baked into
|
|
23
|
+
* the bytes, so their HTML has nothing in it that can be pasted wrong -- which
|
|
24
|
+
* matters more here than it would elsewhere, because this product's real
|
|
25
|
+
* install path is an AI coding agent reading `/install.md`.
|
|
26
|
+
*
|
|
27
|
+
* ── ⚠️ First-party, so no third-party origin is involved ─────────────────────
|
|
28
|
+
* Same-origin means no extra DNS, no extra TLS, no `*.agenthoney.ai` in the
|
|
29
|
+
* customer's `script-src`, and nothing for a tracker blocker to match on the
|
|
30
|
+
* script itself.
|
|
31
|
+
*/
|
|
32
|
+
/** ⚠️ Must match `RAW` in `packages/tag/src/config.ts`. */
|
|
33
|
+
declare const TAG_CONFIG_PLACEHOLDER = "__AGENTHONEY_TAG_CONFIG__";
|
|
34
|
+
/** Where the adapters serve it. Underscored so it cannot collide with a route. */
|
|
35
|
+
export declare const TAG_PATH = "/_agenthoney/t.js";
|
|
36
|
+
export interface TagOptions {
|
|
37
|
+
/**
|
|
38
|
+
* The site's PUBLIC key, `ep_live_public_…`.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ Not a secret, and cannot be made one -- it is served inside this file to
|
|
41
|
+
* anyone who asks. What makes that safe is the class: `authenticate()` in
|
|
42
|
+
* ingest requires `server` on `/v1/events`, so a public key cannot write
|
|
43
|
+
* events. Never put `AGENTHONEY_SERVER_KEY` here; the SDK's own
|
|
44
|
+
* `check-browser-safety.mjs` exists to stop that value reaching a browser.
|
|
45
|
+
*/
|
|
46
|
+
publicKey: string;
|
|
47
|
+
/**
|
|
48
|
+
* Where the tag will post. An ORIGIN; the tag appends its own paths.
|
|
49
|
+
* Defaults to the origin of the SDK's configured ingest URL.
|
|
50
|
+
*/
|
|
51
|
+
endpoint?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Let the tag read this site's rendered pages and offer them as twins.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ **Site-level, and off by default.** The plan for this phase said the
|
|
56
|
+
* middleware would refuse harvesting per request -- when the request carried
|
|
57
|
+
* a session cookie or an `Authorization` header -- and that is **not
|
|
58
|
+
* implementable**: this file is one shared artifact, cached, served to every
|
|
59
|
+
* visitor. A flag baked into it cannot describe the request that will later
|
|
60
|
+
* load a page. Discovering that late is exactly why it is written here.
|
|
61
|
+
*
|
|
62
|
+
* What actually protects a signed-in visitor's page is the consensus gate:
|
|
63
|
+
* nothing is published until several DISTINCT anonymous visitors
|
|
64
|
+
* independently produced the same content, so personalised and authenticated
|
|
65
|
+
* pages never converge. The tag additionally skips any page the site itself
|
|
66
|
+
* marks `noindex`, which is the owner saying "not for machines" in the one
|
|
67
|
+
* place they already say it.
|
|
68
|
+
*/
|
|
69
|
+
harvest?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Path prefixes the tag must never read on this site.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ **A deny list, not an allow list, and the difference is whether anybody
|
|
74
|
+
* will fill it in.** An allow list asks a client-rendered site's operator
|
|
75
|
+
* for a complete route inventory -- which is the hand-authoring burden the
|
|
76
|
+
* tag exists to remove -- while this asks only for the prefixes a signed-in
|
|
77
|
+
* user lands on, which an operator knows without looking anything up.
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ Enforced again at ingest from the SITE's own record, because this file
|
|
80
|
+
* is cached for five minutes and by whatever CDN fronts it. What is baked in
|
|
81
|
+
* here saves the browser the work; what is on the server is the control.
|
|
82
|
+
*/
|
|
83
|
+
harvestDeny?: readonly string[];
|
|
84
|
+
/**
|
|
85
|
+
* ⚠️ Short on purpose. A revoked public key must stop being handed out, and a
|
|
86
|
+
* long cache on the file that CONTAINS it would make revocation meaningless.
|
|
87
|
+
* Five minutes is long enough to matter for a busy site and short enough that
|
|
88
|
+
* a rotation takes effect while somebody is still watching.
|
|
89
|
+
*/
|
|
90
|
+
cacheControl?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Where the tag will post, derived from the SDK's own ingest URL.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ Derived rather than defaulted to a constant, so a customer pointed at a
|
|
96
|
+
* staging ingest does not have their browser traffic silently going to
|
|
97
|
+
* production.
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ **An ORIGIN, and it used to be a route that does not exist.** This
|
|
100
|
+
* returned `<origin>/v1/signals` until Phase 35, and the install guide told
|
|
101
|
+
* every customer to construct that path by hand. Ingest has never served it.
|
|
102
|
+
* Nothing broke, because the tag takes `new URL(endpoint).origin` and appends
|
|
103
|
+
* its own paths -- so the wrong half was silently discarded on every page load,
|
|
104
|
+
* and the guide taught a URL that 404s if anyone ever fetched it. That is the
|
|
105
|
+
* same silent-misconfiguration shape the bare-origin `ingestUrl` bug had in
|
|
106
|
+
* Phase 32, and it is corrected the same way: emit the thing that is true.
|
|
107
|
+
*
|
|
108
|
+
* Returns `""` when there is nothing to derive from, and the caller treats that
|
|
109
|
+
* as "do not serve the tag" -- a tag with no endpoint is a file that runs in
|
|
110
|
+
* somebody's browser for no reason.
|
|
111
|
+
*/
|
|
112
|
+
export declare function tagEndpointFor(ingestUrl: string): string;
|
|
113
|
+
export interface RenderedTag {
|
|
114
|
+
body: string;
|
|
115
|
+
headers: Record<string, string>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* ⚠️ Throws on a placeholder count that is not exactly one, rather than serving
|
|
119
|
+
* a best-effort file.
|
|
120
|
+
*
|
|
121
|
+
* Zero would mean a build that eliminated the placeholder, and every visitor
|
|
122
|
+
* would run an inert tag that reports nothing -- a silent, total failure that
|
|
123
|
+
* looks exactly like "no traffic". More than one would mean substituting
|
|
124
|
+
* something that was not meant to be substituted. Both are bugs in OUR build,
|
|
125
|
+
* caught here at wiring time; the caller wraps this so a throw can never reach
|
|
126
|
+
* the customer's response.
|
|
127
|
+
*/
|
|
128
|
+
export declare function renderTag(options: TagOptions & {
|
|
129
|
+
endpoint: string;
|
|
130
|
+
}): RenderedTag;
|
|
131
|
+
export { TAG_CONFIG_PLACEHOLDER, TAG_SHA256 };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown twin: the decision, as a pure function.
|
|
3
|
+
*
|
|
4
|
+
* ── Why the decision is separated from the serving ───────────────────────────
|
|
5
|
+
* Two adapters have to make the identical choice, and the choice is the part
|
|
6
|
+
* with consequences -- serve the wrong thing and a human gets a text file
|
|
7
|
+
* instead of a website. One function, one set of tests, two thin call sites.
|
|
8
|
+
*/
|
|
9
|
+
export interface Twin {
|
|
10
|
+
body: string;
|
|
11
|
+
/** Defaults to `text/markdown; charset=utf-8`. */
|
|
12
|
+
contentType?: string;
|
|
13
|
+
etag?: string;
|
|
14
|
+
lastModified?: string;
|
|
15
|
+
}
|
|
16
|
+
export type TwinResolver = (path: string) => Twin | null | undefined | Promise<Twin | null | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Serve twins from the corpus this site's own visitors wrote.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ **`true` is the whole configuration.** The server key and ingest URL are
|
|
21
|
+
* already in `AgentHoneyConfig` for the observe half, so a corpus-backed
|
|
22
|
+
* install is one word rather than the lazy singleton, the `waitUntil` and the
|
|
23
|
+
* hand-wired resolver that `apps/www` had to assemble -- which, until Phase 49,
|
|
24
|
+
* was the only place anyone had ever done it, because `hostedTwins` appeared in
|
|
25
|
+
* exactly zero customer-facing documents.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ **It stays OPT-IN even though we know the key.** Turning it on changes
|
|
28
|
+
* what a visitor receives under the customer's own domain, and doing that
|
|
29
|
+
* because somebody took a patch upgrade is not a thing this package does.
|
|
30
|
+
*/
|
|
31
|
+
export type HostedTwinOptions = true | {
|
|
32
|
+
/** Defaults to the SDK's configured server key. */
|
|
33
|
+
serverKey?: string;
|
|
34
|
+
/** Defaults to the SDK's configured ingest URL. */
|
|
35
|
+
ingestUrl?: string;
|
|
36
|
+
/** How stale the in-memory set may get. Default five minutes. */
|
|
37
|
+
refreshMs?: number;
|
|
38
|
+
/** The ceiling on the one await a COLD process is allowed. */
|
|
39
|
+
coldTimeoutMs?: number;
|
|
40
|
+
/** How long a failed refresh holds off the next. Default thirty seconds. */
|
|
41
|
+
failureBackoffMs?: number;
|
|
42
|
+
/** The ceiling on a single refresh's network call. Default five seconds. */
|
|
43
|
+
refreshTimeoutMs?: number;
|
|
44
|
+
/** Injected for tests. */
|
|
45
|
+
fetchImpl?: typeof fetch;
|
|
46
|
+
};
|
|
47
|
+
interface TwinOptionsBase {
|
|
48
|
+
/** `Cache-Control` for a served twin. Edge-cacheable by default. */
|
|
49
|
+
cacheControl?: string;
|
|
50
|
+
/** Advertise an available twin on the HTML response. Default true. */
|
|
51
|
+
advertise?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Serve `/llms.txt`, `/llms-full.txt` and `/install.md` from the same
|
|
54
|
+
* manifest that serves the twins.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ Derived, not written: a hand-maintained index is wrong the first time a
|
|
57
|
+
* page is added, and an index listing pages that no longer exist is worse
|
|
58
|
+
* than none -- an agent spends its budget on 404s and concludes the site is
|
|
59
|
+
* broken.
|
|
60
|
+
*/
|
|
61
|
+
discovery?: import("./discovery.js").DiscoveryOptions;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* ⚠️ **A union, so each half is sufficient alone and neither loses its type.**
|
|
65
|
+
*
|
|
66
|
+
* `resolve` is for a site that already HAS markdown; `hosted` is for one whose
|
|
67
|
+
* twins were written by its own visitors. Supplying both is allowed and
|
|
68
|
+
* `resolve` wins -- authored markdown beats harvested markdown, every time,
|
|
69
|
+
* because the customer wrote one of them on purpose.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ This widens a published surface rather than retyping one: every existing
|
|
72
|
+
* caller passes `resolve` and keeps working, which is what the append-only rule
|
|
73
|
+
* requires.
|
|
74
|
+
*/
|
|
75
|
+
export type TwinOptions = TwinOptionsBase & ({
|
|
76
|
+
/**
|
|
77
|
+
* Find the twin for a normalised path, or return null.
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ **Registered, not derived.** Returning a twin for any path that
|
|
80
|
+
* "looks like" it should have one produces a site where every URL
|
|
81
|
+
* answers, including the typos -- caprail.dev returns a plain 404 for
|
|
82
|
+
* an unregistered `.md`, and that is the behaviour to match.
|
|
83
|
+
*/
|
|
84
|
+
resolve: TwinResolver;
|
|
85
|
+
hosted?: HostedTwinOptions;
|
|
86
|
+
} | {
|
|
87
|
+
resolve?: TwinResolver;
|
|
88
|
+
hosted: HostedTwinOptions;
|
|
89
|
+
});
|
|
90
|
+
/** Paths the discovery block answers, when it is configured. */
|
|
91
|
+
export declare const DISCOVERY_PATHS: readonly ["/llms.txt", "/llms-full.txt", "/install.md"];
|
|
92
|
+
export declare const DEFAULT_TWIN_CONTENT_TYPE = "text/markdown; charset=utf-8";
|
|
93
|
+
export declare const DEFAULT_TWIN_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
|
|
94
|
+
export type TwinDecision =
|
|
95
|
+
/** Serve the twin for `lookupPath`. */
|
|
96
|
+
{
|
|
97
|
+
action: "serve";
|
|
98
|
+
lookupPath: string;
|
|
99
|
+
reason: "md_path" | "accept_header";
|
|
100
|
+
}
|
|
101
|
+
/** Not ours to answer, but a twin may exist worth advertising. */
|
|
102
|
+
| {
|
|
103
|
+
action: "pass";
|
|
104
|
+
reason: "not_get" | "no_signal";
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Map a request onto a decision. Nothing here touches I/O.
|
|
108
|
+
*
|
|
109
|
+
* Two gates, and only two:
|
|
110
|
+
* 1. the path ends in `.md` -- a distinct resource, no negotiation at all;
|
|
111
|
+
* 2. `Accept` explicitly prefers markdown -- content negotiation, which owes
|
|
112
|
+
* a `Vary: Accept`.
|
|
113
|
+
*
|
|
114
|
+
* ⚠️ Never the User-Agent. See `accept.ts`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function decideTwin(input: {
|
|
117
|
+
method: string;
|
|
118
|
+
path: string;
|
|
119
|
+
accept: string | null | undefined;
|
|
120
|
+
}): TwinDecision;
|
|
121
|
+
/**
|
|
122
|
+
* `/docs/intro.md` -> `/docs/intro`, and `/index.md` -> `/`.
|
|
123
|
+
*
|
|
124
|
+
* The root is the case worth naming: a site's home page has no slug, so its
|
|
125
|
+
* twin is `/index.md` by convention -- the same convention caprail.dev uses.
|
|
126
|
+
*/
|
|
127
|
+
export declare function stripMdSuffix(path: string): string;
|
|
128
|
+
/** `/docs/intro` -> `/docs/intro.md`, and `/` -> `/index.md`. */
|
|
129
|
+
export declare function twinPathFor(path: string): string;
|
|
130
|
+
export interface TwinResponse {
|
|
131
|
+
status: number;
|
|
132
|
+
headers: Record<string, string>;
|
|
133
|
+
body: string;
|
|
134
|
+
}
|
|
135
|
+
export declare function buildTwinResponse(twin: Twin, decision: Extract<TwinDecision, {
|
|
136
|
+
action: "serve";
|
|
137
|
+
}>, options: TwinOptions): TwinResponse;
|
|
138
|
+
/**
|
|
139
|
+
* The advertisement for an available twin, as a `Link` header.
|
|
140
|
+
*
|
|
141
|
+
* ⚠️ **A header, not a tag injected into the HTML.** Caprail puts
|
|
142
|
+
* `<link rel="alternate">` in its own document head, which it can do because it
|
|
143
|
+
* owns the template. A middleware cannot: injecting into the body means
|
|
144
|
+
* buffering and rewriting somebody else's response, which is precisely what
|
|
145
|
+
* this package refuses to do. RFC 8288 makes the header the equivalent, it
|
|
146
|
+
* costs nothing, and it works for JSON and plain text as well as HTML.
|
|
147
|
+
*
|
|
148
|
+
* A customer who also wants the tag can add it to their own template, and
|
|
149
|
+
* should -- some crawlers read one and not the other.
|
|
150
|
+
*/
|
|
151
|
+
/**
|
|
152
|
+
* Is a resolver's answer a promise?
|
|
153
|
+
*
|
|
154
|
+
* ⚠️ **Asked so that the ADVERTISE path can decline to wait.** `resolve` may
|
|
155
|
+
* return a value or a promise, and the difference decides whether a caller is
|
|
156
|
+
* allowed to use the answer: a request that has not asked for markdown must
|
|
157
|
+
* never be delayed for one. Duck-typed rather than `instanceof Promise`,
|
|
158
|
+
* because a customer's resolver may return a thenable from any library.
|
|
159
|
+
*/
|
|
160
|
+
export declare function isThenable<T>(value: T | Promise<T>): value is Promise<T>;
|
|
161
|
+
export declare function advertiseHeader(path: string): string;
|
|
162
|
+
export {};
|