@chatpanel/events 0.101.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -348
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/source-gate.js
CHANGED
|
@@ -1,92 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// away? — as a GATE, not a ranking.
|
|
3
|
-
//
|
|
4
|
-
// Routing is not enough. A router returns null in every uncertain case and null means
|
|
5
|
-
// "leave the choice alone", which is right for a preference and catastrophic for a privacy
|
|
6
|
-
// rule: an internal page with no local model available would have gone to the third party
|
|
7
|
-
// the user had selected. And routing only runs under Auto, so a manually chosen cloud model
|
|
8
|
-
// bypassed it entirely. So this runs on every turn, after routing has had its chance to
|
|
9
|
-
// pick something local, and it REFUSES rather than substituting: silently answering from a
|
|
10
|
-
// different model is the substitution this codebase keeps removing, and silently sending
|
|
11
|
-
// anyway is the leak it exists to stop.
|
|
12
|
-
//
|
|
13
|
-
// Was the extension's `sourceGate` + `sourcePolicySettings` + `sourceUrlsOf`; the desktop
|
|
14
|
-
// had none of it and sent internal pages wherever the picker pointed. The policy (which
|
|
15
|
-
// hosts are internal, how far their content may travel) is the user's and travels in the
|
|
16
|
-
// shared `internalSites` preference section, so both clients enforce the same rule.
|
|
17
|
-
//
|
|
18
|
-
// Class R: no I/O. The target's reach comes from `reachOf` (a bridge agent is 'trusted', a
|
|
19
|
-
// localhost endpoint 'device', the rest 'any') unless the caller already knows it.
|
|
20
|
-
|
|
21
|
-
import { sourcePolicyFor, sourceUrlsOf, DEFAULT_INTERNAL_PATTERNS } from './sources.js';
|
|
22
|
-
import { reachOf } from './model-candidates.js';
|
|
23
|
-
import { REACH, reachRank } from './reach.js';
|
|
24
|
-
|
|
25
|
-
// `sourceUrlsOf` lives in sources.js (no router on its graph) so a client's first paint can
|
|
26
|
-
// list a turn's addresses without pulling the router in; it is re-exported here for callers
|
|
27
|
-
// that have the gate anyway.
|
|
28
|
-
export { sourceUrlsOf };
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The internal-source policy, read from the `privacy` settings object in ONE place.
|
|
32
|
-
*
|
|
33
|
-
* NEVER CONFIGURED and CONFIGURED TO NOTHING are different answers. Undefined means the
|
|
34
|
-
* user has not been here yet, so the built-ins apply; an array — even an empty one — is a
|
|
35
|
-
* list they edited, and prepending our own to it would make a default impossible to
|
|
36
|
-
* remove. Someone testing against localhost has a real reason to delete that line.
|
|
37
|
-
*/
|
|
38
|
-
export function sourcePolicySettings(privacy = {}) {
|
|
39
|
-
const cfg = privacy || {};
|
|
40
|
-
const saved = cfg.internalPatterns;
|
|
41
|
-
const list = Array.isArray(saved)
|
|
42
|
-
? saved
|
|
43
|
-
: (saved == null ? DEFAULT_INTERNAL_PATTERNS : String(saved).split(/[\s,]+/));
|
|
44
|
-
return {
|
|
45
|
-
enabled: cfg.internalGuard !== false,
|
|
46
|
-
patterns: list.map((x) => String(x || '').trim().toLowerCase()).filter(Boolean),
|
|
47
|
-
ceiling: cfg.internalCeiling === 'trusted' ? 'trusted' : 'device',
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** What the sources of a turn allow. Null when the guard is off or nothing matched. */
|
|
52
|
-
export function sourceGuardFor(policy, sources = []) {
|
|
53
|
-
if (!policy || !policy.enabled || !sources?.length) return null;
|
|
54
|
-
const p = sourcePolicyFor(sources, { patterns: policy.patterns, ceiling: policy.ceiling });
|
|
55
|
-
return p.internal ? p : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The gate. `{ blocked, why, reach, message }` when the target is out of reach for these
|
|
60
|
-
* sources; null when the turn may go.
|
|
61
|
-
*
|
|
62
|
-
* @param policy `sourcePolicySettings(privacy)`
|
|
63
|
-
* @param messages the conversation as it will be sent
|
|
64
|
-
* @param sources anything else the caller knows was attached
|
|
65
|
-
* @param target `{ kind, baseUrl }` (reach computed) or `{ reach }` (reach known)
|
|
66
|
-
* @param label how to name the target in the message
|
|
67
|
-
*/
|
|
68
|
-
export function sourceGate({ policy, messages = [], sources = [], target = {}, label = '' } = {}) {
|
|
69
|
-
const guard = sourceGuardFor(policy, sourceUrlsOf(messages, sources));
|
|
70
|
-
if (!guard) return null;
|
|
71
|
-
const actual = REACH.includes(target?.reach) ? target.reach : reachOf(target || {});
|
|
72
|
-
if (reachRank(actual) <= reachRank(guard.reach)) return null;
|
|
73
|
-
const where = guard.reach === 'device' ? 'stay on this device' : 'stay inside your workspace';
|
|
74
|
-
return {
|
|
75
|
-
blocked: true,
|
|
76
|
-
why: guard.why,
|
|
77
|
-
reach: guard.reach,
|
|
78
|
-
// Name the source, the model and the way out. A refusal a person cannot act on gets
|
|
79
|
-
// switched off wholesale, which would leave them worse protected than before.
|
|
80
|
-
message: `Not sent: ${guard.why}. "${label || 'this model'}" is outside that, and content from an internal source must ${where}. `
|
|
81
|
-
+ 'Pick a local model (or run one), or remove this site under Settings → Privacy → Internal sites.',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Is this error the gate's refusal? A runner reads it as "this model, not this task". */
|
|
86
|
-
export const isSourceGateError = (err) => /^Not sent: /.test(String(err?.message || err || ''));
|
|
87
|
-
|
|
88
|
-
/** The candidates a turn under `guard` may be handed at all — for a roster, before appointment. */
|
|
89
|
-
export function withinReach(candidates = [], guard = null) {
|
|
90
|
-
if (!guard) return candidates;
|
|
91
|
-
return (candidates || []).filter((c) => reachRank(REACH.includes(c?.reach) ? c.reach : reachOf(c || {})) <= reachRank(guard.reach));
|
|
92
|
-
}
|
|
1
|
+
import{sourcePolicyFor as f,sourceUrlsOf as a,DEFAULT_INTERNAL_PATTERNS as d}from"./sources.js";import{reachOf as c}from"./model-candidates.js";import{REACH as l,reachRank as o}from"./reach.js";function w(e={}){const t=e||{},r=t.internalPatterns,i=Array.isArray(r)?r:r==null?d:String(r).split(/[\s,]+/);return{enabled:t.internalGuard!==!1,patterns:i.map(s=>String(s||"").trim().toLowerCase()).filter(Boolean),ceiling:t.internalCeiling==="trusted"?"trusted":"device"}}function m(e,t=[]){if(!e||!e.enabled||!t?.length)return null;const r=f(t,{patterns:e.patterns,ceiling:e.ceiling});return r.internal?r:null}function S({policy:e,messages:t=[],sources:r=[],target:i={},label:s=""}={}){const n=m(e,a(t,r));if(!n)return null;const u=l.includes(i?.reach)?i.reach:c(i||{});if(o(u)<=o(n.reach))return null;const h=n.reach==="device"?"stay on this device":"stay inside your workspace";return{blocked:!0,why:n.why,reach:n.reach,message:`Not sent: ${n.why}. "${s||"this model"}" is outside that, and content from an internal source must ${h}. Pick a local model (or run one), or remove this site under Settings → Privacy → Internal sites.`}}const v=e=>/^Not sent: /.test(String(e?.message||e||""));function x(e=[],t=null){return t?(e||[]).filter(r=>o(l.includes(r?.reach)?r.reach:c(r||{}))<=o(t.reach)):e}export{v as isSourceGateError,S as sourceGate,m as sourceGuardFor,w as sourcePolicySettings,a as sourceUrlsOf,x as withinReach};
|
package/sources-retrieval.js
CHANGED
|
@@ -1,189 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Attached context used to be flattened into the first message: every attached tab, in full,
|
|
5
|
-
* before the model had said anything. So "hi" on a long page paid for the whole page, and
|
|
6
|
-
* five attached tabs meant five documents in the prompt to answer a question that concerned
|
|
7
|
-
* one paragraph of one of them.
|
|
8
|
-
*
|
|
9
|
-
* The model is instead shown a MANIFEST — what exists, how big it is, where it came from —
|
|
10
|
-
* and pulls what it needs. Three things follow that are not just savings:
|
|
11
|
-
*
|
|
12
|
-
* - The turn starts small, so a cheap model can handle a greeting on a heavy page.
|
|
13
|
-
* - What was read is a fact in the log rather than an assumption about the prompt.
|
|
14
|
-
* - Retrieval takes a QUERY, so a large source returns the relevant part instead of its
|
|
15
|
-
* first N characters — which is what truncation gives you, and it is rarely the part
|
|
16
|
-
* that matters.
|
|
17
|
-
*
|
|
18
|
-
* Pure and dependency-free: the extension, the gateway and the bridge all have attached
|
|
19
|
-
* sources, and three implementations of "what did the model actually read" would drift.
|
|
20
|
-
*/
|
|
1
|
+
const $=/\n{2,}/,d=i=>Math.ceil(String(i||"").length/4);function p(i,r=0){return`${String(i?.kind||"src").toLowerCase().replace(/[^a-z0-9]+/g,"")||"src"}-${r+1}`}function S(i=[]){const r=(Array.isArray(i)?i:[i]).filter(e=>e&&(e.text||e.url)).map((e,t)=>({id:e.id||p(e,t),kind:e.kind||"context",title:e.title||e.url||"Untitled",url:e.url||"",text:String(e.text||""),tokens:d(e.text)})),o=new Map(r.map(e=>[e.id,e]));return{entries:r,get:e=>o.get(String(e||"").trim())||null,get tokens(){return r.reduce((e,t)=>e+t.tokens,0)}}}function k(i,r=120){const o=String(i||"");if(!o||o.length<=r)return o;try{const e=new URL(o),t=`${e.host}${e.pathname}`.replace(/\/$/,""),c=[...e.searchParams.entries()][0],u=c&&`${c[0]}=${c[1]}`,a=u&&`${t}?${u}`,h=a&&a.length<=r?a:t;return h.length<=r?`${h}…`:`${h.slice(0,r)}…`}catch{return`${o.slice(0,r)}…`}}function b(i){return i?.entries?.length?["Attached sources (NOT included below — read them with the `source` tool):",...i.entries.map(o=>`- ${o.id} — ${o.title}${o.url?` (${k(o.url)})`:""} · ~${o.tokens} tokens`)].join(`
|
|
2
|
+
`):""}function w(i,r){if(!r.length)return 0;const o=i.toLowerCase();let e=0;for(const t of r)o.includes(t)&&(e+=Math.min(3,t.length/4));return e}const x=new Set(["the","and","for","this","that","with","from","what","when","where","which","have","has","was","are","you","your","can","about","into","does","did","how","why","all","any","its"]);function m(i){return String(i||"").toLowerCase().split(/[^a-z0-9]+/).filter(r=>r.length>2&&!x.has(r))}function y(i,{id:r,query:o="",maxTokens:e=2e3}={}){const t=i?.get?.(r);if(!t){const n=(i?.entries||[]).map(s=>s.id).join(", ");return{error:`No attached source '${r}'.${n?` Available: ${n}.`:" Nothing is attached."}`}}const c=Math.max(200,Number(e)||2e3);if(t.tokens<=c)return{id:t.id,title:t.title,url:t.url,text:t.text,truncated:!1,of:t.tokens};const u=m(o),a=t.text.split($).filter(n=>n.trim());if(!u.length){const n=t.text.slice(0,c*4);return{id:t.id,title:t.title,url:t.url,text:n,truncated:!0,of:t.tokens,note:`Showing the first ~${d(n)} of ${t.tokens} tokens. Pass a query to get the relevant parts instead.`}}const h=a.map((n,s)=>({c:n,i:s,s:w(n,u)})).filter(n=>n.s>0).sort((n,s)=>s.s-n.s),l=[];let f=0;for(const n of h){const s=d(n.c);f+s>c||(l.push(n),f+=s)}if(!l.length){const n=t.text.slice(0,c*4);return{id:t.id,title:t.title,url:t.url,text:n,truncated:!0,of:t.tokens,note:`Nothing in this source matched '${o}'. Showing the beginning instead.`}}l.sort((n,s)=>n.i-s.i);const g=l.some((n,s)=>s>0&&n.i!==l[s-1].i+1);return{id:t.id,title:t.title,url:t.url,text:l.map(n=>n.c).join(g?`
|
|
21
3
|
|
|
22
|
-
|
|
4
|
+
[…]
|
|
23
5
|
|
|
24
|
-
|
|
25
|
-
export const approxTokens = (s) => Math.ceil(String(s || '').length / 4);
|
|
6
|
+
`:`
|
|
26
7
|
|
|
27
|
-
|
|
28
|
-
* A stable, short id for a source. The model has to type this back, so it is derived from the
|
|
29
|
-
* title rather than from a random string: `page-2` is recoverable from a manifest a model
|
|
30
|
-
* half-remembers in a way that `k3f9a1` is not.
|
|
31
|
-
*/
|
|
32
|
-
export function sourceId(source, index = 0) {
|
|
33
|
-
const base = String(source?.kind || 'src').toLowerCase().replace(/[^a-z0-9]+/g, '') || 'src';
|
|
34
|
-
return `${base}-${index + 1}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Build the manifest the model sees INSTEAD of the content.
|
|
39
|
-
*
|
|
40
|
-
* Deliberately includes the size. A model that can see one source is 300 tokens and another
|
|
41
|
-
* is 40,000 can decide to read the small one whole and query the large one — a decision it
|
|
42
|
-
* cannot make when both are simply "a document".
|
|
43
|
-
*/
|
|
44
|
-
export function makeSourceStore(sources = []) {
|
|
45
|
-
const entries = (Array.isArray(sources) ? sources : [sources])
|
|
46
|
-
.filter((s) => s && (s.text || s.url))
|
|
47
|
-
.map((s, i) => ({
|
|
48
|
-
id: s.id || sourceId(s, i),
|
|
49
|
-
kind: s.kind || 'context',
|
|
50
|
-
title: s.title || s.url || 'Untitled',
|
|
51
|
-
url: s.url || '',
|
|
52
|
-
text: String(s.text || ''),
|
|
53
|
-
tokens: approxTokens(s.text),
|
|
54
|
-
}));
|
|
55
|
-
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
56
|
-
return {
|
|
57
|
-
entries,
|
|
58
|
-
get: (id) => byId.get(String(id || '').trim()) || null,
|
|
59
|
-
get tokens() { return entries.reduce((a, e) => a + e.tokens, 0); },
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* WHERE IT CAME FROM, not the whole address bar.
|
|
65
|
-
*
|
|
66
|
-
* A Google results URL carries about a thousand characters of tracking parameters — sca_esv,
|
|
67
|
-
* gs_lp, sclient — and the manifest printed every one of them, on every turn, in a line whose
|
|
68
|
-
* job is "here is a source you may read". That is a quarter of a thousand tokens of noise the
|
|
69
|
-
* model cannot use and must still read past.
|
|
70
|
-
*
|
|
71
|
-
* The host and path identify the source; the query string is kept only when it is short
|
|
72
|
-
* enough to be meaning rather than machinery. An unparseable URL is left exactly as given —
|
|
73
|
-
* guessing at a string we could not read is how a source becomes unidentifiable.
|
|
74
|
-
*/
|
|
75
|
-
export function shortUrl(url, max = 120) {
|
|
76
|
-
const raw = String(url || '');
|
|
77
|
-
if (!raw || raw.length <= max) return raw;
|
|
78
|
-
try {
|
|
79
|
-
const u = new URL(raw);
|
|
80
|
-
const base = `${u.host}${u.pathname}`.replace(/\/$/, '');
|
|
81
|
-
// A short query is often the whole point of the address ("?q=how+do+tides+work");
|
|
82
|
-
// a long one is machinery. Keep the first parameter when it fits, drop the rest.
|
|
83
|
-
const first = [...u.searchParams.entries()][0];
|
|
84
|
-
const q = first && `${first[0]}=${first[1]}`;
|
|
85
|
-
const withQ = q && `${base}?${q}`;
|
|
86
|
-
const out = withQ && withQ.length <= max ? withQ : base;
|
|
87
|
-
return out.length <= max ? `${out}…` : `${out.slice(0, max)}…`;
|
|
88
|
-
} catch {
|
|
89
|
-
return `${raw.slice(0, max)}…`;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** One line per source: what it is, where it came from, what it would cost to read. */
|
|
94
|
-
export function manifestText(store) {
|
|
95
|
-
if (!store?.entries?.length) return '';
|
|
96
|
-
const lines = store.entries.map((e) => `- ${e.id} — ${e.title}${e.url ? ` (${shortUrl(e.url)})` : ''} · ~${e.tokens} tokens`);
|
|
97
|
-
return [
|
|
98
|
-
'Attached sources (NOT included below — read them with the `source` tool):',
|
|
99
|
-
...lines,
|
|
100
|
-
].join('\n');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Score a chunk against the query by term overlap.
|
|
105
|
-
*
|
|
106
|
-
* Deliberately not a model call and not an embedding: retrieval that needs a model to decide
|
|
107
|
-
* what to retrieve costs a round trip before the real one, and this runs on every source read.
|
|
108
|
-
* Term overlap is crude and instant, and for "which paragraph of this page mentions X" crude
|
|
109
|
-
* is usually right.
|
|
110
|
-
*/
|
|
111
|
-
function scoreChunk(chunk, terms) {
|
|
112
|
-
if (!terms.length) return 0;
|
|
113
|
-
const hay = chunk.toLowerCase();
|
|
114
|
-
let score = 0;
|
|
115
|
-
for (const t of terms) {
|
|
116
|
-
if (!hay.includes(t)) continue;
|
|
117
|
-
// Rarer terms are worth more, approximated by length: 'authentication' discriminates,
|
|
118
|
-
// 'the' does not.
|
|
119
|
-
score += Math.min(3, t.length / 4);
|
|
120
|
-
}
|
|
121
|
-
return score;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const STOP = new Set(['the', 'and', 'for', 'this', 'that', 'with', 'from', 'what', 'when', 'where', 'which', 'have', 'has', 'was', 'are', 'you', 'your', 'can', 'about', 'into', 'does', 'did', 'how', 'why', 'all', 'any', 'its']);
|
|
125
|
-
|
|
126
|
-
export function queryTerms(query) {
|
|
127
|
-
return String(query || '')
|
|
128
|
-
.toLowerCase()
|
|
129
|
-
.split(/[^a-z0-9]+/)
|
|
130
|
-
.filter((t) => t.length > 2 && !STOP.has(t));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Read a source, optionally narrowed by a query.
|
|
135
|
-
*
|
|
136
|
-
* Returns `{ id, title, url, text, truncated, of }`. `truncated` is stated rather than
|
|
137
|
-
* implied: a model handed a silently-cut document answers confidently about the part it was
|
|
138
|
-
* not given, and there is no way for it — or the person reading the answer — to tell.
|
|
139
|
-
*/
|
|
140
|
-
export function readSource(store, { id, query = '', maxTokens = 2000 } = {}) {
|
|
141
|
-
const entry = store?.get?.(id);
|
|
142
|
-
if (!entry) {
|
|
143
|
-
const known = (store?.entries || []).map((e) => e.id).join(', ');
|
|
144
|
-
return { error: `No attached source '${id}'.${known ? ` Available: ${known}.` : ' Nothing is attached.'}` };
|
|
145
|
-
}
|
|
146
|
-
const budget = Math.max(200, Number(maxTokens) || 2000);
|
|
147
|
-
if (entry.tokens <= budget) {
|
|
148
|
-
return { id: entry.id, title: entry.title, url: entry.url, text: entry.text, truncated: false, of: entry.tokens };
|
|
149
|
-
}
|
|
150
|
-
const terms = queryTerms(query);
|
|
151
|
-
const chunks = entry.text.split(CHUNK_SPLIT).filter((c) => c.trim());
|
|
152
|
-
if (!terms.length) {
|
|
153
|
-
// No query: the head is the honest default — it is the only part we can justify choosing
|
|
154
|
-
// without being told what matters.
|
|
155
|
-
const text = entry.text.slice(0, budget * 4);
|
|
156
|
-
return { id: entry.id, title: entry.title, url: entry.url, text, truncated: true, of: entry.tokens,
|
|
157
|
-
note: `Showing the first ~${approxTokens(text)} of ${entry.tokens} tokens. Pass a query to get the relevant parts instead.` };
|
|
158
|
-
}
|
|
159
|
-
// Keep the ORIGINAL ORDER of whatever is selected. Ranking by score and presenting in score
|
|
160
|
-
// order rearranges a document, and a reordered document reads as a different argument.
|
|
161
|
-
const ranked = chunks
|
|
162
|
-
.map((c, i) => ({ c, i, s: scoreChunk(c, terms) }))
|
|
163
|
-
.filter((x) => x.s > 0)
|
|
164
|
-
.sort((a, b) => b.s - a.s);
|
|
165
|
-
const picked = [];
|
|
166
|
-
let used = 0;
|
|
167
|
-
for (const x of ranked) {
|
|
168
|
-
const t = approxTokens(x.c);
|
|
169
|
-
if (used + t > budget) continue;
|
|
170
|
-
picked.push(x);
|
|
171
|
-
used += t;
|
|
172
|
-
}
|
|
173
|
-
if (!picked.length) {
|
|
174
|
-
const text = entry.text.slice(0, budget * 4);
|
|
175
|
-
return { id: entry.id, title: entry.title, url: entry.url, text, truncated: true, of: entry.tokens,
|
|
176
|
-
note: `Nothing in this source matched '${query}'. Showing the beginning instead.` };
|
|
177
|
-
}
|
|
178
|
-
picked.sort((a, b) => a.i - b.i);
|
|
179
|
-
const gaps = picked.some((x, n) => n > 0 && x.i !== picked[n - 1].i + 1);
|
|
180
|
-
return {
|
|
181
|
-
id: entry.id,
|
|
182
|
-
title: entry.title,
|
|
183
|
-
url: entry.url,
|
|
184
|
-
text: picked.map((x) => x.c).join(gaps ? '\n\n[…]\n\n' : '\n\n'),
|
|
185
|
-
truncated: true,
|
|
186
|
-
of: entry.tokens,
|
|
187
|
-
note: `${picked.length} of ${chunks.length} sections, matched on '${query}'.`,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
8
|
+
`),truncated:!0,of:t.tokens,note:`${l.length} of ${a.length} sections, matched on '${o}'.`}}export{d as approxTokens,S as makeSourceStore,b as manifestText,m as queryTerms,y as readSource,k as shortUrl,p as sourceId};
|
package/sources.js
CHANGED
|
@@ -1,273 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* WHERE THE CONTENT CAME FROM DECIDES HOW FAR IT MAY TRAVEL.
|
|
3
|
-
*
|
|
4
|
-
* A model was summarising an internal access-management page and the turn went to a public
|
|
5
|
-
* inference host, because routing asked what the WORK needed — tools, vision, quality — and
|
|
6
|
-
* never asked where the material came from. Capability decided; provenance did not exist.
|
|
7
|
-
*
|
|
8
|
-
* This module supplies the missing question. It classifies a source URL as internal or not,
|
|
9
|
-
* and turns that into a REACH CEILING the router already knows how to enforce. Two properties
|
|
10
|
-
* make it a guard rather than a preference:
|
|
11
|
-
*
|
|
12
|
-
* - It only ever NARROWS. `meetReach` takes the tighter of two ceilings, so no later step,
|
|
13
|
-
* plugin or user dial can widen what an internal source already restricted. A guard that
|
|
14
|
-
* something downstream can relax is a suggestion.
|
|
15
|
-
* - It fails CLOSED. An unparseable URL is treated as internal, because the alternative is
|
|
16
|
-
* to send data outward on the strength of a string we could not read.
|
|
17
|
-
*
|
|
18
|
-
* Pure and dependency-free: the same rules must hold in the extension, the gateway and the
|
|
19
|
-
* bridge, or "internal" means three different things and the strictest one is not the one
|
|
20
|
-
* that runs.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const REACH_ORDER = ['device', 'trusted', 'any'];
|
|
24
|
-
|
|
25
|
-
/** The tighter of two reach ceilings. Guards compose by MEET — they can only narrow. */
|
|
26
|
-
export function meetReach(a, b) {
|
|
27
|
-
const ia = REACH_ORDER.indexOf(a);
|
|
28
|
-
const ib = REACH_ORDER.indexOf(b);
|
|
29
|
-
// An unknown value is not a licence to travel further: treat it as the tightest.
|
|
30
|
-
if (ia < 0) return REACH_ORDER.includes(b) ? b : 'device';
|
|
31
|
-
if (ib < 0) return a;
|
|
32
|
-
return REACH_ORDER[Math.min(ia, ib)];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Hosts that are internal by NETWORK TOPOLOGY — the STARTING list, not a floor.
|
|
37
|
-
*
|
|
38
|
-
* Deliberately limited to what the address itself proves. A corporate wiki on a public SaaS
|
|
39
|
-
* domain looks exactly like any other public host from here — that case needs the user's own
|
|
40
|
-
* patterns, and pretending to detect it would give a false sense of coverage.
|
|
41
|
-
*
|
|
42
|
-
* These are seeded into the user's editable list rather than silently prepended to it,
|
|
43
|
-
* because "internal" is a fact about someone's network, not about ours: a developer testing
|
|
44
|
-
* against localhost may well want that traffic to reach a cloud model, and a rule they cannot
|
|
45
|
-
* see is a rule they cannot correct.
|
|
46
|
-
*/
|
|
47
|
-
export const INTERNAL_PATTERN_CATALOG = Object.freeze([
|
|
48
|
-
{ pattern: 'localhost', label: 'This machine, by name' },
|
|
49
|
-
{ pattern: '127.0.0.0/8', label: 'Loopback' },
|
|
50
|
-
{ pattern: '::1', label: 'Loopback (IPv6)' },
|
|
51
|
-
{ pattern: '0.0.0.0/8', label: 'This network' },
|
|
52
|
-
{ pattern: '10.0.0.0/8', label: 'Private network' },
|
|
53
|
-
{ pattern: '172.16.0.0/12', label: 'Private network' },
|
|
54
|
-
{ pattern: '192.168.0.0/16', label: 'Private network (home / office)' },
|
|
55
|
-
{ pattern: '100.64.0.0/10', label: 'Carrier-grade NAT — used by some corporate networks' },
|
|
56
|
-
{ pattern: '169.254.0.0/16', label: 'Link-local — never routes off this segment' },
|
|
57
|
-
{ pattern: 'fe80::/10', label: 'Link-local (IPv6)' },
|
|
58
|
-
{ pattern: 'fc00::/7', label: 'Unique local (IPv6) — the 10.x of IPv6' },
|
|
59
|
-
{ pattern: '*.internal', label: 'Reserved name' },
|
|
60
|
-
{ pattern: '*.intranet', label: 'Reserved name' },
|
|
61
|
-
{ pattern: '*.corp', label: 'Conventional corporate suffix' },
|
|
62
|
-
{ pattern: '*.lan', label: 'Conventional LAN suffix' },
|
|
63
|
-
{ pattern: '*.local', label: 'mDNS / Bonjour' },
|
|
64
|
-
{ pattern: '*.localdomain', label: 'Default suffix on many routers' },
|
|
65
|
-
{ pattern: '*.home', label: 'Home network' },
|
|
66
|
-
{ pattern: '*.home.arpa', label: 'Home network (RFC 8375)' },
|
|
67
|
-
{ pattern: '*.private', label: 'Conventional private suffix' },
|
|
68
|
-
{ pattern: '*.test', label: 'Reserved for testing (RFC 2606)' },
|
|
69
|
-
{ pattern: '*.invalid', label: 'Reserved as never-resolvable' },
|
|
70
|
-
{ pattern: '<intranet>', label: 'Any bare hostname with no dots, e.g. http://wiki/ — includes localhost' },
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The patterns on by default. Derived from the catalog so the list a UI offers and the list
|
|
75
|
-
* the classifier applies cannot drift — two copies of this would mean a rule someone can see
|
|
76
|
-
* and not switch off, or switch off and not escape.
|
|
77
|
-
*/
|
|
78
|
-
export const DEFAULT_INTERNAL_PATTERNS = Object.freeze(INTERNAL_PATTERN_CATALOG.map((x) => x.pattern));
|
|
79
|
-
const IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
80
|
-
|
|
81
|
-
function ipToInt(host) {
|
|
82
|
-
const m = IPV4.exec(host);
|
|
83
|
-
if (!m) return null;
|
|
84
|
-
const parts = m.slice(1).map(Number);
|
|
85
|
-
if (parts.some((n) => n > 255)) return null;
|
|
86
|
-
return parts.reduce((acc, n) => acc * 256 + n, 0);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* An IPv6 address as its 128 bits, or null if it is not one. Bits rather than a normalised
|
|
91
|
-
* string because prefix matching (fc00::/7, fe80::/10) is a bit-length comparison — a
|
|
92
|
-
* textual "starts with" would get /7 wrong, since the boundary falls inside a hex digit.
|
|
93
|
-
*/
|
|
94
|
-
function v6Bits(host) {
|
|
95
|
-
const h = String(host || '').replace(/^\[|\]$/g, '').toLowerCase();
|
|
96
|
-
if (!h.includes(':')) return null;
|
|
97
|
-
const [headRaw, tailRaw, ...rest] = h.split('::');
|
|
98
|
-
if (rest.length) return null; // more than one '::' is not a valid address
|
|
99
|
-
const parse = (part) => (part ? part.split(':').filter((x) => x !== '') : []);
|
|
100
|
-
let head = parse(headRaw);
|
|
101
|
-
let tail = tailRaw === undefined ? [] : parse(tailRaw);
|
|
102
|
-
// A trailing IPv4 form (::ffff:10.0.0.1) — the last group is four octets, not two.
|
|
103
|
-
const last = (tail.length ? tail : head)[Math.max(0, (tail.length ? tail : head).length - 1)];
|
|
104
|
-
if (last && last.includes('.')) {
|
|
105
|
-
const v4 = ipToInt(last);
|
|
106
|
-
if (v4 == null) return null;
|
|
107
|
-
const pair = [(v4 >>> 16).toString(16), (v4 & 0xffff).toString(16)];
|
|
108
|
-
if (tail.length) tail = [...tail.slice(0, -1), ...pair];
|
|
109
|
-
else head = [...head.slice(0, -1), ...pair];
|
|
110
|
-
}
|
|
111
|
-
const missing = 8 - head.length - tail.length;
|
|
112
|
-
if (tailRaw === undefined ? missing !== 0 : missing < 0) return null;
|
|
113
|
-
const groups = [...head, ...Array(Math.max(0, missing)).fill('0'), ...tail];
|
|
114
|
-
if (groups.length !== 8) return null;
|
|
115
|
-
let bits = '';
|
|
116
|
-
for (const g of groups) {
|
|
117
|
-
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
118
|
-
bits += parseInt(g, 16).toString(2).padStart(16, '0');
|
|
119
|
-
}
|
|
120
|
-
return bits;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function inCidr(host, cidr) {
|
|
124
|
-
const [net, bitsRaw] = cidr.split('/');
|
|
125
|
-
const hostV6 = v6Bits(host);
|
|
126
|
-
const netV6 = v6Bits(net);
|
|
127
|
-
if (hostV6 || netV6) {
|
|
128
|
-
// A v4 host never sits inside a v6 range, and vice versa — comparing them would be a
|
|
129
|
-
// type confusion that quietly matches or quietly does not.
|
|
130
|
-
if (!hostV6 || !netV6) return false;
|
|
131
|
-
const n = Number(bitsRaw);
|
|
132
|
-
if (!Number.isFinite(n) || n < 0 || n > 128) return false;
|
|
133
|
-
return hostV6.slice(0, n) === netV6.slice(0, n);
|
|
134
|
-
}
|
|
135
|
-
const ip = ipToInt(host);
|
|
136
|
-
const base = ipToInt(net);
|
|
137
|
-
if (ip == null || base == null) return false;
|
|
138
|
-
const bits = Number(bitsRaw);
|
|
139
|
-
if (!Number.isFinite(bits) || bits < 0 || bits > 32) return false;
|
|
140
|
-
// Shifting by 32 is a no-op in JS, so /0 is spelled out rather than computed.
|
|
141
|
-
const mask = bits === 0 ? 0 : (-1 << (32 - bits)) >>> 0;
|
|
142
|
-
return (ip & mask) >>> 0 === (base & mask) >>> 0;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Does `host` match one pattern? Supported forms, in the order a person would expect:
|
|
147
|
-
* `*.example.com` the domain and every subdomain
|
|
148
|
-
* `example.com` the same — a bare domain covers its subdomains, because someone adding
|
|
149
|
-
* their company domain means the whole company, not one host
|
|
150
|
-
* `10.0.0.0/8` a CIDR range
|
|
151
|
-
* `<intranet>` any single-label host, which can only resolve on a private network
|
|
152
|
-
*/
|
|
153
|
-
export function hostMatches(host, pattern) {
|
|
154
|
-
const h = String(host || '').toLowerCase().replace(/\.$/, '');
|
|
155
|
-
const p = String(pattern || '').toLowerCase().trim();
|
|
156
|
-
if (!h || !p) return false;
|
|
157
|
-
// A NAME with no dots — not an address. A public IPv6 literal has no dots either, and
|
|
158
|
-
// sweeping it in here would have quietly pinned every v6 host on the internet as internal.
|
|
159
|
-
if (p === '<intranet>') return !h.includes('.') && !h.includes(':') && !h.startsWith('[') && !IPV4.test(h);
|
|
160
|
-
if (p.includes('/')) return inCidr(h, p);
|
|
161
|
-
// '::1' and '[::1]' and '0:0:0:0:0:0:0:1' are one address written three ways.
|
|
162
|
-
const pv6 = v6Bits(p);
|
|
163
|
-
if (pv6) return v6Bits(h) === pv6;
|
|
164
|
-
const bare = p.startsWith('*.') ? p.slice(2) : p;
|
|
165
|
-
if (h === bare) return true;
|
|
166
|
-
if (h.endsWith(`.${bare}`)) return true;
|
|
167
|
-
// A leading wildcard anywhere else ("*.corp.*") is not supported rather than
|
|
168
|
-
// half-supported: a pattern that silently matches nothing is worse than one that is
|
|
169
|
-
// rejected, because it looks like protection.
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Classify one source. Returns `{ internal, host, matched }`.
|
|
175
|
-
*
|
|
176
|
-
* FAILS CLOSED. A URL we cannot parse counts as internal: we would rather keep a public page
|
|
177
|
-
* on-device than send an internal one out because a string was malformed. The same applies to
|
|
178
|
-
* non-http schemes — a `file:` path is local by definition.
|
|
179
|
-
*/
|
|
180
|
-
export function classifySource(url, { patterns = DEFAULT_INTERNAL_PATTERNS } = {}) {
|
|
181
|
-
const raw = String(url || '').trim();
|
|
182
|
-
if (!raw) return { internal: false, host: '', matched: null };
|
|
183
|
-
let host = '';
|
|
184
|
-
let scheme = '';
|
|
185
|
-
try {
|
|
186
|
-
const u = new URL(raw);
|
|
187
|
-
host = u.hostname;
|
|
188
|
-
scheme = u.protocol.replace(':', '');
|
|
189
|
-
} catch {
|
|
190
|
-
return { internal: true, host: '', matched: 'unparseable' };
|
|
191
|
-
}
|
|
192
|
-
if (scheme === 'file') return { internal: true, host, matched: 'file:' };
|
|
193
|
-
// Extension and browser-internal pages carry no third-party content, and are not sources
|
|
194
|
-
// anyone means to protect — treating them as internal would pin every turn to a local
|
|
195
|
-
// model for no reason.
|
|
196
|
-
if (/^(chrome|edge|about|moz|chrome-extension|data|blob)$/.test(scheme)) {
|
|
197
|
-
return { internal: false, host, matched: null };
|
|
198
|
-
}
|
|
199
|
-
for (const p of patterns) {
|
|
200
|
-
if (hostMatches(host, p)) return { internal: true, host, matched: p };
|
|
201
|
-
}
|
|
202
|
-
return { internal: false, host, matched: null };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Every url written in a piece of text.
|
|
207
|
-
*
|
|
208
|
-
* Declared sources are not the only way internal material enters a turn: someone pastes a
|
|
209
|
-
* link to an internal runbook, or a tool result comes back carrying one. The address is the
|
|
210
|
-
* evidence, and it is evidence wherever it appears — so the same classifier that reads the
|
|
211
|
-
* tab reads the body too.
|
|
212
|
-
*
|
|
213
|
-
* Explicit schemes only. A bare host like `wiki/page` is indistinguishable from an ordinary
|
|
214
|
-
* path, and matching it would pin turns on text that mentions no site at all — a guard that
|
|
215
|
-
* fires on prose gets switched off, which protects nobody.
|
|
216
|
-
*/
|
|
217
|
-
export function extractUrls(text) {
|
|
218
|
-
const out = [];
|
|
219
|
-
const re = /\b(?:https?|file):\/\/[^\s<>"'`)\]}]+/gi;
|
|
220
|
-
for (const m of String(text || '').matchAll(re)) {
|
|
221
|
-
// Trailing punctuation belongs to the sentence, not to the address.
|
|
222
|
-
out.push(m[0].replace(/[.,;:!?]+$/, ''));
|
|
223
|
-
}
|
|
224
|
-
return out;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Build the reach policy for a turn from everything it draws on.
|
|
229
|
-
*
|
|
230
|
-
* ANY internal source pins the WHOLE turn. A turn that mixes an internal page with a public
|
|
231
|
-
* one is still carrying the internal page, and splitting the difference would send it out.
|
|
232
|
-
*
|
|
233
|
-
* `ceiling` is what an internal source narrows to — 'device' for local models only, or
|
|
234
|
-
* 'trusted' to also allow a workspace gateway the user runs. It cannot widen anything: the
|
|
235
|
-
* result is always the tighter of the ceiling and what was already required.
|
|
236
|
-
*/
|
|
237
|
-
export function sourcePolicyFor(sources = [], { patterns, ceiling = 'device', base = 'any' } = {}) {
|
|
238
|
-
const list = (Array.isArray(sources) ? sources : [sources]).filter(Boolean);
|
|
239
|
-
const hits = [];
|
|
240
|
-
for (const s of list) {
|
|
241
|
-
const url = typeof s === 'string' ? s : (s?.url || s?.href || '');
|
|
242
|
-
if (!url) continue;
|
|
243
|
-
const c = classifySource(url, patterns ? { patterns } : undefined);
|
|
244
|
-
if (c.internal) hits.push({ url, host: c.host, matched: c.matched });
|
|
245
|
-
}
|
|
246
|
-
if (!hits.length) return { internal: false, reach: base, hits: [], why: null };
|
|
247
|
-
const safeCeiling = REACH_ORDER.includes(ceiling) ? ceiling : 'device';
|
|
248
|
-
return {
|
|
249
|
-
internal: true,
|
|
250
|
-
reach: meetReach(base, safeCeiling),
|
|
251
|
-
hits,
|
|
252
|
-
// Named so the person can see WHICH rule pinned the turn — an unexplained restriction
|
|
253
|
-
// gets switched off wholesale.
|
|
254
|
-
why: `${hits[0].host || 'the source'} matches '${hits[0].matched}' — kept ${safeCeiling === 'device' ? 'on this device' : 'inside your workspace'}`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Every address a conversation carries: each attachment's `url`, every URL in a message
|
|
260
|
-
* body (an internal link pasted into a message — or arriving in a tool result written back
|
|
261
|
-
* into the conversation — is internal material just as much as an attachment is), plus
|
|
262
|
-
* anything the caller states outright. The WHOLE conversation, because an internal page
|
|
263
|
-
* attached three turns ago is still in the text being sent now.
|
|
264
|
-
*/
|
|
265
|
-
export function sourceUrlsOf(messages, extraSources = []) {
|
|
266
|
-
const urls = [];
|
|
267
|
-
for (const m of messages || []) {
|
|
268
|
-
for (const a of m?.attachments || []) if (a?.url) urls.push(a.url);
|
|
269
|
-
for (const u of extractUrls(typeof m?.content === 'string' ? m.content : '')) urls.push(u);
|
|
270
|
-
}
|
|
271
|
-
for (const s of extraSources || []) if (s) urls.push(typeof s === 'string' ? s : (s.url || s.href || ''));
|
|
272
|
-
return urls.filter(Boolean);
|
|
273
|
-
}
|
|
1
|
+
const h=["device","trusted","any"];function v(n,r){const t=h.indexOf(n),e=h.indexOf(r);return t<0?h.includes(r)?r:"device":e<0?n:h[Math.min(t,e)]}const x=Object.freeze([{pattern:"localhost",label:"This machine, by name"},{pattern:"127.0.0.0/8",label:"Loopback"},{pattern:"::1",label:"Loopback (IPv6)"},{pattern:"0.0.0.0/8",label:"This network"},{pattern:"10.0.0.0/8",label:"Private network"},{pattern:"172.16.0.0/12",label:"Private network"},{pattern:"192.168.0.0/16",label:"Private network (home / office)"},{pattern:"100.64.0.0/10",label:"Carrier-grade NAT — used by some corporate networks"},{pattern:"169.254.0.0/16",label:"Link-local — never routes off this segment"},{pattern:"fe80::/10",label:"Link-local (IPv6)"},{pattern:"fc00::/7",label:"Unique local (IPv6) — the 10.x of IPv6"},{pattern:"*.internal",label:"Reserved name"},{pattern:"*.intranet",label:"Reserved name"},{pattern:"*.corp",label:"Conventional corporate suffix"},{pattern:"*.lan",label:"Conventional LAN suffix"},{pattern:"*.local",label:"mDNS / Bonjour"},{pattern:"*.localdomain",label:"Default suffix on many routers"},{pattern:"*.home",label:"Home network"},{pattern:"*.home.arpa",label:"Home network (RFC 8375)"},{pattern:"*.private",label:"Conventional private suffix"},{pattern:"*.test",label:"Reserved for testing (RFC 2606)"},{pattern:"*.invalid",label:"Reserved as never-resolvable"},{pattern:"<intranet>",label:"Any bare hostname with no dots, e.g. http://wiki/ — includes localhost"}]),w=Object.freeze(x.map(n=>n.pattern)),g=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;function m(n){const r=g.exec(n);if(!r)return null;const t=r.slice(1).map(Number);return t.some(e=>e>255)?null:t.reduce((e,l)=>e*256+l,0)}function d(n){const r=String(n||"").replace(/^\[|\]$/g,"").toLowerCase();if(!r.includes(":"))return null;const[t,e,...l]=r.split("::");if(l.length)return null;const o=u=>u?u.split(":").filter(p=>p!==""):[];let s=o(t),a=e===void 0?[]:o(e);const i=(a.length?a:s)[Math.max(0,(a.length?a:s).length-1)];if(i&&i.includes(".")){const u=m(i);if(u==null)return null;const p=[(u>>>16).toString(16),(u&65535).toString(16)];a.length?a=[...a.slice(0,-1),...p]:s=[...s.slice(0,-1),...p]}const c=8-s.length-a.length;if(e===void 0?c!==0:c<0)return null;const f=[...s,...Array(Math.max(0,c)).fill("0"),...a];if(f.length!==8)return null;let b="";for(const u of f){if(!/^[0-9a-f]{1,4}$/.test(u))return null;b+=parseInt(u,16).toString(2).padStart(16,"0")}return b}function y(n,r){const[t,e]=r.split("/"),l=d(n),o=d(t);if(l||o){if(!l||!o)return!1;const f=Number(e);return!Number.isFinite(f)||f<0||f>128?!1:l.slice(0,f)===o.slice(0,f)}const s=m(n),a=m(t);if(s==null||a==null)return!1;const i=Number(e);if(!Number.isFinite(i)||i<0||i>32)return!1;const c=i===0?0:-1<<32-i>>>0;return(s&c)>>>0===(a&c)>>>0}function R(n,r){const t=String(n||"").toLowerCase().replace(/\.$/,""),e=String(r||"").toLowerCase().trim();if(!t||!e)return!1;if(e==="<intranet>")return!t.includes(".")&&!t.includes(":")&&!t.startsWith("[")&&!g.test(t);if(e.includes("/"))return y(t,e);const l=d(e);if(l)return d(t)===l;const o=e.startsWith("*.")?e.slice(2):e;return!!(t===o||t.endsWith(`.${o}`))}function k(n,{patterns:r=w}={}){const t=String(n||"").trim();if(!t)return{internal:!1,host:"",matched:null};let e="",l="";try{const o=new URL(t);e=o.hostname,l=o.protocol.replace(":","")}catch{return{internal:!0,host:"",matched:"unparseable"}}if(l==="file")return{internal:!0,host:e,matched:"file:"};if(/^(chrome|edge|about|moz|chrome-extension|data|blob)$/.test(l))return{internal:!1,host:e,matched:null};for(const o of r)if(R(e,o))return{internal:!0,host:e,matched:o};return{internal:!1,host:e,matched:null}}function A(n){const r=[],t=/\b(?:https?|file):\/\/[^\s<>"'`)\]}]+/gi;for(const e of String(n||"").matchAll(t))r.push(e[0].replace(/[.,;:!?]+$/,""));return r}function N(n=[],{patterns:r,ceiling:t="device",base:e="any"}={}){const l=(Array.isArray(n)?n:[n]).filter(Boolean),o=[];for(const a of l){const i=typeof a=="string"?a:a?.url||a?.href||"";if(!i)continue;const c=k(i,r?{patterns:r}:void 0);c.internal&&o.push({url:i,host:c.host,matched:c.matched})}if(!o.length)return{internal:!1,reach:e,hits:[],why:null};const s=h.includes(t)?t:"device";return{internal:!0,reach:v(e,s),hits:o,why:`${o[0].host||"the source"} matches '${o[0].matched}' — kept ${s==="device"?"on this device":"inside your workspace"}`}}function C(n,r=[]){const t=[];for(const e of n||[]){for(const l of e?.attachments||[])l?.url&&t.push(l.url);for(const l of A(typeof e?.content=="string"?e.content:""))t.push(l)}for(const e of r||[])e&&t.push(typeof e=="string"?e:e.url||e.href||"");return t.filter(Boolean)}export{w as DEFAULT_INTERNAL_PATTERNS,x as INTERNAL_PATTERN_CATALOG,k as classifySource,A as extractUrls,R as hostMatches,v as meetReach,N as sourcePolicyFor,C as sourceUrlsOf};
|