@entreprenoid/analytics 0.1.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 +82 -0
- package/dist/chunk-5NZNPRMN.js +355 -0
- package/dist/chunk-5NZNPRMN.js.map +1 -0
- package/dist/chunk-EVNUKN5A.js +114 -0
- package/dist/chunk-EVNUKN5A.js.map +1 -0
- package/dist/chunk-GKFYGAKF.js +104 -0
- package/dist/chunk-GKFYGAKF.js.map +1 -0
- package/dist/chunk-IHZCOC2U.js +690 -0
- package/dist/chunk-IHZCOC2U.js.map +1 -0
- package/dist/core/breaker.d.ts +33 -0
- package/dist/core/collector.d.ts +40 -0
- package/dist/core/config.d.ts +101 -0
- package/dist/core/encode.d.ts +32 -0
- package/dist/core/queue.d.ts +39 -0
- package/dist/core/safe.d.ts +17 -0
- package/dist/core/transport.d.ts +45 -0
- package/dist/express.cjs +1042 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.ts +51 -0
- package/dist/express.js +4 -0
- package/dist/express.js.map +1 -0
- package/dist/index.cjs +1289 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +801 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.ts +90 -0
- package/dist/next.js +4 -0
- package/dist/next.js.map +1 -0
- package/dist/observe/redact.d.ts +58 -0
- package/dist/observe/request.d.ts +57 -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 +45 -0
- package/dist/serve/twin.d.ts +102 -0
- package/dist/web.cjs +790 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.ts +43 -0
- package/dist/web.js +4 -0
- package/dist/web.js.map +1 -0
- package/package.json +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/breaker.ts
|
|
4
|
+
function createBreaker(options = {}) {
|
|
5
|
+
const failureThreshold = options.failureThreshold ?? 5;
|
|
6
|
+
const openMs = options.openMs ?? 3e4;
|
|
7
|
+
const now = options.now ?? (() => Date.now());
|
|
8
|
+
let consecutiveFailures = 0;
|
|
9
|
+
let openedAt = 0;
|
|
10
|
+
let state = "closed";
|
|
11
|
+
return {
|
|
12
|
+
get state() {
|
|
13
|
+
return state;
|
|
14
|
+
},
|
|
15
|
+
allow() {
|
|
16
|
+
if (state === "closed") return true;
|
|
17
|
+
if (state === "half-open") return true;
|
|
18
|
+
if (now() - openedAt >= openMs) {
|
|
19
|
+
state = "half-open";
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
},
|
|
24
|
+
success() {
|
|
25
|
+
consecutiveFailures = 0;
|
|
26
|
+
state = "closed";
|
|
27
|
+
},
|
|
28
|
+
failure() {
|
|
29
|
+
consecutiveFailures += 1;
|
|
30
|
+
if (state === "half-open" || consecutiveFailures >= failureThreshold) {
|
|
31
|
+
state = "open";
|
|
32
|
+
openedAt = now();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/core/config.ts
|
|
39
|
+
var UNSET_SITE_ID = "unset";
|
|
40
|
+
var DEFAULTS = {
|
|
41
|
+
batchSize: 20,
|
|
42
|
+
maxBatchSize: 100,
|
|
43
|
+
flushIntervalMs: 2e3,
|
|
44
|
+
maxQueueEvents: 1e3,
|
|
45
|
+
maxBodyBytes: 512 * 1024,
|
|
46
|
+
requestTimeoutMs: 2e3
|
|
47
|
+
};
|
|
48
|
+
var PUBLIC_ENV_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "PUBLIC_", "REACT_APP_", "NUXT_PUBLIC_"];
|
|
49
|
+
var EntreprenoidConfigError = class extends Error {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "EntreprenoidConfigError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function clamp(value, min, max) {
|
|
56
|
+
return Math.min(max, Math.max(min, value));
|
|
57
|
+
}
|
|
58
|
+
function boolFromEnv(raw) {
|
|
59
|
+
if (raw === void 0) return void 0;
|
|
60
|
+
const v = raw.trim().toLowerCase();
|
|
61
|
+
if (v === "1" || v === "true" || v === "yes") return true;
|
|
62
|
+
if (v === "0" || v === "false" || v === "no") return false;
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
function resolveConfig(options = {}, env = typeof process === "undefined" ? {} : process.env) {
|
|
66
|
+
for (const prefix of PUBLIC_ENV_PREFIXES) {
|
|
67
|
+
const name = `${prefix}ENTREPRENOID_SERVER_KEY`;
|
|
68
|
+
if (env[name]) {
|
|
69
|
+
throw new EntreprenoidConfigError(
|
|
70
|
+
`${name} is set. The "${prefix}" prefix inlines a value into client-side JavaScript, so this key is already public. Revoke it, then set ENTREPRENOID_SERVER_KEY instead (no prefix) so it stays on the server.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const ingestUrl = options.ingestUrl ?? env["ENTREPRENOID_INGEST_URL"] ?? "";
|
|
75
|
+
const serverKey = options.serverKey ?? env["ENTREPRENOID_SERVER_KEY"] ?? "";
|
|
76
|
+
const siteId = options.siteId ?? env["ENTREPRENOID_SITE_ID"] ?? UNSET_SITE_ID;
|
|
77
|
+
const enabled = options.enabled ?? boolFromEnv(env["ENTREPRENOID_ENABLED"]) ?? true;
|
|
78
|
+
const debug = options.debug ?? boolFromEnv(env["ENTREPRENOID_DEBUG"]) ?? false;
|
|
79
|
+
let disabled = null;
|
|
80
|
+
if (typeof window !== "undefined") {
|
|
81
|
+
disabled = "browser-environment";
|
|
82
|
+
} else if (!enabled) {
|
|
83
|
+
disabled = "explicitly-disabled";
|
|
84
|
+
} else if (!ingestUrl) {
|
|
85
|
+
disabled = "missing-url";
|
|
86
|
+
} else if (!serverKey) {
|
|
87
|
+
disabled = "missing-key";
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
ingestUrl,
|
|
91
|
+
serverKey,
|
|
92
|
+
siteId,
|
|
93
|
+
debug,
|
|
94
|
+
batchSize: clamp(options.batchSize ?? DEFAULTS.batchSize, 1, DEFAULTS.maxBatchSize),
|
|
95
|
+
flushIntervalMs: clamp(options.flushIntervalMs ?? DEFAULTS.flushIntervalMs, 100, 6e4),
|
|
96
|
+
maxQueueEvents: clamp(options.maxQueueEvents ?? DEFAULTS.maxQueueEvents, 1, 1e5),
|
|
97
|
+
maxBodyBytes: clamp(options.maxBodyBytes ?? DEFAULTS.maxBodyBytes, 1024, DEFAULTS.maxBodyBytes),
|
|
98
|
+
requestTimeoutMs: clamp(options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs, 100, 3e4),
|
|
99
|
+
routeTemplate: options.routeTemplate,
|
|
100
|
+
redactPatterns: options.redactPatterns ?? [],
|
|
101
|
+
// ⚠️ Defaults to TRUE. An `?? true` that a refactor turns into `?? false`
|
|
102
|
+
// is the entire bug returning, so `config.test.ts` asserts the default.
|
|
103
|
+
redactHighEntropyPaths: options.redactHighEntropyPaths ?? true,
|
|
104
|
+
isInternal: options.isInternal,
|
|
105
|
+
disabled
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function redact(text) {
|
|
109
|
+
return text.replace(/\bep_(live|test)_server_[A-Za-z0-9_-]+/g, "ep_$1_server_[redacted]");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/core/encode.ts
|
|
113
|
+
var MAX_BODY_BYTES = 512 * 1024;
|
|
114
|
+
var OPEN = '{"events":[';
|
|
115
|
+
var CLOSE = "]}";
|
|
116
|
+
var byteLength = typeof TextEncoder === "function" ? (s) => new TextEncoder().encode(s).length : (
|
|
117
|
+
// Node 18+ always has TextEncoder; this is here so the module cannot
|
|
118
|
+
// throw at import time on an exotic runtime, which would take the host
|
|
119
|
+
// application down with it.
|
|
120
|
+
(s) => s.length
|
|
121
|
+
);
|
|
122
|
+
function encodeBatch(events, maxBytes = MAX_BODY_BYTES) {
|
|
123
|
+
if (events.length === 0) {
|
|
124
|
+
return { body: OPEN + CLOSE, taken: 0, oversized: false };
|
|
125
|
+
}
|
|
126
|
+
const overhead = byteLength(OPEN) + byteLength(CLOSE);
|
|
127
|
+
let used = overhead;
|
|
128
|
+
const parts = [];
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
const encoded = JSON.stringify(event);
|
|
131
|
+
const cost = byteLength(encoded) + (parts.length > 0 ? 1 : 0);
|
|
132
|
+
if (used + cost > maxBytes) break;
|
|
133
|
+
parts.push(encoded);
|
|
134
|
+
used += cost;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
body: OPEN + parts.join(",") + CLOSE,
|
|
138
|
+
taken: parts.length,
|
|
139
|
+
oversized: parts.length === 0
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/core/queue.ts
|
|
144
|
+
var BoundedQueue = class {
|
|
145
|
+
capacity;
|
|
146
|
+
#items;
|
|
147
|
+
#head = 0;
|
|
148
|
+
#size = 0;
|
|
149
|
+
#dropped = 0;
|
|
150
|
+
constructor(capacity) {
|
|
151
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
152
|
+
throw new TypeError(`capacity must be a positive integer, got ${capacity}`);
|
|
153
|
+
}
|
|
154
|
+
this.capacity = capacity;
|
|
155
|
+
this.#items = new Array(capacity);
|
|
156
|
+
}
|
|
157
|
+
get size() {
|
|
158
|
+
return this.#size;
|
|
159
|
+
}
|
|
160
|
+
/** How many events have been discarded because the buffer was full. */
|
|
161
|
+
get dropped() {
|
|
162
|
+
return this.#dropped;
|
|
163
|
+
}
|
|
164
|
+
push(item) {
|
|
165
|
+
if (this.#size === this.capacity) {
|
|
166
|
+
this.#items[this.#head] = item;
|
|
167
|
+
this.#head = (this.#head + 1) % this.capacity;
|
|
168
|
+
this.#dropped += 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.#items[(this.#head + this.#size) % this.capacity] = item;
|
|
172
|
+
this.#size += 1;
|
|
173
|
+
}
|
|
174
|
+
/** The first `max` items, without removing them. */
|
|
175
|
+
peek(max) {
|
|
176
|
+
const n = Math.min(max, this.#size);
|
|
177
|
+
const out = new Array(n);
|
|
178
|
+
for (let i = 0; i < n; i += 1) {
|
|
179
|
+
out[i] = this.#items[(this.#head + i) % this.capacity];
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/** Remove the first `n` items. Called only after they are safely sent. */
|
|
184
|
+
commit(n) {
|
|
185
|
+
const count = Math.min(n, this.#size);
|
|
186
|
+
for (let i = 0; i < count; i += 1) {
|
|
187
|
+
this.#items[(this.#head + i) % this.capacity] = void 0;
|
|
188
|
+
}
|
|
189
|
+
this.#head = (this.#head + count) % this.capacity;
|
|
190
|
+
this.#size -= count;
|
|
191
|
+
}
|
|
192
|
+
/** Reset the drop counter, once the count has been reported on the wire. */
|
|
193
|
+
clearDropped() {
|
|
194
|
+
this.#dropped = 0;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/core/safe.ts
|
|
199
|
+
function safe(fn, onError) {
|
|
200
|
+
try {
|
|
201
|
+
fn();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
try {
|
|
204
|
+
onError?.(error);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function safeAsync(fn, onError) {
|
|
210
|
+
try {
|
|
211
|
+
await fn();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
try {
|
|
214
|
+
onError?.(error);
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/core/transport.ts
|
|
221
|
+
function outcomeForStatus(status, retryAfterMs) {
|
|
222
|
+
if (status >= 200 && status < 300) return { outcome: "accepted", status };
|
|
223
|
+
if (status === 408 || status === 429 || status >= 500) {
|
|
224
|
+
return retryAfterMs === void 0 ? { outcome: "retryable", status } : { outcome: "retryable", status, retryAfterMs };
|
|
225
|
+
}
|
|
226
|
+
return { outcome: "rejected", status };
|
|
227
|
+
}
|
|
228
|
+
function parseRetryAfter(value) {
|
|
229
|
+
if (!value) return void 0;
|
|
230
|
+
const seconds = Number(value);
|
|
231
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
232
|
+
const date = Date.parse(value);
|
|
233
|
+
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
function fetchTransport(url) {
|
|
237
|
+
return {
|
|
238
|
+
async send(body, headers, signal) {
|
|
239
|
+
try {
|
|
240
|
+
const response = await fetch(url, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers,
|
|
243
|
+
body,
|
|
244
|
+
signal,
|
|
245
|
+
// Never follow a redirect: this request carries a bearer token, and
|
|
246
|
+
// a redirect is an instruction from the network to send that
|
|
247
|
+
// credential somewhere we did not choose.
|
|
248
|
+
redirect: "error",
|
|
249
|
+
keepalive: false
|
|
250
|
+
});
|
|
251
|
+
return outcomeForStatus(
|
|
252
|
+
response.status,
|
|
253
|
+
parseRetryAfter(response.headers.get("retry-after"))
|
|
254
|
+
);
|
|
255
|
+
} catch {
|
|
256
|
+
return { outcome: "retryable" };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/core/collector.ts
|
|
263
|
+
var MAX_RETRIES = 2;
|
|
264
|
+
function defaultSetTimer(fn, ms) {
|
|
265
|
+
const handle = setInterval(fn, ms);
|
|
266
|
+
handle.unref?.();
|
|
267
|
+
return { cancel: () => clearInterval(handle) };
|
|
268
|
+
}
|
|
269
|
+
function createCollector(config, deps = {}) {
|
|
270
|
+
const log = deps.log ?? ((message) => console.warn(redact(message)));
|
|
271
|
+
const debug = (message) => {
|
|
272
|
+
if (config.debug) log(`[entreprenoid] ${message}`);
|
|
273
|
+
};
|
|
274
|
+
if (config.disabled) {
|
|
275
|
+
debug(`collection disabled: ${config.disabled}`);
|
|
276
|
+
const noop = {
|
|
277
|
+
record: () => {
|
|
278
|
+
},
|
|
279
|
+
flush: async () => {
|
|
280
|
+
},
|
|
281
|
+
stats: { queued: 0, dropped: 0, sent: 0, failed: 0, breaker: "closed" },
|
|
282
|
+
close: () => {
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return noop;
|
|
286
|
+
}
|
|
287
|
+
const queue = new BoundedQueue(config.maxQueueEvents);
|
|
288
|
+
const transport = deps.transport ?? fetchTransport(config.ingestUrl);
|
|
289
|
+
const breaker = deps.breaker ?? createBreaker();
|
|
290
|
+
const random = deps.random ?? Math.random;
|
|
291
|
+
const setTimer = deps.setTimer ?? defaultSetTimer;
|
|
292
|
+
let sent = 0;
|
|
293
|
+
let failed = 0;
|
|
294
|
+
let inFlight = null;
|
|
295
|
+
const timer = setTimer(() => {
|
|
296
|
+
void flush();
|
|
297
|
+
}, config.flushIntervalMs);
|
|
298
|
+
function headers() {
|
|
299
|
+
return {
|
|
300
|
+
"content-type": "application/json",
|
|
301
|
+
authorization: `Bearer ${config.serverKey}`
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function sendOnce(body) {
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs);
|
|
307
|
+
timeout.unref?.();
|
|
308
|
+
try {
|
|
309
|
+
return await transport.send(body, headers(), controller.signal);
|
|
310
|
+
} finally {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function backoffMs(attempt, retryAfterMs) {
|
|
315
|
+
if (retryAfterMs !== void 0) return Math.min(retryAfterMs, 3e4);
|
|
316
|
+
return random() * Math.min(5e3, 200 * 3 ** attempt);
|
|
317
|
+
}
|
|
318
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
319
|
+
const t = setTimeout(resolve, ms);
|
|
320
|
+
t.unref?.();
|
|
321
|
+
});
|
|
322
|
+
async function flushOnce() {
|
|
323
|
+
if (queue.size === 0) return;
|
|
324
|
+
if (!breaker.allow()) {
|
|
325
|
+
debug("breaker open, skipping flush");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const candidates = queue.peek(config.batchSize);
|
|
329
|
+
const droppedSoFar = queue.dropped;
|
|
330
|
+
if (droppedSoFar > 0 && candidates[0]) {
|
|
331
|
+
candidates[0] = {
|
|
332
|
+
...candidates[0],
|
|
333
|
+
sdk: { ...candidates[0].sdk, dropped: droppedSoFar }
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const encoded = encodeBatch(candidates, config.maxBodyBytes);
|
|
337
|
+
if (encoded.oversized) {
|
|
338
|
+
queue.commit(1);
|
|
339
|
+
debug("dropped one event larger than the body limit");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
343
|
+
const result = await sendOnce(encoded.body);
|
|
344
|
+
if (result.outcome === "accepted") {
|
|
345
|
+
queue.commit(encoded.taken);
|
|
346
|
+
if (droppedSoFar > 0) queue.clearDropped();
|
|
347
|
+
sent += encoded.taken;
|
|
348
|
+
breaker.success();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (result.outcome === "rejected") {
|
|
352
|
+
queue.commit(encoded.taken);
|
|
353
|
+
failed += encoded.taken;
|
|
354
|
+
debug(`batch rejected with ${result.status}; dropped ${encoded.taken} event(s)`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (attempt === MAX_RETRIES) break;
|
|
358
|
+
await sleep(backoffMs(attempt, result.retryAfterMs));
|
|
359
|
+
}
|
|
360
|
+
failed += encoded.taken;
|
|
361
|
+
breaker.failure();
|
|
362
|
+
debug(`batch failed after ${MAX_RETRIES + 1} attempt(s); ${queue.size} event(s) still buffered`);
|
|
363
|
+
}
|
|
364
|
+
function flush() {
|
|
365
|
+
if (inFlight) return inFlight;
|
|
366
|
+
inFlight = safeAsync(flushOnce, (error) => debug(`flush failed: ${String(error)}`)).finally(
|
|
367
|
+
() => {
|
|
368
|
+
inFlight = null;
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
return inFlight;
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
record(event) {
|
|
375
|
+
try {
|
|
376
|
+
queue.push(event);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
flush,
|
|
381
|
+
get stats() {
|
|
382
|
+
return {
|
|
383
|
+
queued: queue.size,
|
|
384
|
+
dropped: queue.dropped,
|
|
385
|
+
sent,
|
|
386
|
+
failed,
|
|
387
|
+
breaker: breaker.state
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
close() {
|
|
391
|
+
timer.cancel();
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/observe/redact.ts
|
|
397
|
+
var MIN_LENGTH = 16;
|
|
398
|
+
var MIN_ENTROPY_BITS = 3;
|
|
399
|
+
var PLACEHOLDER = /^[:*[{<]/;
|
|
400
|
+
var HAS_EXTENSION = /\.[A-Za-z0-9]{1,8}$/;
|
|
401
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
402
|
+
var CUID = /^c[a-z0-9]{20,31}$/;
|
|
403
|
+
var JWT = /^ey[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
404
|
+
var LONG_HEX = /^[0-9a-f]{16,}$/i;
|
|
405
|
+
var ALL_LOWER_ALPHA = /^[a-z]+$/;
|
|
406
|
+
function entropyBits(value) {
|
|
407
|
+
const counts = /* @__PURE__ */ new Map();
|
|
408
|
+
for (const ch of value) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
409
|
+
let bits = 0;
|
|
410
|
+
for (const n of counts.values()) {
|
|
411
|
+
const p = n / value.length;
|
|
412
|
+
bits -= p * Math.log2(p);
|
|
413
|
+
}
|
|
414
|
+
return bits;
|
|
415
|
+
}
|
|
416
|
+
function dense(value) {
|
|
417
|
+
if (value.length < MIN_LENGTH) return false;
|
|
418
|
+
if (ALL_LOWER_ALPHA.test(value)) return false;
|
|
419
|
+
const classes = Number(/[a-z]/.test(value)) + Number(/[A-Z]/.test(value)) + Number(/[0-9]/.test(value)) + Number(/[^A-Za-z0-9]/.test(value));
|
|
420
|
+
if (classes < 3) return false;
|
|
421
|
+
return entropyBits(value) >= MIN_ENTROPY_BITS;
|
|
422
|
+
}
|
|
423
|
+
function slugLike(segment) {
|
|
424
|
+
const parts = segment.split(/[-_]/).filter(Boolean);
|
|
425
|
+
if (parts.length < 2) return false;
|
|
426
|
+
const tidy = parts.every((p) => /^[a-z]+$/.test(p) || /^[0-9]+$/.test(p));
|
|
427
|
+
const hasWord = parts.some((p) => /^[a-z]{2,}$/.test(p));
|
|
428
|
+
return tidy && hasWord;
|
|
429
|
+
}
|
|
430
|
+
function looksLikeSecret(segment) {
|
|
431
|
+
if (segment.length < MIN_LENGTH) return false;
|
|
432
|
+
if (PLACEHOLDER.test(segment)) return false;
|
|
433
|
+
if (HAS_EXTENSION.test(segment)) return false;
|
|
434
|
+
if (UUID.test(segment) || CUID.test(segment) || JWT.test(segment) || LONG_HEX.test(segment)) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
for (const part of segment.split(/[-_.]/)) {
|
|
438
|
+
if (dense(part)) return true;
|
|
439
|
+
}
|
|
440
|
+
if (slugLike(segment)) return false;
|
|
441
|
+
return dense(segment);
|
|
442
|
+
}
|
|
443
|
+
var REDACTED = "[redacted]";
|
|
444
|
+
function redactPath(path, patterns, useDefault) {
|
|
445
|
+
if (patterns.length === 0 && !useDefault) return { path, redacted: false };
|
|
446
|
+
let redacted = false;
|
|
447
|
+
const out = path.split("/").map((segment) => {
|
|
448
|
+
if (!segment) return segment;
|
|
449
|
+
const matchesCustom = patterns.some((re) => {
|
|
450
|
+
re.lastIndex = 0;
|
|
451
|
+
return re.test(segment);
|
|
452
|
+
});
|
|
453
|
+
if (matchesCustom || useDefault && looksLikeSecret(segment)) {
|
|
454
|
+
redacted = true;
|
|
455
|
+
return REDACTED;
|
|
456
|
+
}
|
|
457
|
+
return segment;
|
|
458
|
+
}).join("/");
|
|
459
|
+
return { path: out, redacted };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/observe/request.ts
|
|
463
|
+
var CAMPAIGN_PARAMS = ["utm_source", "utm_medium", "utm_campaign", "utm_content"];
|
|
464
|
+
var CLICK_ID_PARAMS = ["gclid", "fbclid", "msclkid"];
|
|
465
|
+
function normalisePath(rawPath, redactPatterns = []) {
|
|
466
|
+
const path = cleanPath(rawPath);
|
|
467
|
+
return redactPatterns.length > 0 ? redactPath(path, redactPatterns, false).path : path;
|
|
468
|
+
}
|
|
469
|
+
function cleanPath(rawPath) {
|
|
470
|
+
let path = rawPath;
|
|
471
|
+
const hash = path.indexOf("#");
|
|
472
|
+
if (hash !== -1) path = path.slice(0, hash);
|
|
473
|
+
const query = path.indexOf("?");
|
|
474
|
+
if (query !== -1) path = path.slice(0, query);
|
|
475
|
+
if (path === "") path = "/";
|
|
476
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
477
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
478
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
479
|
+
return path.length > 1024 ? path.slice(0, 1024) : path;
|
|
480
|
+
}
|
|
481
|
+
function referrerOrigin(referer) {
|
|
482
|
+
if (!referer) return void 0;
|
|
483
|
+
try {
|
|
484
|
+
const url = new URL(referer);
|
|
485
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
|
|
486
|
+
return url.origin.slice(0, 255);
|
|
487
|
+
} catch {
|
|
488
|
+
return void 0;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function campaignFrom(params) {
|
|
492
|
+
let found = false;
|
|
493
|
+
const campaign = {};
|
|
494
|
+
for (const name of CAMPAIGN_PARAMS) {
|
|
495
|
+
const value = params.get(name);
|
|
496
|
+
if (value) {
|
|
497
|
+
campaign[name.slice(4)] = value.slice(0, 120);
|
|
498
|
+
found = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return found ? campaign : void 0;
|
|
502
|
+
}
|
|
503
|
+
function clickIdFrom(params) {
|
|
504
|
+
for (const name of CLICK_ID_PARAMS) {
|
|
505
|
+
if (params.has(name)) return name;
|
|
506
|
+
}
|
|
507
|
+
return void 0;
|
|
508
|
+
}
|
|
509
|
+
function observeRequest(facts, config) {
|
|
510
|
+
const cleaned = cleanPath(facts.url);
|
|
511
|
+
let params = new URLSearchParams();
|
|
512
|
+
try {
|
|
513
|
+
params = new URL(facts.url, "http://x").searchParams;
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
const templated = config.routeTemplate?.(cleaned);
|
|
517
|
+
const { path, redacted } = redactPath(
|
|
518
|
+
cleaned,
|
|
519
|
+
config.redactPatterns,
|
|
520
|
+
config.redactHighEntropyPaths
|
|
521
|
+
);
|
|
522
|
+
const observed = {
|
|
523
|
+
method: facts.method.slice(0, 10).toUpperCase(),
|
|
524
|
+
path
|
|
525
|
+
};
|
|
526
|
+
if (templated) {
|
|
527
|
+
const safeRoute = redactPath(templated, config.redactPatterns, config.redactHighEntropyPaths);
|
|
528
|
+
observed.route = safeRoute.path.slice(0, 512);
|
|
529
|
+
if (safeRoute.redacted) observed.pathRedacted = true;
|
|
530
|
+
}
|
|
531
|
+
if (redacted) observed.pathRedacted = true;
|
|
532
|
+
if (facts.host) observed.host = facts.host.slice(0, 253);
|
|
533
|
+
if (facts.protocol) observed.protocol = facts.protocol;
|
|
534
|
+
if (facts.userAgent) observed.userAgent = facts.userAgent.slice(0, 512);
|
|
535
|
+
const origin = referrerOrigin(facts.referer);
|
|
536
|
+
if (origin) observed.referrerOrigin = origin;
|
|
537
|
+
const campaign = campaignFrom(params);
|
|
538
|
+
if (campaign) observed.campaign = campaign;
|
|
539
|
+
const clickIdType = clickIdFrom(params);
|
|
540
|
+
if (clickIdType) observed.clickIdType = clickIdType;
|
|
541
|
+
if (config.isInternal?.(observed)) observed.internal = true;
|
|
542
|
+
return observed;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/observe/response.ts
|
|
546
|
+
function observeResponse(facts) {
|
|
547
|
+
const response = { observation: facts.observation };
|
|
548
|
+
if (facts.observation === "unknown") {
|
|
549
|
+
if (typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs)) {
|
|
550
|
+
response.latencyMs = Math.max(0, Math.round(facts.latencyMs));
|
|
551
|
+
}
|
|
552
|
+
return response;
|
|
553
|
+
}
|
|
554
|
+
if (typeof facts.status === "number" && facts.status >= 100 && facts.status <= 599) {
|
|
555
|
+
response.status = facts.status;
|
|
556
|
+
}
|
|
557
|
+
if (facts.contentType) {
|
|
558
|
+
response.contentType = String(facts.contentType).slice(0, 255);
|
|
559
|
+
}
|
|
560
|
+
const length = typeof facts.contentLength === "string" ? Number(facts.contentLength) : facts.contentLength;
|
|
561
|
+
if (typeof length === "number" && Number.isFinite(length) && length >= 0) {
|
|
562
|
+
response.contentLength = Math.round(length);
|
|
563
|
+
}
|
|
564
|
+
if (typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs)) {
|
|
565
|
+
response.latencyMs = Math.max(0, Math.round(facts.latencyMs));
|
|
566
|
+
}
|
|
567
|
+
return response;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/runtime.ts
|
|
571
|
+
var SDK_NAME = "@entreprenoid/analytics";
|
|
572
|
+
var SDK_VERSION = "0.1.0";
|
|
573
|
+
function runtimeName() {
|
|
574
|
+
try {
|
|
575
|
+
const g = globalThis;
|
|
576
|
+
const deno = g["Deno"];
|
|
577
|
+
if (deno?.version?.deno) return `deno-${deno.version.deno}`;
|
|
578
|
+
const bun = g["Bun"];
|
|
579
|
+
if (bun?.version) return `bun-${bun.version}`;
|
|
580
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
581
|
+
return `node-${process.versions.node}`;
|
|
582
|
+
}
|
|
583
|
+
const nav = g["navigator"];
|
|
584
|
+
if (nav?.userAgent?.includes("Cloudflare-Workers")) return "workerd";
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
return "unknown";
|
|
588
|
+
}
|
|
589
|
+
function newEventId() {
|
|
590
|
+
try {
|
|
591
|
+
const c = globalThis.crypto;
|
|
592
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/serve/accept.ts
|
|
599
|
+
function parseAccept(header2) {
|
|
600
|
+
if (!header2) return [];
|
|
601
|
+
const ranges = [];
|
|
602
|
+
for (const part of header2.split(",")) {
|
|
603
|
+
const segments = part.trim().split(";");
|
|
604
|
+
const type = segments[0]?.trim().toLowerCase();
|
|
605
|
+
if (!type) continue;
|
|
606
|
+
let q = 1;
|
|
607
|
+
for (const segment of segments.slice(1)) {
|
|
608
|
+
const [key, value] = segment.split("=").map((s) => s.trim().toLowerCase());
|
|
609
|
+
if (key === "q") {
|
|
610
|
+
const parsed = Number(value);
|
|
611
|
+
q = Number.isFinite(parsed) ? Math.min(1, Math.max(0, parsed)) : 1;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
ranges.push({ type, q });
|
|
615
|
+
}
|
|
616
|
+
return ranges;
|
|
617
|
+
}
|
|
618
|
+
function exactQ(ranges, type) {
|
|
619
|
+
let best = 0;
|
|
620
|
+
for (const range of ranges) {
|
|
621
|
+
if (range.type === type) best = Math.max(best, range.q);
|
|
622
|
+
}
|
|
623
|
+
return best;
|
|
624
|
+
}
|
|
625
|
+
function effectiveQ(ranges, type) {
|
|
626
|
+
const prefix = `${type.split("/")[0]}/*`;
|
|
627
|
+
let best = 0;
|
|
628
|
+
for (const range of ranges) {
|
|
629
|
+
if (range.type === type || range.type === prefix || range.type === "*/*") {
|
|
630
|
+
best = Math.max(best, range.q);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return best;
|
|
634
|
+
}
|
|
635
|
+
var MARKDOWN_TYPES = ["text/markdown", "text/x-markdown"];
|
|
636
|
+
function prefersMarkdown(header2) {
|
|
637
|
+
const ranges = parseAccept(header2);
|
|
638
|
+
if (ranges.length === 0) return false;
|
|
639
|
+
let markdown = 0;
|
|
640
|
+
for (const type of MARKDOWN_TYPES) {
|
|
641
|
+
markdown = Math.max(markdown, exactQ(ranges, type));
|
|
642
|
+
}
|
|
643
|
+
if (markdown === 0) return false;
|
|
644
|
+
const html = effectiveQ(ranges, "text/html");
|
|
645
|
+
return markdown >= html;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/serve/twin.ts
|
|
649
|
+
var DISCOVERY_PATHS = ["/llms.txt", "/llms-full.txt", "/install.md"];
|
|
650
|
+
var DEFAULT_TWIN_CONTENT_TYPE = "text/markdown; charset=utf-8";
|
|
651
|
+
var DEFAULT_TWIN_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
|
|
652
|
+
function decideTwin(input) {
|
|
653
|
+
const method = input.method.toUpperCase();
|
|
654
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
655
|
+
return { action: "pass", reason: "not_get" };
|
|
656
|
+
}
|
|
657
|
+
if (input.path.endsWith(".md")) {
|
|
658
|
+
return { action: "serve", lookupPath: stripMdSuffix(input.path), reason: "md_path" };
|
|
659
|
+
}
|
|
660
|
+
if (prefersMarkdown(input.accept)) {
|
|
661
|
+
return { action: "serve", lookupPath: input.path, reason: "accept_header" };
|
|
662
|
+
}
|
|
663
|
+
return { action: "pass", reason: "no_signal" };
|
|
664
|
+
}
|
|
665
|
+
function stripMdSuffix(path) {
|
|
666
|
+
const withoutSuffix = path.slice(0, -3);
|
|
667
|
+
if (withoutSuffix === "" || withoutSuffix === "/index") return "/";
|
|
668
|
+
return withoutSuffix;
|
|
669
|
+
}
|
|
670
|
+
function twinPathFor(path) {
|
|
671
|
+
if (path === "/") return "/index.md";
|
|
672
|
+
return `${path}.md`;
|
|
673
|
+
}
|
|
674
|
+
function buildTwinResponse(twin, decision, options) {
|
|
675
|
+
const headers = {
|
|
676
|
+
"content-type": twin.contentType ?? DEFAULT_TWIN_CONTENT_TYPE,
|
|
677
|
+
"cache-control": options.cacheControl ?? DEFAULT_TWIN_CACHE_CONTROL
|
|
678
|
+
};
|
|
679
|
+
if (decision.reason === "accept_header") {
|
|
680
|
+
headers["vary"] = "Accept";
|
|
681
|
+
}
|
|
682
|
+
if (twin.etag) headers["etag"] = twin.etag;
|
|
683
|
+
if (twin.lastModified) headers["last-modified"] = twin.lastModified;
|
|
684
|
+
return { status: 200, headers, body: twin.body };
|
|
685
|
+
}
|
|
686
|
+
function advertiseHeader(path) {
|
|
687
|
+
return `<${twinPathFor(path)}>; rel="alternate"; type="text/markdown"`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/web.ts
|
|
691
|
+
function observe(handler, options = {}) {
|
|
692
|
+
const config = resolveConfig(options);
|
|
693
|
+
const collector = options.collector ?? createCollector(config);
|
|
694
|
+
if (options.twin?.discovery) {
|
|
695
|
+
console.warn(
|
|
696
|
+
"[entreprenoid] twin.discovery is configured but the web adapter does not serve /llms.txt, /llms-full.txt or /install.md. Serve them from your own router, or use the Express adapter. See https://entreprenoid.com/install.md"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return async (request, ...rest) => {
|
|
700
|
+
const startedAt = Date.now();
|
|
701
|
+
let served;
|
|
702
|
+
let twinResponse;
|
|
703
|
+
if (options.twin) {
|
|
704
|
+
const url = new URL(request.url);
|
|
705
|
+
const path = normalisePath(url.pathname, config.redactPatterns);
|
|
706
|
+
const decision = decideTwin({
|
|
707
|
+
method: request.method,
|
|
708
|
+
path,
|
|
709
|
+
accept: request.headers.get("accept")
|
|
710
|
+
});
|
|
711
|
+
if (decision.action === "serve") {
|
|
712
|
+
try {
|
|
713
|
+
const found = await options.twin.resolve(decision.lookupPath);
|
|
714
|
+
if (found) {
|
|
715
|
+
const built = buildTwinResponse(found, decision, options.twin);
|
|
716
|
+
served = {
|
|
717
|
+
decision: "served",
|
|
718
|
+
reason: decision.reason,
|
|
719
|
+
format: built.headers["content-type"] ?? "text/markdown"
|
|
720
|
+
};
|
|
721
|
+
twinResponse = new Response(
|
|
722
|
+
request.method.toUpperCase() === "HEAD" ? null : built.body,
|
|
723
|
+
{ status: built.status, headers: built.headers }
|
|
724
|
+
);
|
|
725
|
+
} else {
|
|
726
|
+
served = { decision: "fell_through", reason: "no_twin" };
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
served = { decision: "error", reason: "resolver_error" };
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
served = { decision: "fell_through", reason: decision.reason };
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const response = twinResponse ?? await handler(request, ...rest);
|
|
736
|
+
if (options.twin && !twinResponse && options.twin.advertise !== false) {
|
|
737
|
+
try {
|
|
738
|
+
const path = normalisePath(new URL(request.url).pathname, config.redactPatterns);
|
|
739
|
+
if (await options.twin.resolve(path)) {
|
|
740
|
+
response.headers.append("link", advertiseHeader(path));
|
|
741
|
+
served = { decision: "advertised", reason: "no_twin" };
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
safe(() => {
|
|
747
|
+
if (config.disabled) return;
|
|
748
|
+
const url = new URL(request.url);
|
|
749
|
+
const observed = observeRequest(
|
|
750
|
+
{
|
|
751
|
+
method: request.method,
|
|
752
|
+
url: url.pathname + url.search,
|
|
753
|
+
host: url.host,
|
|
754
|
+
protocol: url.protocol === "https:" ? "https" : "http",
|
|
755
|
+
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
756
|
+
referer: request.headers.get("referer") ?? void 0
|
|
757
|
+
},
|
|
758
|
+
config
|
|
759
|
+
);
|
|
760
|
+
const event = {
|
|
761
|
+
...observed,
|
|
762
|
+
eventId: newEventId(),
|
|
763
|
+
siteId: config.siteId,
|
|
764
|
+
observedAt: new Date(startedAt).toISOString(),
|
|
765
|
+
...served ? { serve: served } : {},
|
|
766
|
+
response: observeResponse({
|
|
767
|
+
status: response.status,
|
|
768
|
+
contentType: response.headers.get("content-type"),
|
|
769
|
+
contentLength: response.headers.get("content-length"),
|
|
770
|
+
latencyMs: Date.now() - startedAt,
|
|
771
|
+
observation: "measured"
|
|
772
|
+
}),
|
|
773
|
+
sdk: {
|
|
774
|
+
name: SDK_NAME,
|
|
775
|
+
version: SDK_VERSION,
|
|
776
|
+
adapter: "web",
|
|
777
|
+
runtime: runtimeName()
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
collector.record(event);
|
|
781
|
+
});
|
|
782
|
+
if (options.waitUntil) {
|
|
783
|
+
safe(() => options.waitUntil?.(collector.flush()));
|
|
784
|
+
}
|
|
785
|
+
return response;
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/serve/discovery.ts
|
|
790
|
+
function titleFor(entry) {
|
|
791
|
+
if (entry.title) return entry.title;
|
|
792
|
+
if (entry.path === "/") return "Home";
|
|
793
|
+
const last = entry.path.split("/").filter(Boolean).pop() ?? entry.path;
|
|
794
|
+
return last.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
795
|
+
}
|
|
796
|
+
var twinPath = (path) => path === "/" ? "/index.md" : `${path}.md`;
|
|
797
|
+
function renderLlmsTxt(options) {
|
|
798
|
+
const origin = options.origin ?? "";
|
|
799
|
+
const lines = [`# ${options.siteName}`, ""];
|
|
800
|
+
if (options.description) {
|
|
801
|
+
for (const line of options.description.split("\n")) lines.push(`> ${line}`);
|
|
802
|
+
lines.push("");
|
|
803
|
+
}
|
|
804
|
+
lines.push(
|
|
805
|
+
"Every page listed here is also available as markdown: append `.md` to the path",
|
|
806
|
+
"(`/` becomes `/index.md`), or send `Accept: text/markdown`.",
|
|
807
|
+
"",
|
|
808
|
+
"## Pages",
|
|
809
|
+
""
|
|
810
|
+
);
|
|
811
|
+
for (const entry of options.entries) {
|
|
812
|
+
const href = `${origin}${twinPath(entry.path)}`;
|
|
813
|
+
lines.push(`- [${titleFor(entry)}](${href})${entry.summary ? `: ${entry.summary}` : ""}`);
|
|
814
|
+
}
|
|
815
|
+
lines.push("");
|
|
816
|
+
return lines.join("\n");
|
|
817
|
+
}
|
|
818
|
+
async function renderLlmsFullTxt(options, resolve) {
|
|
819
|
+
const parts = [`# ${options.siteName}`, ""];
|
|
820
|
+
if (options.description) {
|
|
821
|
+
for (const line of options.description.split("\n")) parts.push(`> ${line}`);
|
|
822
|
+
parts.push("");
|
|
823
|
+
}
|
|
824
|
+
for (const entry of options.entries) {
|
|
825
|
+
let twin;
|
|
826
|
+
try {
|
|
827
|
+
twin = await resolve(entry.path);
|
|
828
|
+
} catch {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (!twin) continue;
|
|
832
|
+
parts.push(`---`, ``, `## ${titleFor(entry)}`, ``, `Source: ${entry.path}`, ``, twin.body.trim(), ``);
|
|
833
|
+
}
|
|
834
|
+
return parts.join("\n");
|
|
835
|
+
}
|
|
836
|
+
function renderInstallMd(options = {}) {
|
|
837
|
+
const pkg = options.packageName ?? "@entreprenoid/analytics";
|
|
838
|
+
return `# entreprenoid install guide (for AI coding agents)
|
|
839
|
+
|
|
840
|
+
You are installing **entreprenoid** \u2014 analytics for the agentic web. It records every
|
|
841
|
+
request a site serves, especially from AI agents, and can serve those agents a clean
|
|
842
|
+
markdown twin of each page. Collection is fire-and-forget: it never blocks and never
|
|
843
|
+
fails the user's responses.
|
|
844
|
+
|
|
845
|
+
## Before you start
|
|
846
|
+
|
|
847
|
+
Every environment variable this package reads:
|
|
848
|
+
|
|
849
|
+
| Variable | Required | What it is |
|
|
850
|
+
| --- | --- | --- |
|
|
851
|
+
| \`ENTREPRENOID_INGEST_URL\` | **yes** | The ingest endpoint. \u26A0\uFE0F It must end in \`/v1/events\` \u2014 the SDK POSTs this URL verbatim and appends nothing, so a bare origin sends every batch to \`POST /\`, which is rejected. The SDK fails open, so you would see nothing at all. |
|
|
852
|
+
| \`ENTREPRENOID_SERVER_KEY\` | **yes** | The site's key, starting with \`ep_live_server_\`. |
|
|
853
|
+
| \`ENTREPRENOID_SITE_ID\` | no | The dashboard shows it beside the key. Safe to set, safe to omit: the server resolves the site from the credential and ignores this value. |
|
|
854
|
+
| \`ENTREPRENOID_ENABLED\` | no | \`false\` makes collection a transparent no-op without removing any code. Useful per environment. |
|
|
855
|
+
| \`ENTREPRENOID_DEBUG\` | no | \`true\` prints one startup line saying whether collection is on, and why not if it is off. |
|
|
856
|
+
|
|
857
|
+
**If either required value is missing, STOP and ask.** Do not invent them, and do not
|
|
858
|
+
leave a placeholder \u2014 a config that looks complete and is not is worse than no config.
|
|
859
|
+
|
|
860
|
+
Safety rules that apply throughout:
|
|
861
|
+
|
|
862
|
+
- **Never hardcode the key**, never print it, never commit it. Environment variables only.
|
|
863
|
+
- Before writing an env file, make sure it is gitignored.
|
|
864
|
+
- \u26A0\uFE0F **Never prefix the key with \`NEXT_PUBLIC_\`, \`VITE_\`, \`PUBLIC_\` or \`REACT_APP_\`.**
|
|
865
|
+
Those prefixes inline a value into client-side JavaScript, which publishes the key to
|
|
866
|
+
everyone who loads the page. The SDK refuses to start if it finds one.
|
|
867
|
+
- Only touch the files this install needs.
|
|
868
|
+
|
|
869
|
+
## Step 1 \u2014 install the package
|
|
870
|
+
|
|
871
|
+
Detect the package manager from the lockfile:
|
|
872
|
+
|
|
873
|
+
| Lockfile | Command |
|
|
874
|
+
| --- | --- |
|
|
875
|
+
| \`pnpm-lock.yaml\` | \`pnpm add ${pkg}\` |
|
|
876
|
+
| \`yarn.lock\` | \`yarn add ${pkg}\` |
|
|
877
|
+
| \`bun.lock\` | \`bun add ${pkg}\` |
|
|
878
|
+
| \`package-lock.json\` or none | \`npm install ${pkg}\` |
|
|
879
|
+
|
|
880
|
+
## Step 2 \u2014 wire up the collector (pick exactly ONE)
|
|
881
|
+
|
|
882
|
+
### Express
|
|
883
|
+
|
|
884
|
+
\`\`\`ts
|
|
885
|
+
import { entreprenoid } from "${pkg}/express";
|
|
886
|
+
|
|
887
|
+
app.use(entreprenoid());
|
|
888
|
+
\`\`\`
|
|
889
|
+
|
|
890
|
+
Add it **before** your routes so it observes all of them.
|
|
891
|
+
|
|
892
|
+
### Next.js (App Router, 14+)
|
|
893
|
+
|
|
894
|
+
Use the **Next adapter**, not the web one. In \`proxy.ts\` at the project root
|
|
895
|
+
(\`middleware.ts\` on Next 15 and earlier \u2014 same file, renamed in Next 16):
|
|
896
|
+
|
|
897
|
+
\`\`\`ts
|
|
898
|
+
import { after } from "next/server";
|
|
899
|
+
import { proxy } from "${pkg}/next";
|
|
900
|
+
|
|
901
|
+
export default proxy({ after });
|
|
902
|
+
|
|
903
|
+
export const config = {
|
|
904
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
905
|
+
};
|
|
906
|
+
\`\`\`
|
|
907
|
+
|
|
908
|
+
\u26A0\uFE0F **Do not use \`${pkg}/web\` in a Next proxy.** It runs and it lies. A proxy
|
|
909
|
+
executes *before* the route and hands control onward with a sentinel response \u2014 status
|
|
910
|
+
200, no real content type \u2014 so the web adapter would record a **measured 200 for every
|
|
911
|
+
request**, including the ones your routes render as 404 or 500.
|
|
912
|
+
|
|
913
|
+
The Next adapter emits only what a proxy can actually know, and **omits the response
|
|
914
|
+
entirely** rather than guessing at it. Your dashboard will show those requests with no
|
|
915
|
+
status, which is the truth: nothing observed one.
|
|
916
|
+
|
|
917
|
+
\u26A0\uFE0F **Pass \`after\`.** Without it the collector relies on its own timer, and a serverless
|
|
918
|
+
invocation can be frozen before that timer fires \u2014 events are simply lost, silently.
|
|
919
|
+
|
|
920
|
+
### Web-standard runtimes (Cloudflare Workers, Deno, Bun, Hono)
|
|
921
|
+
|
|
922
|
+
\`\`\`ts
|
|
923
|
+
import { observe } from "${pkg}/web";
|
|
924
|
+
|
|
925
|
+
export default {
|
|
926
|
+
fetch: observe(handler, { waitUntil: (p) => ctx.waitUntil(p) }),
|
|
927
|
+
};
|
|
928
|
+
\`\`\`
|
|
929
|
+
|
|
930
|
+
Pass \`waitUntil\` where the runtime offers one, or a serverless invocation can be
|
|
931
|
+
frozen before the events are sent.
|
|
932
|
+
|
|
933
|
+
## Step 3 \u2014 optional: serve a markdown twin
|
|
934
|
+
|
|
935
|
+
Agents pay for every token they read, and most of a modern page is markup they do not
|
|
936
|
+
want. Supply a resolver and the same middleware will serve clean markdown when a client
|
|
937
|
+
asks for it:
|
|
938
|
+
|
|
939
|
+
\`\`\`ts
|
|
940
|
+
app.use(entreprenoid({
|
|
941
|
+
twin: { resolve: (path) => markdownFor(path) },
|
|
942
|
+
}));
|
|
943
|
+
\`\`\`
|
|
944
|
+
|
|
945
|
+
\u26A0\uFE0F **A browser never receives markdown.** The twin is served only when the path ends in
|
|
946
|
+
\`.md\` or the \`Accept\` header explicitly prefers \`text/markdown\` \u2014 never based on the
|
|
947
|
+
User-Agent, which would be cloaking and would break shared caching.
|
|
948
|
+
|
|
949
|
+
## Step 4 \u2014 \u26A0\uFE0F look at the routes before you go live
|
|
950
|
+
|
|
951
|
+
**Do not skip this one.** The path is sent as it arrives. The query string is dropped
|
|
952
|
+
before anything parses it, and the \`Referer\` is reduced to an origin \u2014 but the path
|
|
953
|
+
itself is data, and on a lot of sites the path carries secrets:
|
|
954
|
+
|
|
955
|
+
\`\`\`
|
|
956
|
+
/reveal/<single-use-token> /join/<invite-code>
|
|
957
|
+
/confirm/<token> /upload/<ticket>
|
|
958
|
+
\`\`\`
|
|
959
|
+
|
|
960
|
+
Read the project's routes. For each one, decide:
|
|
961
|
+
|
|
962
|
+
\`\`\`ts
|
|
963
|
+
app.use(entreprenoid({
|
|
964
|
+
// Collapse identifiers so analytics never sees a per-user value, and so one
|
|
965
|
+
// route does not become ten thousand rows.
|
|
966
|
+
routeTemplate: (path) => path.replace(/\\/\\d+(?=\\/|$)/g, "/:id"),
|
|
967
|
+
|
|
968
|
+
// Replace any segment that is a token, code or personal identifier.
|
|
969
|
+
redactPatterns: [/^[A-Za-z0-9_-]{20,}$/],
|
|
970
|
+
|
|
971
|
+
// Traffic you do not want counted: health checks, your own office, previews.
|
|
972
|
+
isInternal: (req) => req.path.startsWith("/_health"),
|
|
973
|
+
}));
|
|
974
|
+
\`\`\`
|
|
975
|
+
|
|
976
|
+
\u26A0\uFE0F **A default backstop already runs, and you should not rely on it.** Segments that
|
|
977
|
+
look like credentials \u2014 uuids, cuids, JWTs, long hex, dense mixed-case strings \u2014 are
|
|
978
|
+
replaced with \`[redacted]\` before the event is sent, and the event records that it
|
|
979
|
+
happened. It cannot catch a short token like \`/j/aB3xK9\`, and it does not know which of
|
|
980
|
+
this project's ids are sensitive. **Only the routes tell you that.** Set
|
|
981
|
+
\`redactHighEntropyPaths: false\` to turn the backstop off; that never disables
|
|
982
|
+
\`redactPatterns\`, which are yours.
|
|
983
|
+
|
|
984
|
+
If you are unsure whether a path segment is a secret, treat it as one and say so in your
|
|
985
|
+
summary to the user.
|
|
986
|
+
|
|
987
|
+
## Step 5 \u2014 verify
|
|
988
|
+
|
|
989
|
+
Start the app and make one request. Within a few seconds the dashboard should show it.
|
|
990
|
+
If nothing arrives:
|
|
991
|
+
|
|
992
|
+
- check the key is set in the server's environment, not the client's
|
|
993
|
+
- check \`ENTREPRENOID_INGEST_URL\` ends in \`/v1/events\`
|
|
994
|
+
- set \`ENTREPRENOID_DEBUG=true\` and read the startup line
|
|
995
|
+
|
|
996
|
+
**Do not add retry logic, queues or error handling around the SDK.** It already buffers,
|
|
997
|
+
retries with backoff, and fails open. Wrapping it in a try/catch is harmless; awaiting it
|
|
998
|
+
is not, and would put analytics on your critical path.
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/express.ts
|
|
1003
|
+
function header(req, name) {
|
|
1004
|
+
const value = req.headers[name];
|
|
1005
|
+
if (Array.isArray(value)) return value[0];
|
|
1006
|
+
return value;
|
|
1007
|
+
}
|
|
1008
|
+
function headerString(res, name) {
|
|
1009
|
+
const value = res.getHeader(name);
|
|
1010
|
+
if (value === void 0 || value === null) return void 0;
|
|
1011
|
+
return Array.isArray(value) ? value[0] : String(value);
|
|
1012
|
+
}
|
|
1013
|
+
function entreprenoid(options = {}) {
|
|
1014
|
+
const config = resolveConfig(options);
|
|
1015
|
+
const collector = options.collector ?? createCollector(config);
|
|
1016
|
+
const twin = options.twin;
|
|
1017
|
+
return function entreprenoidMiddleware(req, res, next) {
|
|
1018
|
+
if (config.disabled && !twin) {
|
|
1019
|
+
next();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const startedAt = Date.now();
|
|
1023
|
+
const startedHr = process.hrtime.bigint();
|
|
1024
|
+
let recorded = false;
|
|
1025
|
+
let served;
|
|
1026
|
+
const record = () => {
|
|
1027
|
+
if (recorded) return;
|
|
1028
|
+
recorded = true;
|
|
1029
|
+
safe(() => {
|
|
1030
|
+
const observed = observeRequest(
|
|
1031
|
+
{
|
|
1032
|
+
method: req.method ?? "GET",
|
|
1033
|
+
url: req.originalUrl ?? req.url ?? "/",
|
|
1034
|
+
host: header(req, "host"),
|
|
1035
|
+
protocol: req.secure || req.protocol === "https" ? "https" : "http",
|
|
1036
|
+
userAgent: header(req, "user-agent"),
|
|
1037
|
+
referer: header(req, "referer") ?? header(req, "referrer")
|
|
1038
|
+
},
|
|
1039
|
+
config
|
|
1040
|
+
);
|
|
1041
|
+
const finished = res.writableFinished !== false;
|
|
1042
|
+
const latencyMs = Number(process.hrtime.bigint() - startedHr) / 1e6;
|
|
1043
|
+
const event = {
|
|
1044
|
+
...observed,
|
|
1045
|
+
eventId: newEventId(),
|
|
1046
|
+
siteId: config.siteId,
|
|
1047
|
+
observedAt: new Date(startedAt).toISOString(),
|
|
1048
|
+
response: finished ? observeResponse({
|
|
1049
|
+
status: res.statusCode,
|
|
1050
|
+
contentType: headerString(res, "content-type"),
|
|
1051
|
+
contentLength: headerString(res, "content-length"),
|
|
1052
|
+
latencyMs,
|
|
1053
|
+
observation: "measured"
|
|
1054
|
+
}) : observeResponse({ latencyMs, observation: "unknown" }),
|
|
1055
|
+
...served ? { serve: served } : {},
|
|
1056
|
+
sdk: {
|
|
1057
|
+
name: SDK_NAME,
|
|
1058
|
+
version: SDK_VERSION,
|
|
1059
|
+
adapter: "express",
|
|
1060
|
+
runtime: runtimeName()
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
collector.record(event);
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
safe(() => {
|
|
1067
|
+
res.once("finish", record);
|
|
1068
|
+
res.once("close", record);
|
|
1069
|
+
});
|
|
1070
|
+
if (!twin) {
|
|
1071
|
+
next();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const path = normalisePath(req.originalUrl ?? req.url ?? "/", config.redactPatterns);
|
|
1075
|
+
if (twin.discovery && DISCOVERY_PATHS.includes(path)) {
|
|
1076
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1077
|
+
if (method === "GET" || method === "HEAD") {
|
|
1078
|
+
void (async () => {
|
|
1079
|
+
try {
|
|
1080
|
+
const body = path === "/install.md" ? renderInstallMd() : path === "/llms.txt" ? renderLlmsTxt(twin.discovery) : await renderLlmsFullTxt(twin.discovery, twin.resolve);
|
|
1081
|
+
res.setHeader("content-type", "text/markdown; charset=utf-8");
|
|
1082
|
+
res.setHeader("cache-control", twin.cacheControl ?? "public, max-age=3600, s-maxage=86400");
|
|
1083
|
+
res.statusCode = 200;
|
|
1084
|
+
served = { decision: "served", reason: "md_path", format: "text/markdown; charset=utf-8" };
|
|
1085
|
+
if (method === "HEAD") res.end();
|
|
1086
|
+
else res.end(body);
|
|
1087
|
+
} catch {
|
|
1088
|
+
served = { decision: "error", reason: "resolver_error" };
|
|
1089
|
+
if (!res.headersSent) next();
|
|
1090
|
+
}
|
|
1091
|
+
})();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const decision = decideTwin({
|
|
1096
|
+
method: req.method ?? "GET",
|
|
1097
|
+
path,
|
|
1098
|
+
accept: header(req, "accept")
|
|
1099
|
+
});
|
|
1100
|
+
if (decision.action === "pass") {
|
|
1101
|
+
if (twin.advertise !== false) {
|
|
1102
|
+
void Promise.resolve().then(() => twin.resolve(path)).then((found) => {
|
|
1103
|
+
if (found && !res.headersSent) {
|
|
1104
|
+
safe(() => res.setHeader("link", advertiseHeader(path)));
|
|
1105
|
+
served = { decision: "advertised", reason: "no_twin" };
|
|
1106
|
+
}
|
|
1107
|
+
}).catch(() => void 0).finally(() => next());
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
served = { decision: "fell_through", reason: decision.reason };
|
|
1111
|
+
next();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
void Promise.resolve().then(() => twin.resolve(decision.lookupPath)).then((found) => {
|
|
1115
|
+
if (!found) {
|
|
1116
|
+
served = { decision: "fell_through", reason: "no_twin" };
|
|
1117
|
+
next();
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const built = buildTwinResponse(found, decision, twin);
|
|
1121
|
+
served = {
|
|
1122
|
+
decision: "served",
|
|
1123
|
+
reason: decision.reason,
|
|
1124
|
+
format: built.headers["content-type"] ?? "text/markdown"
|
|
1125
|
+
};
|
|
1126
|
+
for (const [name, value] of Object.entries(built.headers)) {
|
|
1127
|
+
res.setHeader(name, value);
|
|
1128
|
+
}
|
|
1129
|
+
res.statusCode = built.status;
|
|
1130
|
+
if ((req.method ?? "GET").toUpperCase() === "HEAD") res.end();
|
|
1131
|
+
else res.end(built.body);
|
|
1132
|
+
}).catch(() => {
|
|
1133
|
+
served = { decision: "error", reason: "resolver_error" };
|
|
1134
|
+
if (!res.headersSent) next();
|
|
1135
|
+
});
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/next.ts
|
|
1140
|
+
function proxy(options = {}) {
|
|
1141
|
+
const config = resolveConfig(options);
|
|
1142
|
+
const collector = options.collector ?? createCollector(config);
|
|
1143
|
+
return async (request) => {
|
|
1144
|
+
const startedAt = Date.now();
|
|
1145
|
+
let served;
|
|
1146
|
+
let twinResponse;
|
|
1147
|
+
if (options.twin) {
|
|
1148
|
+
try {
|
|
1149
|
+
const url = new URL(request.url);
|
|
1150
|
+
const decision = decideTwin({
|
|
1151
|
+
method: request.method,
|
|
1152
|
+
path: normalisePath(url.pathname),
|
|
1153
|
+
accept: request.headers.get("accept")
|
|
1154
|
+
});
|
|
1155
|
+
if (decision.action === "serve") {
|
|
1156
|
+
const found = await options.twin.resolve(decision.lookupPath);
|
|
1157
|
+
if (found) {
|
|
1158
|
+
const built = buildTwinResponse(found, decision, options.twin);
|
|
1159
|
+
served = {
|
|
1160
|
+
decision: "served",
|
|
1161
|
+
reason: decision.reason,
|
|
1162
|
+
format: built.headers["content-type"] ?? "text/markdown"
|
|
1163
|
+
};
|
|
1164
|
+
twinResponse = new Response(
|
|
1165
|
+
request.method.toUpperCase() === "HEAD" ? null : built.body,
|
|
1166
|
+
{ status: built.status, headers: built.headers }
|
|
1167
|
+
);
|
|
1168
|
+
} else {
|
|
1169
|
+
served = { decision: "fell_through", reason: "no_twin" };
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
served = { decision: "fell_through", reason: decision.reason };
|
|
1173
|
+
}
|
|
1174
|
+
} catch {
|
|
1175
|
+
served = { decision: "error", reason: "resolver_error" };
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const record = () => {
|
|
1179
|
+
safe(() => {
|
|
1180
|
+
if (config.disabled) return;
|
|
1181
|
+
const url = new URL(request.url);
|
|
1182
|
+
const observed = observeRequest(
|
|
1183
|
+
{
|
|
1184
|
+
method: request.method,
|
|
1185
|
+
url: url.pathname + url.search,
|
|
1186
|
+
host: url.host,
|
|
1187
|
+
protocol: url.protocol === "https:" ? "https" : "http",
|
|
1188
|
+
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
1189
|
+
referer: request.headers.get("referer") ?? void 0
|
|
1190
|
+
},
|
|
1191
|
+
config
|
|
1192
|
+
);
|
|
1193
|
+
const event = {
|
|
1194
|
+
...observed,
|
|
1195
|
+
eventId: newEventId(),
|
|
1196
|
+
// ⚠️ Emitted so a post-response observation CAN be merged later.
|
|
1197
|
+
// Nothing merges it today, and the docblock above says so plainly
|
|
1198
|
+
// rather than letting the field imply otherwise.
|
|
1199
|
+
requestId: newEventId(),
|
|
1200
|
+
siteId: config.siteId,
|
|
1201
|
+
observedAt: new Date(startedAt).toISOString(),
|
|
1202
|
+
...served ? { serve: served } : {},
|
|
1203
|
+
// ⚠️ **The response object is present ONLY when we built it.**
|
|
1204
|
+
//
|
|
1205
|
+
// If the twin was served, this proxy IS the responder and measured it.
|
|
1206
|
+
// Otherwise the route has not run yet and there is nothing to
|
|
1207
|
+
// observe — so the key is OMITTED, not set to a guess, not set to
|
|
1208
|
+
// `observation: "unknown"` with a status (which the schema refuses),
|
|
1209
|
+
// and not set to a latency-only object (we did not wait for the
|
|
1210
|
+
// response, so we did not measure a latency either).
|
|
1211
|
+
...twinResponse ? {
|
|
1212
|
+
response: observeResponse({
|
|
1213
|
+
status: twinResponse.status,
|
|
1214
|
+
contentType: twinResponse.headers.get("content-type"),
|
|
1215
|
+
contentLength: twinResponse.headers.get("content-length"),
|
|
1216
|
+
latencyMs: Date.now() - startedAt,
|
|
1217
|
+
observation: "measured"
|
|
1218
|
+
})
|
|
1219
|
+
} : {},
|
|
1220
|
+
sdk: {
|
|
1221
|
+
name: SDK_NAME,
|
|
1222
|
+
version: SDK_VERSION,
|
|
1223
|
+
// ⚠️ `next-proxy`, which is the name the WIRE SCHEMA already uses
|
|
1224
|
+
// in its own docblock (`event.ts:157`). Its own name matters here
|
|
1225
|
+
// more than for any other adapter: this is the one whose events
|
|
1226
|
+
// legitimately carry no response, and the dashboard must not
|
|
1227
|
+
// present an adapter's blind spot as a fact about the traffic.
|
|
1228
|
+
adapter: "next-proxy",
|
|
1229
|
+
runtime: runtimeName()
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
collector.record(event);
|
|
1233
|
+
});
|
|
1234
|
+
};
|
|
1235
|
+
if (options.after) {
|
|
1236
|
+
try {
|
|
1237
|
+
options.after(record);
|
|
1238
|
+
} catch {
|
|
1239
|
+
record();
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
record();
|
|
1243
|
+
}
|
|
1244
|
+
return twinResponse;
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
exports.BoundedQueue = BoundedQueue;
|
|
1249
|
+
exports.CAMPAIGN_PARAMS = CAMPAIGN_PARAMS;
|
|
1250
|
+
exports.DEFAULTS = DEFAULTS;
|
|
1251
|
+
exports.DEFAULT_TWIN_CACHE_CONTROL = DEFAULT_TWIN_CACHE_CONTROL;
|
|
1252
|
+
exports.DEFAULT_TWIN_CONTENT_TYPE = DEFAULT_TWIN_CONTENT_TYPE;
|
|
1253
|
+
exports.EntreprenoidConfigError = EntreprenoidConfigError;
|
|
1254
|
+
exports.MARKDOWN_TYPES = MARKDOWN_TYPES;
|
|
1255
|
+
exports.MAX_BODY_BYTES = MAX_BODY_BYTES;
|
|
1256
|
+
exports.REDACTED = REDACTED;
|
|
1257
|
+
exports.UNSET_SITE_ID = UNSET_SITE_ID;
|
|
1258
|
+
exports.advertiseHeader = advertiseHeader;
|
|
1259
|
+
exports.buildTwinResponse = buildTwinResponse;
|
|
1260
|
+
exports.createBreaker = createBreaker;
|
|
1261
|
+
exports.createCollector = createCollector;
|
|
1262
|
+
exports.decideTwin = decideTwin;
|
|
1263
|
+
exports.encodeBatch = encodeBatch;
|
|
1264
|
+
exports.entreprenoid = entreprenoid;
|
|
1265
|
+
exports.fetchTransport = fetchTransport;
|
|
1266
|
+
exports.looksLikeSecret = looksLikeSecret;
|
|
1267
|
+
exports.newEventId = newEventId;
|
|
1268
|
+
exports.normalisePath = normalisePath;
|
|
1269
|
+
exports.observe = observe;
|
|
1270
|
+
exports.observeRequest = observeRequest;
|
|
1271
|
+
exports.observeResponse = observeResponse;
|
|
1272
|
+
exports.outcomeForStatus = outcomeForStatus;
|
|
1273
|
+
exports.parseAccept = parseAccept;
|
|
1274
|
+
exports.prefersMarkdown = prefersMarkdown;
|
|
1275
|
+
exports.proxy = proxy;
|
|
1276
|
+
exports.redact = redact;
|
|
1277
|
+
exports.redactPath = redactPath;
|
|
1278
|
+
exports.referrerOrigin = referrerOrigin;
|
|
1279
|
+
exports.renderInstallMd = renderInstallMd;
|
|
1280
|
+
exports.renderLlmsFullTxt = renderLlmsFullTxt;
|
|
1281
|
+
exports.renderLlmsTxt = renderLlmsTxt;
|
|
1282
|
+
exports.resolveConfig = resolveConfig;
|
|
1283
|
+
exports.runtimeName = runtimeName;
|
|
1284
|
+
exports.safe = safe;
|
|
1285
|
+
exports.safeAsync = safeAsync;
|
|
1286
|
+
exports.stripMdSuffix = stripMdSuffix;
|
|
1287
|
+
exports.twinPathFor = twinPathFor;
|
|
1288
|
+
//# sourceMappingURL=index.cjs.map
|
|
1289
|
+
//# sourceMappingURL=index.cjs.map
|