@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,148 @@
|
|
|
1
|
+
import type { Twin } from "./serve/twin.js";
|
|
2
|
+
/**
|
|
3
|
+
* Answer pages, for a framework that renders them itself (Phase 95).
|
|
4
|
+
*
|
|
5
|
+
* ── ⚠️ Why this is NOT the middleware, and may await ─────────────────────────
|
|
6
|
+
* Everything else in this package obeys one rule: **nothing awaits the network
|
|
7
|
+
* on the customer's request path.** This module does await, and the difference
|
|
8
|
+
* is where it runs. The middleware runs on every request to a customer's site,
|
|
9
|
+
* for traffic that never asked us for anything; these functions run inside a
|
|
10
|
+
* route or page the customer wrote, at build time or behind their framework's
|
|
11
|
+
* own data cache, for a request that is asking for the answer page itself.
|
|
12
|
+
* That is their own data fetch, not our code taxing somebody else's traffic.
|
|
13
|
+
*
|
|
14
|
+
* The rule that does survive, unchanged: **a failure is never the customer's
|
|
15
|
+
* problem.** Nothing here throws. A dead ingest, a revoked key, a malformed
|
|
16
|
+
* body — all of them answer "no pages", so the route renders its own 404 and
|
|
17
|
+
* the site works normally. `verify-fail-open.sh` is the standing argument.
|
|
18
|
+
*
|
|
19
|
+
* ── ⚠️ Why the HTML is ours and the page is theirs ───────────────────────────
|
|
20
|
+
* `get()` hands back rendered HTML alongside the markdown. It is rendered where
|
|
21
|
+
* the body was written and escaped there (`@agenthoney/answers`), so no
|
|
22
|
+
* client needs a markdown library and no client re-implements the escaping that
|
|
23
|
+
* keeps a quoted passage from becoming markup. What wraps it is the customer's
|
|
24
|
+
* own layout: this package ships no chrome, no CSS and no document.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* // app/answers/[slug]/page.tsx
|
|
28
|
+
* import { createAnswers } from "@agenthoney/analytics/answers";
|
|
29
|
+
* const answers = createAnswers({ serverKey: process.env.AGENTHONEY_SERVER_KEY! });
|
|
30
|
+
*
|
|
31
|
+
* export async function generateStaticParams() {
|
|
32
|
+
* return (await answers.list()).map((a) => ({ slug: a.slug }));
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* export default async function Page({ params }) {
|
|
36
|
+
* const answer = await answers.get((await params).slug);
|
|
37
|
+
* if (!answer) notFound();
|
|
38
|
+
* return <article dangerouslySetInnerHTML={{ __html: answer.html }} />;
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* // app/answers/page.tsx — the index at the folder root (0.13.0), with
|
|
42
|
+
* // `serveIndex: true` passed to `createAnswers` wherever the resolver is built
|
|
43
|
+
* export default async function Index() {
|
|
44
|
+
* const index = await answers.index();
|
|
45
|
+
* if (!index) notFound();
|
|
46
|
+
* return <article dangerouslySetInnerHTML={{ __html: index.html }} />;
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface AnswerPage {
|
|
51
|
+
/** The path on the customer's own site, normalised: `/answers/how-to`. */
|
|
52
|
+
path: string;
|
|
53
|
+
slug: string;
|
|
54
|
+
title: string;
|
|
55
|
+
markdown: string;
|
|
56
|
+
/** Rendered by us, escaped by us. A fragment — never a document. */
|
|
57
|
+
html: string;
|
|
58
|
+
contentHash: string;
|
|
59
|
+
updatedAt: string;
|
|
60
|
+
}
|
|
61
|
+
export interface AnswersOptions {
|
|
62
|
+
/** The SERVER key. This corpus is not public and a browser never reads it. */
|
|
63
|
+
serverKey: string;
|
|
64
|
+
/** Defaults to the SDK's own ingest origin; the `/v1/events` form is accepted. */
|
|
65
|
+
ingestUrl?: string;
|
|
66
|
+
/**
|
|
67
|
+
* How long a framework may cache the corpus, in seconds. Passed to `fetch`
|
|
68
|
+
* as `next.revalidate`, which plain `fetch` ignores harmlessly.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ **A cache, not a stale read.** Without it a statically generated page is
|
|
71
|
+
* frozen at build time and a withdrawal never reaches it — which is the
|
|
72
|
+
* failure that matters here, since a withdrawal is how an operator takes
|
|
73
|
+
* words off their own domain.
|
|
74
|
+
*/
|
|
75
|
+
revalidateSeconds?: number;
|
|
76
|
+
/**
|
|
77
|
+
* This site routes the index at the folder root (`app/answers/page.tsx`), so
|
|
78
|
+
* `twinResolver()` and `sitemapEntries()` may include it (0.13.0).
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ **Off by default, deliberately.** Without the route the folder root is
|
|
81
|
+
* the customer's own page, or a 404, and a resolver that answered it anyway
|
|
82
|
+
* would serve our markdown for their page — and list it in their sitemap —
|
|
83
|
+
* on every site that upgraded without adding the route (security review).
|
|
84
|
+
* `index()` needs no flag: the route calling it is the evidence.
|
|
85
|
+
*/
|
|
86
|
+
serveIndex?: boolean;
|
|
87
|
+
/** Injected for tests. */
|
|
88
|
+
fetchImpl?: typeof fetch;
|
|
89
|
+
/** Injected for tests. */
|
|
90
|
+
now?: () => number;
|
|
91
|
+
}
|
|
92
|
+
export interface Answers {
|
|
93
|
+
/** Every published page. Empty on any failure — never a throw. */
|
|
94
|
+
list: () => Promise<AnswerPage[]>;
|
|
95
|
+
/** One page by slug, or `undefined`: a route renders its own 404. */
|
|
96
|
+
get: (slug: string) => Promise<AnswerPage | undefined>;
|
|
97
|
+
/**
|
|
98
|
+
* The index at the folder root (0.13.0), or `undefined` when the site has
|
|
99
|
+
* not published one — so the route renders its own 404.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ **Never among `list()`.** Its path is the folder itself and its slug is
|
|
102
|
+
* empty; a `generateStaticParams` built from `list()` must not mint a page
|
|
103
|
+
* for it, which is why ingest sends it in a field of its own.
|
|
104
|
+
*/
|
|
105
|
+
index: () => Promise<AnswerPage | undefined>;
|
|
106
|
+
/** The folder these pages live under, as the site has it set. */
|
|
107
|
+
folder: () => Promise<string>;
|
|
108
|
+
/**
|
|
109
|
+
* Entries for the customer's own sitemap.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ **Their sitemap, not ours.** The pages are on their domain and their
|
|
112
|
+
* framework generates the file; a sitemap of ours on our host would be the
|
|
113
|
+
* cross-site submission that proves nothing about who owns the pages.
|
|
114
|
+
*/
|
|
115
|
+
sitemapEntries: (origin: string) => Promise<{
|
|
116
|
+
url: string;
|
|
117
|
+
lastModified: Date;
|
|
118
|
+
}[]>;
|
|
119
|
+
/**
|
|
120
|
+
* A `twin.resolve` for the middleware, so `/answers/x.md` and
|
|
121
|
+
* `Accept: text/markdown` answer with the page's markdown.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ **SYNCHRONOUS, and it never starts a fetch — this is the important part
|
|
124
|
+
* of this module.** `twinSourceFor` calls a customer's resolver from
|
|
125
|
+
* `lookup`, and `lookup` runs on EVERY passing GET to decide whether to
|
|
126
|
+
* advertise (`express.ts`, `web.ts`). An async resolver looks harmless there,
|
|
127
|
+
* because the promise is dropped — but the request it started was already on
|
|
128
|
+
* the wire, so every visitor pageview would fire a call to our ingest from
|
|
129
|
+
* inside the customer's process, discard it, and burn their `/v1/answers`
|
|
130
|
+
* throttle. That is precisely what `source.ts`'s Phase 49 docblock exists to
|
|
131
|
+
* prevent, defeated from the other side.
|
|
132
|
+
*
|
|
133
|
+
* So this reads the in-process cache and answers `undefined` when cold, which
|
|
134
|
+
* is the truth: this process does not know of an answer page yet. `list`,
|
|
135
|
+
* `get` and `warm` — all of which run in the customer's own route — are what
|
|
136
|
+
* fill it.
|
|
137
|
+
*/
|
|
138
|
+
twinResolver: () => (path: string) => Twin | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* Refresh the cache if it is stale. Never throws, never blocks a visitor.
|
|
141
|
+
*
|
|
142
|
+
* ⚠️ For a host that wants the twin resolver warm without rendering a page
|
|
143
|
+
* first: hand it to `after` on Next, `waitUntil` on a Worker, or call it from
|
|
144
|
+
* a route. It is a clock comparison in the common case.
|
|
145
|
+
*/
|
|
146
|
+
warm: () => Promise<void>;
|
|
147
|
+
}
|
|
148
|
+
export declare function createAnswers(options: AnswersOptions): Answers;
|
package/dist/answers.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { normalisePath } from './chunk-F3PRHEXB.js';
|
|
2
|
+
|
|
3
|
+
// src/answers.ts
|
|
4
|
+
function originOf(url) {
|
|
5
|
+
try {
|
|
6
|
+
return new URL(url).origin;
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var REFRESH_MS = 3e5;
|
|
12
|
+
var FAILURE_BACKOFF_MS = 3e4;
|
|
13
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
14
|
+
function createAnswers(options) {
|
|
15
|
+
const base = originOf(options.ingestUrl ?? "https://ingest.agenthoney.ai/v1/events");
|
|
16
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
17
|
+
const revalidate = options.revalidateSeconds ?? 300;
|
|
18
|
+
const now = options.now ?? (() => Date.now());
|
|
19
|
+
let folder = "/answers";
|
|
20
|
+
let pages = [];
|
|
21
|
+
let indexPage;
|
|
22
|
+
let byPath = /* @__PURE__ */ new Map();
|
|
23
|
+
let fetchedAt = 0;
|
|
24
|
+
let failedAt = 0;
|
|
25
|
+
let inFlight;
|
|
26
|
+
function abortAfter(ms) {
|
|
27
|
+
const Controller = globalThis.AbortController;
|
|
28
|
+
if (!Controller) return { signal: void 0, done: () => {
|
|
29
|
+
} };
|
|
30
|
+
const controller = new Controller();
|
|
31
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
32
|
+
timer.unref?.();
|
|
33
|
+
return { signal: controller.signal, done: () => clearTimeout(timer) };
|
|
34
|
+
}
|
|
35
|
+
async function refresh() {
|
|
36
|
+
if (!base) return;
|
|
37
|
+
const deadline = abortAfter(FETCH_TIMEOUT_MS);
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetchImpl(`${base}/v1/answers`, {
|
|
40
|
+
headers: { authorization: `Bearer ${options.serverKey}` },
|
|
41
|
+
// ⚠️ An unknown option, and that is fine: Next reads it, every other
|
|
42
|
+
// runtime ignores it. Declaring it here rather than asking the customer
|
|
43
|
+
// to remember it is the difference between a page that updates and one
|
|
44
|
+
// frozen at its build.
|
|
45
|
+
next: { revalidate },
|
|
46
|
+
...deadline.signal ? { signal: deadline.signal } : {}
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
failedAt = now();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const body = await response.json();
|
|
53
|
+
if (!Array.isArray(body.answers)) {
|
|
54
|
+
failedAt = now();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const next = [];
|
|
58
|
+
for (const row of body.answers) {
|
|
59
|
+
const page = pageOf(row);
|
|
60
|
+
if (page) next.push(page);
|
|
61
|
+
}
|
|
62
|
+
const nextIndex = pageOf(body.index);
|
|
63
|
+
pages = next;
|
|
64
|
+
indexPage = nextIndex;
|
|
65
|
+
byPath = new Map([...next, ...nextIndex && options.serveIndex ? [nextIndex] : []].map((page) => [page.path, page]));
|
|
66
|
+
folder = typeof body.folder === "string" ? body.folder : folder;
|
|
67
|
+
fetchedAt = now();
|
|
68
|
+
failedAt = 0;
|
|
69
|
+
} catch {
|
|
70
|
+
failedAt = now();
|
|
71
|
+
} finally {
|
|
72
|
+
deadline.done();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function refreshIfStale() {
|
|
76
|
+
if (inFlight) return inFlight;
|
|
77
|
+
if (fetchedAt !== 0 && now() - fetchedAt < REFRESH_MS) return Promise.resolve();
|
|
78
|
+
if (failedAt !== 0 && now() - failedAt < FAILURE_BACKOFF_MS) return Promise.resolve();
|
|
79
|
+
inFlight = refresh().finally(() => {
|
|
80
|
+
inFlight = void 0;
|
|
81
|
+
});
|
|
82
|
+
return inFlight;
|
|
83
|
+
}
|
|
84
|
+
function pageOf(value) {
|
|
85
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
86
|
+
const row = value;
|
|
87
|
+
if (typeof row["path"] !== "string" || typeof row["html"] !== "string") return void 0;
|
|
88
|
+
return {
|
|
89
|
+
// ⚠️ Normalised on the way IN as well as out: a client asking for
|
|
90
|
+
// `/answers/x/` and a corpus holding `/answers/x` must be one page.
|
|
91
|
+
// `Corpus.php` learned this the expensive way and `hosted.ts` carries
|
|
92
|
+
// the same note.
|
|
93
|
+
path: normalisePath(row["path"]),
|
|
94
|
+
slug: typeof row["slug"] === "string" ? row["slug"] : "",
|
|
95
|
+
title: typeof row["title"] === "string" ? row["title"] : "",
|
|
96
|
+
markdown: typeof row["markdown"] === "string" ? row["markdown"] : "",
|
|
97
|
+
html: row["html"],
|
|
98
|
+
contentHash: typeof row["contentHash"] === "string" ? row["contentHash"] : "",
|
|
99
|
+
updatedAt: typeof row["updatedAt"] === "string" ? row["updatedAt"] : ""
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const list = async () => {
|
|
103
|
+
await refreshIfStale();
|
|
104
|
+
return pages;
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
list,
|
|
108
|
+
async get(slug) {
|
|
109
|
+
return (await list()).find((page) => page.slug === slug);
|
|
110
|
+
},
|
|
111
|
+
async index() {
|
|
112
|
+
await refreshIfStale();
|
|
113
|
+
return indexPage;
|
|
114
|
+
},
|
|
115
|
+
async folder() {
|
|
116
|
+
await refreshIfStale();
|
|
117
|
+
return folder;
|
|
118
|
+
},
|
|
119
|
+
async sitemapEntries(origin) {
|
|
120
|
+
const clean = origin.replace(/\/+$/, "");
|
|
121
|
+
const all = await list();
|
|
122
|
+
return (indexPage && options.serveIndex ? [indexPage, ...all] : all).map((page) => ({
|
|
123
|
+
url: `${clean}${page.path}`,
|
|
124
|
+
lastModified: new Date(page.updatedAt || Date.now())
|
|
125
|
+
}));
|
|
126
|
+
},
|
|
127
|
+
twinResolver() {
|
|
128
|
+
return (path) => {
|
|
129
|
+
const page = byPath.get(normalisePath(path));
|
|
130
|
+
return page ? { body: page.markdown } : void 0;
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
warm: refreshIfStale
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { createAnswers };
|
|
138
|
+
//# sourceMappingURL=answers.js.map
|
|
139
|
+
//# sourceMappingURL=answers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/answers.ts"],"names":[],"mappings":"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","file":"answers.js","sourcesContent":["import { normalisePath } from \"./observe/request.js\";\nimport type { Twin } from \"./serve/twin.js\";\n\n/**\n * Answer pages, for a framework that renders them itself (Phase 95).\n *\n * ── ⚠️ Why this is NOT the middleware, and may await ─────────────────────────\n * Everything else in this package obeys one rule: **nothing awaits the network\n * on the customer's request path.** This module does await, and the difference\n * is where it runs. The middleware runs on every request to a customer's site,\n * for traffic that never asked us for anything; these functions run inside a\n * route or page the customer wrote, at build time or behind their framework's\n * own data cache, for a request that is asking for the answer page itself.\n * That is their own data fetch, not our code taxing somebody else's traffic.\n *\n * The rule that does survive, unchanged: **a failure is never the customer's\n * problem.** Nothing here throws. A dead ingest, a revoked key, a malformed\n * body — all of them answer \"no pages\", so the route renders its own 404 and\n * the site works normally. `verify-fail-open.sh` is the standing argument.\n *\n * ── ⚠️ Why the HTML is ours and the page is theirs ───────────────────────────\n * `get()` hands back rendered HTML alongside the markdown. It is rendered where\n * the body was written and escaped there (`@agenthoney/answers`), so no\n * client needs a markdown library and no client re-implements the escaping that\n * keeps a quoted passage from becoming markup. What wraps it is the customer's\n * own layout: this package ships no chrome, no CSS and no document.\n *\n * ```tsx\n * // app/answers/[slug]/page.tsx\n * import { createAnswers } from \"@agenthoney/analytics/answers\";\n * const answers = createAnswers({ serverKey: process.env.AGENTHONEY_SERVER_KEY! });\n *\n * export async function generateStaticParams() {\n * return (await answers.list()).map((a) => ({ slug: a.slug }));\n * }\n *\n * export default async function Page({ params }) {\n * const answer = await answers.get((await params).slug);\n * if (!answer) notFound();\n * return <article dangerouslySetInnerHTML={{ __html: answer.html }} />;\n * }\n *\n * // app/answers/page.tsx — the index at the folder root (0.13.0), with\n * // `serveIndex: true` passed to `createAnswers` wherever the resolver is built\n * export default async function Index() {\n * const index = await answers.index();\n * if (!index) notFound();\n * return <article dangerouslySetInnerHTML={{ __html: index.html }} />;\n * }\n * ```\n */\n\nexport interface AnswerPage {\n /** The path on the customer's own site, normalised: `/answers/how-to`. */\n path: string;\n slug: string;\n title: string;\n markdown: string;\n /** Rendered by us, escaped by us. A fragment — never a document. */\n html: string;\n contentHash: string;\n updatedAt: string;\n}\n\nexport interface AnswersOptions {\n /** The SERVER key. This corpus is not public and a browser never reads it. */\n serverKey: string;\n /** Defaults to the SDK's own ingest origin; the `/v1/events` form is accepted. */\n ingestUrl?: string;\n /**\n * How long a framework may cache the corpus, in seconds. Passed to `fetch`\n * as `next.revalidate`, which plain `fetch` ignores harmlessly.\n *\n * ⚠️ **A cache, not a stale read.** Without it a statically generated page is\n * frozen at build time and a withdrawal never reaches it — which is the\n * failure that matters here, since a withdrawal is how an operator takes\n * words off their own domain.\n */\n revalidateSeconds?: number;\n /**\n * This site routes the index at the folder root (`app/answers/page.tsx`), so\n * `twinResolver()` and `sitemapEntries()` may include it (0.13.0).\n *\n * ⚠️ **Off by default, deliberately.** Without the route the folder root is\n * the customer's own page, or a 404, and a resolver that answered it anyway\n * would serve our markdown for their page — and list it in their sitemap —\n * on every site that upgraded without adding the route (security review).\n * `index()` needs no flag: the route calling it is the evidence.\n */\n serveIndex?: boolean;\n /** Injected for tests. */\n fetchImpl?: typeof fetch;\n /** Injected for tests. */\n now?: () => number;\n}\n\nexport interface Answers {\n /** Every published page. Empty on any failure — never a throw. */\n list: () => Promise<AnswerPage[]>;\n /** One page by slug, or `undefined`: a route renders its own 404. */\n get: (slug: string) => Promise<AnswerPage | undefined>;\n /**\n * The index at the folder root (0.13.0), or `undefined` when the site has\n * not published one — so the route renders its own 404.\n *\n * ⚠️ **Never among `list()`.** Its path is the folder itself and its slug is\n * empty; a `generateStaticParams` built from `list()` must not mint a page\n * for it, which is why ingest sends it in a field of its own.\n */\n index: () => Promise<AnswerPage | undefined>;\n /** The folder these pages live under, as the site has it set. */\n folder: () => Promise<string>;\n /**\n * Entries for the customer's own sitemap.\n *\n * ⚠️ **Their sitemap, not ours.** The pages are on their domain and their\n * framework generates the file; a sitemap of ours on our host would be the\n * cross-site submission that proves nothing about who owns the pages.\n */\n sitemapEntries: (origin: string) => Promise<{ url: string; lastModified: Date }[]>;\n /**\n * A `twin.resolve` for the middleware, so `/answers/x.md` and\n * `Accept: text/markdown` answer with the page's markdown.\n *\n * ⚠️ **SYNCHRONOUS, and it never starts a fetch — this is the important part\n * of this module.** `twinSourceFor` calls a customer's resolver from\n * `lookup`, and `lookup` runs on EVERY passing GET to decide whether to\n * advertise (`express.ts`, `web.ts`). An async resolver looks harmless there,\n * because the promise is dropped — but the request it started was already on\n * the wire, so every visitor pageview would fire a call to our ingest from\n * inside the customer's process, discard it, and burn their `/v1/answers`\n * throttle. That is precisely what `source.ts`'s Phase 49 docblock exists to\n * prevent, defeated from the other side.\n *\n * So this reads the in-process cache and answers `undefined` when cold, which\n * is the truth: this process does not know of an answer page yet. `list`,\n * `get` and `warm` — all of which run in the customer's own route — are what\n * fill it.\n */\n twinResolver: () => (path: string) => Twin | undefined;\n /**\n * Refresh the cache if it is stale. Never throws, never blocks a visitor.\n *\n * ⚠️ For a host that wants the twin resolver warm without rendering a page\n * first: hand it to `after` on Next, `waitUntil` on a Worker, or call it from\n * a route. It is a clock comparison in the common case.\n */\n warm: () => Promise<void>;\n}\n\nfunction originOf(url: string): string | undefined {\n try {\n return new URL(url).origin;\n } catch {\n return undefined;\n }\n}\n\n/**\n * ⚠️ **The same three numbers `hostedTwins` uses, and for the same reasons**\n * (Phase 66): a refresh window so a warm process does no I/O, a failure backoff\n * so our outage does not become a fleet of customer servers retrying us\n * continuously, and a hard deadline so a HANGING ingest cannot pin a fetch for\n * as long as the runtime's socket timeout. Two sets of numbers for \"stop\n * calling a dead endpoint\" would be two behaviours to explain.\n */\nconst REFRESH_MS = 300_000;\nconst FAILURE_BACKOFF_MS = 30_000;\nconst FETCH_TIMEOUT_MS = 5_000;\n\nexport function createAnswers(options: AnswersOptions): Answers {\n const base = originOf(options.ingestUrl ?? \"https://ingest.agenthoney.ai/v1/events\");\n const fetchImpl = options.fetchImpl ?? fetch;\n const revalidate = options.revalidateSeconds ?? 300;\n const now = options.now ?? (() => Date.now());\n\n /** The cache. ⚠️ Built once per `createAnswers`, never per request. */\n let folder = \"/answers\";\n let pages: AnswerPage[] = [];\n let indexPage: AnswerPage | undefined;\n let byPath = new Map<string, AnswerPage>();\n let fetchedAt = 0;\n let failedAt = 0;\n let inFlight: Promise<void> | undefined;\n\n /**\n * ⚠️ A deadline built from `AbortController` rather than\n * `AbortSignal.timeout`, which is not on every runtime this SDK supports —\n * the same construction `hosted.ts` uses.\n */\n function abortAfter(ms: number): { signal: AbortSignal | undefined; done: () => void } {\n const Controller = (globalThis as { AbortController?: typeof AbortController }).AbortController;\n if (!Controller) return { signal: undefined, done: () => {} };\n const controller = new Controller();\n const timer = setTimeout(() => controller.abort(), ms);\n (timer as unknown as { unref?: () => void }).unref?.();\n return { signal: controller.signal, done: () => clearTimeout(timer) };\n }\n\n async function refresh(): Promise<void> {\n if (!base) return;\n const deadline = abortAfter(FETCH_TIMEOUT_MS);\n try {\n const response = await fetchImpl(`${base}/v1/answers`, {\n headers: { authorization: `Bearer ${options.serverKey}` },\n // ⚠️ An unknown option, and that is fine: Next reads it, every other\n // runtime ignores it. Declaring it here rather than asking the customer\n // to remember it is the difference between a page that updates and one\n // frozen at its build.\n next: { revalidate },\n ...(deadline.signal ? { signal: deadline.signal } : {}),\n } as RequestInit);\n if (!response.ok) {\n // A 5xx is the endpoint failing and backs off. So does a 401: a revoked\n // key will not start working because we asked again in 200ms.\n failedAt = now();\n return;\n }\n const body = (await response.json()) as { folder?: unknown; answers?: unknown };\n if (!Array.isArray(body.answers)) {\n failedAt = now();\n return;\n }\n const next: AnswerPage[] = [];\n for (const row of body.answers as unknown[]) {\n const page = pageOf(row);\n if (page) next.push(page);\n }\n // ⚠️ Absent means NO index, and it replaces a held one: that is how a\n // withdrawal of the index reaches this process (0.13.0).\n const nextIndex = pageOf((body as { index?: unknown }).index);\n /*\n ⚠️ Built into a NEW map and swapped, never mutated in place: a resolve\n running during a refresh must see the old set or the new one, never a\n half-populated one. `hosted.ts` carries the same note.\n\n ⚠️ And an empty answer DOES replace a populated corpus here, unlike for\n twins — an empty corpus is how a withdrawal, or an operator switching\n answer pages off, reaches a client. Keeping the old set would leave a\n page serving that nobody publishes any more, which is the one failure\n this feature must not have.\n */\n pages = next;\n indexPage = nextIndex;\n byPath = new Map([...next, ...(nextIndex && options.serveIndex ? [nextIndex] : [])].map((page) => [page.path, page]));\n folder = typeof body.folder === \"string\" ? body.folder : folder;\n fetchedAt = now();\n failedAt = 0;\n } catch {\n // A throw is a network failure or our own abort; both mean the next\n // refresh waits. The previous set keeps serving until one succeeds.\n failedAt = now();\n } finally {\n deadline.done();\n }\n }\n\n /** Refresh only if stale, never more than one at a time, never throwing. */\n function refreshIfStale(): Promise<void> {\n if (inFlight) return inFlight;\n if (fetchedAt !== 0 && now() - fetchedAt < REFRESH_MS) return Promise.resolve();\n // ⚠️ After the staleness check, so a healthy warm process is never held\n // back, and before any I/O: that is the whole point of the backoff.\n if (failedAt !== 0 && now() - failedAt < FAILURE_BACKOFF_MS) return Promise.resolve();\n inFlight = refresh().finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n }\n\n /** One wire row, or undefined when it is not one. */\n function pageOf(value: unknown): AnswerPage | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n const row = value as Record<string, unknown>;\n if (typeof row[\"path\"] !== \"string\" || typeof row[\"html\"] !== \"string\") return undefined;\n return {\n // ⚠️ Normalised on the way IN as well as out: a client asking for\n // `/answers/x/` and a corpus holding `/answers/x` must be one page.\n // `Corpus.php` learned this the expensive way and `hosted.ts` carries\n // the same note.\n path: normalisePath(row[\"path\"]),\n slug: typeof row[\"slug\"] === \"string\" ? row[\"slug\"] : \"\",\n title: typeof row[\"title\"] === \"string\" ? row[\"title\"] : \"\",\n markdown: typeof row[\"markdown\"] === \"string\" ? row[\"markdown\"] : \"\",\n html: row[\"html\"],\n contentHash: typeof row[\"contentHash\"] === \"string\" ? row[\"contentHash\"] : \"\",\n updatedAt: typeof row[\"updatedAt\"] === \"string\" ? row[\"updatedAt\"] : \"\",\n };\n }\n\n const list = async (): Promise<AnswerPage[]> => {\n await refreshIfStale();\n return pages;\n };\n\n return {\n list,\n async get(slug) {\n return (await list()).find((page) => page.slug === slug);\n },\n async index() {\n await refreshIfStale();\n return indexPage;\n },\n async folder() {\n await refreshIfStale();\n return folder;\n },\n async sitemapEntries(origin) {\n const clean = origin.replace(/\\/+$/, \"\");\n const all = await list();\n return (indexPage && options.serveIndex ? [indexPage, ...all] : all).map((page) => ({\n url: `${clean}${page.path}`,\n lastModified: new Date(page.updatedAt || Date.now()),\n }));\n },\n twinResolver() {\n // ⚠️ Synchronous, and it starts nothing. See the interface's docblock:\n // this is called on every passing GET, by `lookup`.\n return (path: string) => {\n const page = byPath.get(normalisePath(path));\n return page ? { body: page.markdown } : undefined;\n };\n },\n warm: refreshIfStale,\n };\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/serve/tag-asset.generated.ts
|
|
2
|
+
var 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';
|
|
3
|
+
|
|
4
|
+
// src/serve/tag.ts
|
|
5
|
+
var TAG_CONFIG_PLACEHOLDER = "__AGENTHONEY_TAG_CONFIG__";
|
|
6
|
+
var TAG_PATH = "/_agenthoney/t.js";
|
|
7
|
+
function tagEndpointFor(ingestUrl) {
|
|
8
|
+
if (!ingestUrl) return "";
|
|
9
|
+
try {
|
|
10
|
+
return new URL(ingestUrl).origin;
|
|
11
|
+
} catch {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function renderTag(options) {
|
|
16
|
+
const occurrences = TAG_SOURCE.split(TAG_CONFIG_PLACEHOLDER).length - 1;
|
|
17
|
+
if (occurrences !== 1) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`agenthoney: tag artifact has ${occurrences} config placeholders, expected exactly 1`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const config = JSON.stringify({
|
|
23
|
+
key: options.publicKey,
|
|
24
|
+
endpoint: options.endpoint,
|
|
25
|
+
// ⚠️ Emitted only when true. The tag treats a MISSING flag as "no", so an
|
|
26
|
+
// older adapter or a cached copy can never start harvesting by omission.
|
|
27
|
+
...options.harvest === true ? { harvest: true } : {},
|
|
28
|
+
// ⚠️ Emitted only when harvesting is on and the list is non-empty. A
|
|
29
|
+
// `deny: []` in every served file would be bytes on every page of every
|
|
30
|
+
// customer's site to say nothing.
|
|
31
|
+
...options.harvest === true && options.harvestDeny?.length ? { deny: [...options.harvestDeny] } : {}
|
|
32
|
+
});
|
|
33
|
+
const literal = JSON.stringify(config);
|
|
34
|
+
const body = TAG_SOURCE.replace(`"${TAG_CONFIG_PLACEHOLDER}"`, () => literal);
|
|
35
|
+
return {
|
|
36
|
+
body,
|
|
37
|
+
headers: {
|
|
38
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
39
|
+
"cache-control": options.cacheControl ?? "public, max-age=300",
|
|
40
|
+
// The file is JavaScript and must never be sniffed as anything else.
|
|
41
|
+
"x-content-type-options": "nosniff"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { TAG_PATH, renderTag, tagEndpointFor };
|
|
47
|
+
//# sourceMappingURL=chunk-CD4WLJX7.js.map
|
|
48
|
+
//# sourceMappingURL=chunk-CD4WLJX7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/serve/tag-asset.generated.ts","../src/serve/tag.ts"],"names":[],"mappings":";AAYO,IAAM,UAAA,GAAa,m5HAAA;;;ACsB1B,IAAM,sBAAA,GAAyB,2BAAA;AAGxB,IAAM,QAAA,GAAW;AA+EjB,SAAS,eAAe,SAAA,EAA2B;AACxD,EAAA,IAAI,CAAC,WAAW,OAAO,EAAA;AACvB,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,SAAS,CAAA,CAAE,MAAA;AAAA,EAC5B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAA;AAAA,EACT;AACF;AAkBO,SAAS,UAAU,OAAA,EAAyD;AACjF,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,KAAA,CAAM,sBAAsB,EAAE,MAAA,GAAS,CAAA;AACtE,EAAA,IAAI,gBAAgB,CAAA,EAAG;AACrB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,gCAAgC,WAAW,CAAA,wCAAA;AAAA,KAC7C;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,KAAK,SAAA,CAAU;AAAA,IAC5B,KAAK,OAAA,CAAQ,SAAA;AAAA,IACb,UAAU,OAAA,CAAQ,QAAA;AAAA;AAAA;AAAA,IAGlB,GAAI,QAAQ,OAAA,KAAY,IAAA,GAAO,EAAE,OAAA,EAAS,IAAA,KAAS,EAAC;AAAA;AAAA;AAAA;AAAA,IAIpD,GAAI,OAAA,CAAQ,OAAA,KAAY,IAAA,IAAQ,QAAQ,WAAA,EAAa,MAAA,GACjD,EAAE,IAAA,EAAM,CAAC,GAAG,OAAA,CAAQ,WAAW,CAAA,KAC/B;AAAC,GACN,CAAA;AAUD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AACrC,EAAA,MAAM,OAAO,UAAA,CAAW,OAAA,CAAQ,IAAI,sBAAsB,CAAA,CAAA,CAAA,EAAK,MAAM,OAAO,CAAA;AAE5E,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,gCAAA;AAAA,MAChB,eAAA,EAAiB,QAAQ,YAAA,IAAgB,qBAAA;AAAA;AAAA,MAEzC,wBAAA,EAA0B;AAAA;AAC5B,GACF;AACF","file":"chunk-CD4WLJX7.js","sourcesContent":["/**\n * ⚠️ GENERATED by `scripts/build-tag-asset.ts` from `packages/tag/dist/t.js`.\n * Do not edit by hand -- `tag-asset.test.ts` regenerates this and fails if the\n * two disagree.\n *\n * The SDK holds the page tag as a STRING, never as an import:\n * `@agenthoney/tag` is private and unpublished, so a surviving import would\n * make the published SDK uninstallable and `check-sdk-artifact.mjs` refuses\n * one anyway.\n */\n\n/** The built tag, with its configuration placeholder still in place. */\nexport 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\";\n\n/** Of the UNSUBSTITUTED source, so it identifies the build and not the site. */\nexport const TAG_SHA256 = \"0421439eca918bb309b6fdd572e7db685466fb2ae25a7a93337c52811502e7e6\";\n","import { TAG_SHA256, TAG_SOURCE } from \"./tag-asset.generated.js\";\n\n/**\n * Serving the page tag from the customer's own origin.\n *\n * ── ⚠️ Served, never injected ────────────────────────────────────────────────\n * The obvious design is to insert a `<script>` into the HTML on the way past.\n * It is prohibited, for three independent reasons:\n *\n * 1. It is **body rewriting**. `res` is a single-read stream; monkey-patching\n * `write`/`end` to splice a tag in adds latency, adds memory, and can\n * corrupt what the customer sends. This middleware has no body path at all\n * today and must not grow one.\n * 2. An **inline script breaks a nonce-based CSP**. That is not hypothetical\n * -- **R6** is written down about Cloudflare's Scrape Shield doing exactly\n * this. An external SAME-ORIGIN script satisfies `script-src 'self'` with\n * no nonce at all.\n * 3. Injection would make the tag impossible to remove without removing the\n * middleware, which is the wrong kill switch for the half that runs in\n * somebody else's browser.\n *\n * So: the adapter serves the file, and the customer writes one `<script async>`\n * themselves. Serving it ourselves is what lets the configuration be baked into\n * the bytes, so their HTML has nothing in it that can be pasted wrong -- which\n * matters more here than it would elsewhere, because this product's real\n * install path is an AI coding agent reading `/install.md`.\n *\n * ── ⚠️ First-party, so no third-party origin is involved ─────────────────────\n * Same-origin means no extra DNS, no extra TLS, no `*.agenthoney.ai` in the\n * customer's `script-src`, and nothing for a tracker blocker to match on the\n * script itself.\n */\n\n/** ⚠️ Must match `RAW` in `packages/tag/src/config.ts`. */\nconst TAG_CONFIG_PLACEHOLDER = \"__AGENTHONEY_TAG_CONFIG__\";\n\n/** Where the adapters serve it. Underscored so it cannot collide with a route. */\nexport const TAG_PATH = \"/_agenthoney/t.js\";\n\nexport interface TagOptions {\n /**\n * The site's PUBLIC key, `ep_live_public_…`.\n *\n * ⚠️ Not a secret, and cannot be made one -- it is served inside this file to\n * anyone who asks. What makes that safe is the class: `authenticate()` in\n * ingest requires `server` on `/v1/events`, so a public key cannot write\n * events. Never put `AGENTHONEY_SERVER_KEY` here; the SDK's own\n * `check-browser-safety.mjs` exists to stop that value reaching a browser.\n */\n publicKey: string;\n /**\n * Where the tag will post. An ORIGIN; the tag appends its own paths.\n * Defaults to the origin of the SDK's configured ingest URL.\n */\n endpoint?: string;\n /**\n * Let the tag read this site's rendered pages and offer them as twins.\n *\n * ⚠️ **Site-level, and off by default.** The plan for this phase said the\n * middleware would refuse harvesting per request -- when the request carried\n * a session cookie or an `Authorization` header -- and that is **not\n * implementable**: this file is one shared artifact, cached, served to every\n * visitor. A flag baked into it cannot describe the request that will later\n * load a page. Discovering that late is exactly why it is written here.\n *\n * What actually protects a signed-in visitor's page is the consensus gate:\n * nothing is published until several DISTINCT anonymous visitors\n * independently produced the same content, so personalised and authenticated\n * pages never converge. The tag additionally skips any page the site itself\n * marks `noindex`, which is the owner saying \"not for machines\" in the one\n * place they already say it.\n */\n harvest?: boolean;\n /**\n * Path prefixes the tag must never read on this site.\n *\n * ⚠️ **A deny list, not an allow list, and the difference is whether anybody\n * will fill it in.** An allow list asks a client-rendered site's operator\n * for a complete route inventory -- which is the hand-authoring burden the\n * tag exists to remove -- while this asks only for the prefixes a signed-in\n * user lands on, which an operator knows without looking anything up.\n *\n * ⚠️ Enforced again at ingest from the SITE's own record, because this file\n * is cached for five minutes and by whatever CDN fronts it. What is baked in\n * here saves the browser the work; what is on the server is the control.\n */\n harvestDeny?: readonly string[];\n /**\n * ⚠️ Short on purpose. A revoked public key must stop being handed out, and a\n * long cache on the file that CONTAINS it would make revocation meaningless.\n * Five minutes is long enough to matter for a busy site and short enough that\n * a rotation takes effect while somebody is still watching.\n */\n cacheControl?: string;\n}\n\n/**\n * Where the tag will post, derived from the SDK's own ingest URL.\n *\n * ⚠️ Derived rather than defaulted to a constant, so a customer pointed at a\n * staging ingest does not have their browser traffic silently going to\n * production.\n *\n * ⚠️ **An ORIGIN, and it used to be a route that does not exist.** This\n * returned `<origin>/v1/signals` until Phase 35, and the install guide told\n * every customer to construct that path by hand. Ingest has never served it.\n * Nothing broke, because the tag takes `new URL(endpoint).origin` and appends\n * its own paths -- so the wrong half was silently discarded on every page load,\n * and the guide taught a URL that 404s if anyone ever fetched it. That is the\n * same silent-misconfiguration shape the bare-origin `ingestUrl` bug had in\n * Phase 32, and it is corrected the same way: emit the thing that is true.\n *\n * Returns `\"\"` when there is nothing to derive from, and the caller treats that\n * as \"do not serve the tag\" -- a tag with no endpoint is a file that runs in\n * somebody's browser for no reason.\n */\nexport function tagEndpointFor(ingestUrl: string): string {\n if (!ingestUrl) return \"\";\n try {\n return new URL(ingestUrl).origin;\n } catch {\n return \"\";\n }\n}\n\nexport interface RenderedTag {\n body: string;\n headers: Record<string, string>;\n}\n\n/**\n * ⚠️ Throws on a placeholder count that is not exactly one, rather than serving\n * a best-effort file.\n *\n * Zero would mean a build that eliminated the placeholder, and every visitor\n * would run an inert tag that reports nothing -- a silent, total failure that\n * looks exactly like \"no traffic\". More than one would mean substituting\n * something that was not meant to be substituted. Both are bugs in OUR build,\n * caught here at wiring time; the caller wraps this so a throw can never reach\n * the customer's response.\n */\nexport function renderTag(options: TagOptions & { endpoint: string }): RenderedTag {\n const occurrences = TAG_SOURCE.split(TAG_CONFIG_PLACEHOLDER).length - 1;\n if (occurrences !== 1) {\n throw new Error(\n `agenthoney: tag artifact has ${occurrences} config placeholders, expected exactly 1`,\n );\n }\n\n const config = JSON.stringify({\n key: options.publicKey,\n endpoint: options.endpoint,\n // ⚠️ Emitted only when true. The tag treats a MISSING flag as \"no\", so an\n // older adapter or a cached copy can never start harvesting by omission.\n ...(options.harvest === true ? { harvest: true } : {}),\n // ⚠️ Emitted only when harvesting is on and the list is non-empty. A\n // `deny: []` in every served file would be bytes on every page of every\n // customer's site to say nothing.\n ...(options.harvest === true && options.harvestDeny?.length\n ? { deny: [...options.harvestDeny] }\n : {}),\n });\n // ⚠️ The placeholder is replaced INCLUDING its surrounding quotes, with a\n // fully-escaped JavaScript string literal. `JSON.stringify` of the JSON text\n // is what does the escaping; hand-rolling it would be one backslash away from\n // a syntax error on every page of a customer's site.\n //\n // ⚠️ And the replacement is a FUNCTION, not a string. `String.replace` reads\n // `$&`, `$\\'` and friends out of a replacement string, so an endpoint or key\n // containing a dollar sign would silently splice part of the file into\n // itself. A function replacement is substituted verbatim.\n const literal = JSON.stringify(config);\n const body = TAG_SOURCE.replace(`\"${TAG_CONFIG_PLACEHOLDER}\"`, () => literal);\n\n return {\n body,\n headers: {\n \"content-type\": \"text/javascript; charset=utf-8\",\n \"cache-control\": options.cacheControl ?? \"public, max-age=300\",\n // The file is JavaScript and must never be sniffed as anything else.\n \"x-content-type-options\": \"nosniff\",\n },\n };\n}\n\nexport { TAG_CONFIG_PLACEHOLDER, TAG_SHA256 };\n"]}
|