@namingsignal/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/agent-guidance.js +7 -0
- package/dist/core/batch.js +142 -0
- package/dist/core/brief-constraints.js +72 -0
- package/dist/core/brief.js +402 -0
- package/dist/core/candidate-sort.js +47 -0
- package/dist/core/candidates.js +210 -0
- package/dist/core/domain.js +607 -0
- package/dist/core/editorial-review.js +27 -0
- package/dist/core/export.js +235 -0
- package/dist/core/import-safety.js +298 -0
- package/dist/core/index.js +31 -0
- package/dist/core/namesake.js +53 -0
- package/dist/core/namespaces.js +295 -0
- package/dist/core/normalize.js +75 -0
- package/dist/core/scoring.js +409 -0
- package/dist/core/types.js +3 -0
- package/dist/core/url-input.js +28 -0
- package/dist/core/usage.js +271 -0
- package/dist/index.js +19 -0
- package/dist/server-core.js +418 -0
- package/package.json +48 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportDecisionJson = exportDecisionJson;
|
|
4
|
+
exports.exportDecisionMarkdown = exportDecisionMarkdown;
|
|
5
|
+
const LEGAL_NOTICE = "This decision record is business research, not legal advice or trademark clearance. Names are primarily a trademark and likelihood-of-confusion issue; a qualified professional should review serious launch candidates.";
|
|
6
|
+
function stableValue(value) {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return value.map(stableValue);
|
|
9
|
+
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
10
|
+
return Object.fromEntries(Object.entries(value)
|
|
11
|
+
.filter(([, item]) => item !== undefined)
|
|
12
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
13
|
+
.map(([key, item]) => [key, stableValue(item)]));
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function exportEnvelope(record, options) {
|
|
18
|
+
const createdAt = record.createdAt ?? options.now?.().toISOString() ?? new Date().toISOString();
|
|
19
|
+
const envelope = {
|
|
20
|
+
...record,
|
|
21
|
+
};
|
|
22
|
+
envelope.schema = "naming-signal.decision/v1";
|
|
23
|
+
envelope.version = record.version ?? "1.0";
|
|
24
|
+
envelope.createdAt = createdAt;
|
|
25
|
+
envelope.legalNotice = LEGAL_NOTICE;
|
|
26
|
+
return envelope;
|
|
27
|
+
}
|
|
28
|
+
function exportDecisionJson(record, options = {}) {
|
|
29
|
+
if (!record || typeof record !== "object")
|
|
30
|
+
throw new TypeError("A decision record is required.");
|
|
31
|
+
return `${JSON.stringify(stableValue(exportEnvelope(record, options)), null, options.pretty === false ? 0 : 2)}\n`;
|
|
32
|
+
}
|
|
33
|
+
function text(value) {
|
|
34
|
+
return String(value ?? "").replace(/\r?\n/g, " ").trim();
|
|
35
|
+
}
|
|
36
|
+
function cell(value) {
|
|
37
|
+
return text(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
38
|
+
}
|
|
39
|
+
function heading(value) {
|
|
40
|
+
return text(value).replace(/[#*_`[\]]/g, "").trim();
|
|
41
|
+
}
|
|
42
|
+
function safeLink(url, label) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
46
|
+
return cell(label);
|
|
47
|
+
return `[${cell(label)}](${parsed.toString().replace(/\)/g, "%29")})`;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return cell(label);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function scoreOf(item) {
|
|
54
|
+
return item.score?.overall;
|
|
55
|
+
}
|
|
56
|
+
function domainsOf(item) {
|
|
57
|
+
return item.domains ?? [];
|
|
58
|
+
}
|
|
59
|
+
function namespacesOf(item) {
|
|
60
|
+
return "namespaces" in item ? item.namespaces ?? [] : [];
|
|
61
|
+
}
|
|
62
|
+
function evidenceOf(item) {
|
|
63
|
+
if ("evidence" in item && item.evidence)
|
|
64
|
+
return item.evidence;
|
|
65
|
+
return [...domainsOf(item).map((domain) => domain.evidence), ...namespacesOf(item).map((namespace) => namespace.evidence)];
|
|
66
|
+
}
|
|
67
|
+
function bestDomain(domains) {
|
|
68
|
+
if (!domains.length)
|
|
69
|
+
return "Not checked";
|
|
70
|
+
const priority = ["likely_available", "premium", "parked", "registered", "active", "unknown", "unsupported"];
|
|
71
|
+
const sorted = [...domains].sort((left, right) => priority.indexOf(left.status) - priority.indexOf(right.status));
|
|
72
|
+
return `${sorted[0].domain} — ${sorted[0].status.replace(/_/g, " ")}`;
|
|
73
|
+
}
|
|
74
|
+
function domainSignals(domains) {
|
|
75
|
+
if (!domains.length)
|
|
76
|
+
return "Not checked";
|
|
77
|
+
return domains.map((item) => `${item.domain} — ${item.status.replace(/_/g, " ")}`).join("; ");
|
|
78
|
+
}
|
|
79
|
+
function namespaceSignals(namespaces) {
|
|
80
|
+
if (!namespaces.length)
|
|
81
|
+
return "Not checked";
|
|
82
|
+
return namespaces.map((item) => `${item.label || item.namespace} — ${item.status.replace(/_/g, " ")}`).join("; ");
|
|
83
|
+
}
|
|
84
|
+
function table(lines, headers, rows) {
|
|
85
|
+
lines.push(`| ${headers.join(" | ")} |`);
|
|
86
|
+
lines.push(`| ${headers.map(() => "---").join(" | ")} |`);
|
|
87
|
+
for (const row of rows)
|
|
88
|
+
lines.push(`| ${row.map(cell).join(" | ")} |`);
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
function writeEvidence(lines, evidence) {
|
|
92
|
+
if (!evidence.length) {
|
|
93
|
+
lines.push("No source-backed evidence was recorded.", "");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
table(lines, ["Finding", "State", "Confidence", "Source", "Checked"], evidence.map((item) => [
|
|
97
|
+
item.label || item.detail || item.provider,
|
|
98
|
+
item.status,
|
|
99
|
+
item.confidence,
|
|
100
|
+
safeLink(item.sourceUrl, item.provider),
|
|
101
|
+
item.checkedAt,
|
|
102
|
+
]));
|
|
103
|
+
}
|
|
104
|
+
/** Produce a portable decision record with evidence and unresolved actions kept visible. */
|
|
105
|
+
function exportDecisionMarkdown(record, options = {}) {
|
|
106
|
+
if (!record || typeof record !== "object")
|
|
107
|
+
throw new TypeError("A decision record is required.");
|
|
108
|
+
const generatedAt = record.createdAt ?? options.now?.().toISOString() ?? new Date().toISOString();
|
|
109
|
+
const lines = [];
|
|
110
|
+
const title = options.title ?? (record.project ? `${record.project} naming decision` : "Naming decision record");
|
|
111
|
+
lines.push(`# ${heading(title)}`, "", `Generated: ${generatedAt}`, "", `> ${LEGAL_NOTICE}`, "");
|
|
112
|
+
if (record.selectedName || record.recommendation) {
|
|
113
|
+
lines.push("## Decision", "");
|
|
114
|
+
if (record.selectedName)
|
|
115
|
+
lines.push(`**Selected name:** ${text(record.selectedName)}`, "");
|
|
116
|
+
if (record.recommendation)
|
|
117
|
+
lines.push(text(record.recommendation), "");
|
|
118
|
+
}
|
|
119
|
+
if (record.brief) {
|
|
120
|
+
lines.push("## Naming brief", "");
|
|
121
|
+
table(lines, ["Field", "Current understanding"], [
|
|
122
|
+
["Product", record.brief.product || "Unknown"],
|
|
123
|
+
["Primary user", record.brief.primaryUser || "Unknown"],
|
|
124
|
+
["Pain", record.brief.pain || "Unknown"],
|
|
125
|
+
["Outcome", record.brief.outcome || "Unknown"],
|
|
126
|
+
["Mechanism", record.brief.mechanism || "Unknown"],
|
|
127
|
+
["Platforms", record.brief.platform.join(", ") || "Unknown"],
|
|
128
|
+
["Acquisition", record.brief.acquisitionChannels.join(", ") || "Unknown"],
|
|
129
|
+
["Tone", record.brief.brandTone.join(", ") || "Unknown"],
|
|
130
|
+
["Future scope", record.brief.futureExpansion || "Unknown"],
|
|
131
|
+
["Domain requirements", record.brief.domainRequirements.join(", ") || "Unknown"],
|
|
132
|
+
["Avoid", record.brief.avoid.join(", ") || "None supplied"],
|
|
133
|
+
]);
|
|
134
|
+
if (record.brief.uncertainties.length) {
|
|
135
|
+
lines.push("### Brief uncertainties", "", ...record.brief.uncertainties.map((item) => `- ${text(item)}`), "");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const shortlist = record.shortlist ?? [];
|
|
139
|
+
if (shortlist.length) {
|
|
140
|
+
lines.push("## Shortlist", "");
|
|
141
|
+
table(lines, ["Name", "Direction", "Score", "Best domain signal", "Rationale"], shortlist.map((item) => [
|
|
142
|
+
item.name,
|
|
143
|
+
"vein" in item ? item.vein : "finalist",
|
|
144
|
+
scoreOf(item) === undefined ? "Unscored" : `${scoreOf(item)}/100`,
|
|
145
|
+
bestDomain(domainsOf(item)),
|
|
146
|
+
"rationale" in item ? item.rationale ?? "" : item.strongestReasonFor ?? item.rationale ?? "",
|
|
147
|
+
]));
|
|
148
|
+
}
|
|
149
|
+
if (record.candidates?.length) {
|
|
150
|
+
lines.push("## Checked candidate ledger", "");
|
|
151
|
+
table(lines, ["Name", "Direction", "Score", "All domain checks", "All namespace checks", "Rationale"], record.candidates.map((item) => [
|
|
152
|
+
item.name,
|
|
153
|
+
item.vein,
|
|
154
|
+
scoreOf(item) === undefined ? "Unscored" : `${scoreOf(item)}/100`,
|
|
155
|
+
domainSignals(domainsOf(item)),
|
|
156
|
+
namespaceSignals(namespacesOf(item)),
|
|
157
|
+
item.rationale,
|
|
158
|
+
]));
|
|
159
|
+
}
|
|
160
|
+
if (record.finalists?.length) {
|
|
161
|
+
lines.push("## Finalist diligence", "");
|
|
162
|
+
for (const finalist of record.finalists) {
|
|
163
|
+
lines.push(`### ${heading(finalist.name)}`, "", `**Preliminary score:** ${finalist.score.overall}/100 — ${text(finalist.score.label)}`, "");
|
|
164
|
+
if (finalist.founderResonance !== undefined) {
|
|
165
|
+
lines.push(`**Founder resonance:** ${finalist.founderResonance}/10`, "");
|
|
166
|
+
}
|
|
167
|
+
if (finalist.strongestReasonFor)
|
|
168
|
+
lines.push(`**Strongest reason for:** ${text(finalist.strongestReasonFor)}`, "");
|
|
169
|
+
if (finalist.strongestReasonAgainst)
|
|
170
|
+
lines.push(`**Strongest reason against:** ${text(finalist.strongestReasonAgainst)}`, "");
|
|
171
|
+
if (finalist.score.hardBlockers.length) {
|
|
172
|
+
lines.push("**Hard blockers (the score does not override these):**", "", ...finalist.score.hardBlockers.map((item) => `- ${text(item)}`), "");
|
|
173
|
+
}
|
|
174
|
+
if (finalist.score.warnings.length) {
|
|
175
|
+
lines.push("**Warnings and unknowns:**", "", ...finalist.score.warnings.map((item) => `- ${text(item)}`), "");
|
|
176
|
+
}
|
|
177
|
+
table(lines, ["Dimension", "Score", "Weight", "Basis"], finalist.score.dimensions.map((item) => [item.label, `${item.score}/100`, item.weight, item.note]));
|
|
178
|
+
if (finalist.domains?.length) {
|
|
179
|
+
lines.push("#### Domains", "");
|
|
180
|
+
table(lines, ["Domain", "State", "Evidence state", "Source", "Checked"], finalist.domains.map((item) => [
|
|
181
|
+
item.domain,
|
|
182
|
+
item.status,
|
|
183
|
+
item.evidence.status,
|
|
184
|
+
safeLink(item.evidence.sourceUrl, item.provider),
|
|
185
|
+
item.checkedAt,
|
|
186
|
+
]));
|
|
187
|
+
}
|
|
188
|
+
if (finalist.namespaces?.length) {
|
|
189
|
+
lines.push("#### Namespaces", "");
|
|
190
|
+
table(lines, ["Namespace", "State", "Evidence state", "Source"], finalist.namespaces.map((item) => [
|
|
191
|
+
item.label,
|
|
192
|
+
item.status,
|
|
193
|
+
item.evidence.status,
|
|
194
|
+
safeLink(item.evidence.sourceUrl, item.evidence.provider),
|
|
195
|
+
]));
|
|
196
|
+
}
|
|
197
|
+
lines.push("#### Evidence ledger", "");
|
|
198
|
+
writeEvidence(lines, evidenceOf(finalist));
|
|
199
|
+
if (finalist.manualLinks?.length) {
|
|
200
|
+
lines.push("#### Manual verification", "", ...finalist.manualLinks.map((item) => `- ${safeLink(item.url, item.label)}${item.reason ? ` — ${text(item.reason)}` : ""}`), "");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (record.rejected?.length) {
|
|
205
|
+
lines.push("## Rejected-name ledger", "");
|
|
206
|
+
table(lines, ["Name", "Reason", "Rejected"], record.rejected.map((item) => [item.name, item.reason, item.rejectedAt ?? "Time not recorded"]));
|
|
207
|
+
}
|
|
208
|
+
const actions = record.humanActions ?? [];
|
|
209
|
+
lines.push("## Human actions before commitment", "");
|
|
210
|
+
const requiredActions = [
|
|
211
|
+
...actions,
|
|
212
|
+
"Recheck the chosen domain at a registrar immediately before purchase.",
|
|
213
|
+
"Run spoken dictation/caption tests when word of mouth, audio, or creator video matters.",
|
|
214
|
+
"Have appropriate counsel perform a comprehensive trademark clearance search for a serious launch.",
|
|
215
|
+
];
|
|
216
|
+
lines.push(...[...new Set(requiredActions)].map((item) => `- ${text(item)}`), "");
|
|
217
|
+
if (record.notes?.length)
|
|
218
|
+
lines.push("## Notes", "", ...record.notes.map((item) => `- ${text(item)}`), "");
|
|
219
|
+
if (record.usage) {
|
|
220
|
+
lines.push("## Request usage", "");
|
|
221
|
+
table(lines, ["Metric", "Value"], [
|
|
222
|
+
["Model calls", record.usage.modelCalls],
|
|
223
|
+
["Input tokens", record.usage.inputTokens],
|
|
224
|
+
["Cached input tokens", record.usage.cachedInputTokens],
|
|
225
|
+
["Reasoning tokens", record.usage.reasoningTokens],
|
|
226
|
+
["Output tokens", record.usage.outputTokens],
|
|
227
|
+
["Domain requests", record.usage.domainRequests],
|
|
228
|
+
["Namespace requests", record.usage.namespaceRequests],
|
|
229
|
+
["Search queries", record.usage.searchQueries],
|
|
230
|
+
["Cache hits", record.usage.cacheHits],
|
|
231
|
+
["Estimated USD", `$${record.usage.estimatedUsd.toFixed(6)}`],
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
235
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateImportText = validateImportText;
|
|
4
|
+
exports.validatePublicUrl = validatePublicUrl;
|
|
5
|
+
const DENIED_FILE_PATTERNS = [
|
|
6
|
+
[/(?:^|\/)\.env(?:\.|$)/i, "Environment files are excluded by default."],
|
|
7
|
+
[/(?:^|\/)(?:id_rsa|id_ed25519|credentials|secrets?)(?:\.|$)/i, "Credential and secret files are excluded."],
|
|
8
|
+
[/\.(?:pem|key|p12|pfx|jks|keystore)$/i, "Certificate and private-key containers are excluded."],
|
|
9
|
+
[/\.(?:zip|tar|gz|tgz|bz2|xz|7z|rar|dmg|iso)$/i, "Archives are not accepted as text imports."],
|
|
10
|
+
[/\.(?:png|jpe?g|gif|webp|heic|pdf|mp[34]|mov|avi|woff2?|ttf|ico)$/i, "Binary media is not accepted as a text import."],
|
|
11
|
+
[/(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|poetry\.lock|Cargo\.lock)$/i, "Lockfiles are excluded from naming context."],
|
|
12
|
+
[/\.(?:map|min\.js)$/i, "Generated files are excluded from naming context."],
|
|
13
|
+
];
|
|
14
|
+
const SECRET_PATTERNS = [
|
|
15
|
+
{ kind: "private_key", pattern: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g },
|
|
16
|
+
{ kind: "openai_style_key", pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}\b/g },
|
|
17
|
+
{ kind: "github_token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g },
|
|
18
|
+
{ kind: "aws_access_key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
19
|
+
{ kind: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
20
|
+
{
|
|
21
|
+
kind: "credential_assignment",
|
|
22
|
+
pattern: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|password|passwd|secret)\s*[:=]\s*["']?(?!\s|["']?(?:your|example|placeholder|changeme|todo|none|null)\b)[^\s,"']{8,}["']?/gi,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
kind: "credential_url",
|
|
26
|
+
pattern: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s/:]+:[^\s/@]{4,}@[^\s]+/gi,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const INJECTION_PATTERNS = [
|
|
30
|
+
{
|
|
31
|
+
kind: "instruction_override",
|
|
32
|
+
pattern: /\bignore (?:all |any )?(?:previous|prior|system|developer) instructions?\b/i,
|
|
33
|
+
message: "Instruction-override language is preserved only as quoted project data.",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
kind: "role_impersonation",
|
|
37
|
+
pattern: /\b(?:system|developer|assistant)\s*(?:message|prompt)?\s*:/i,
|
|
38
|
+
message: "Role-like text is not promoted to an instruction.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
kind: "secret_exfiltration",
|
|
42
|
+
pattern: /\b(?:reveal|print|return|upload|exfiltrate)\b.{0,40}\b(?:secret|token|credential|system prompt)\b/i,
|
|
43
|
+
message: "A possible data-exfiltration instruction was treated as inert text.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
kind: "tool_instruction",
|
|
47
|
+
pattern: /\b(?:call|invoke|run|execute)\b.{0,30}\b(?:tool|shell|command|curl|fetch)\b/i,
|
|
48
|
+
message: "Tool-execution language is treated as content and is never executed by the compiler.",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
function byteLength(value) {
|
|
52
|
+
return new TextEncoder().encode(value).byteLength;
|
|
53
|
+
}
|
|
54
|
+
function estimatedTokens(value) {
|
|
55
|
+
const words = value.trim() ? value.trim().split(/\s+/).length : 0;
|
|
56
|
+
return Math.ceil(Math.max(value.length / 4, words * 1.28));
|
|
57
|
+
}
|
|
58
|
+
function redactLine(line, findingKinds) {
|
|
59
|
+
let output = line;
|
|
60
|
+
for (const item of SECRET_PATTERNS) {
|
|
61
|
+
item.pattern.lastIndex = 0;
|
|
62
|
+
if (!findingKinds.has(item.kind))
|
|
63
|
+
continue;
|
|
64
|
+
output = output.replace(item.pattern, `[REDACTED:${item.kind}]`);
|
|
65
|
+
}
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
/** Validate arbitrary text before it can become provider input. */
|
|
69
|
+
function validateImportText(text, options = {}) {
|
|
70
|
+
const maxBytes = options.maxBytes ?? 250_000;
|
|
71
|
+
const maxTokens = options.maxTokens ?? 12_000;
|
|
72
|
+
const errors = [];
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const secretFindings = [];
|
|
75
|
+
const injectionSignals = [];
|
|
76
|
+
if (typeof text !== "string") {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
valid: false,
|
|
80
|
+
sanitizedText: "",
|
|
81
|
+
safeText: "",
|
|
82
|
+
byteLength: 0,
|
|
83
|
+
estimatedTokens: 0,
|
|
84
|
+
errors: ["Import must be text."],
|
|
85
|
+
warnings,
|
|
86
|
+
secretFindings,
|
|
87
|
+
injectionSignals,
|
|
88
|
+
source: options.source,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const normalized = text.normalize("NFKC").replace(/\r\n?/g, "\n");
|
|
92
|
+
const bytes = byteLength(normalized);
|
|
93
|
+
const tokens = estimatedTokens(normalized);
|
|
94
|
+
if (!normalized.trim())
|
|
95
|
+
errors.push("Import is empty.");
|
|
96
|
+
if (bytes > maxBytes)
|
|
97
|
+
errors.push(`Import is ${bytes} bytes; the limit is ${maxBytes} bytes.`);
|
|
98
|
+
if (tokens > maxTokens)
|
|
99
|
+
errors.push(`Import is approximately ${tokens} tokens; the model-input limit is ${maxTokens}.`);
|
|
100
|
+
if (/\0/.test(normalized) || (normalized.match(/\uFFFD/g)?.length ?? 0) > Math.max(2, normalized.length / 100)) {
|
|
101
|
+
errors.push("Import appears to contain binary or undecodable content.");
|
|
102
|
+
}
|
|
103
|
+
if (options.fileName) {
|
|
104
|
+
for (const [pattern, message] of DENIED_FILE_PATTERNS) {
|
|
105
|
+
if (pattern.test(options.fileName))
|
|
106
|
+
errors.push(message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const lines = normalized.split("\n");
|
|
110
|
+
const sanitizedLines = [];
|
|
111
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
112
|
+
const line = lines[index];
|
|
113
|
+
const kindsOnLine = new Set();
|
|
114
|
+
for (const secret of SECRET_PATTERNS) {
|
|
115
|
+
secret.pattern.lastIndex = 0;
|
|
116
|
+
if (secret.pattern.test(line)) {
|
|
117
|
+
kindsOnLine.add(secret.kind);
|
|
118
|
+
secretFindings.push({
|
|
119
|
+
kind: secret.kind,
|
|
120
|
+
line: index + 1,
|
|
121
|
+
message: `Possible ${secret.kind.replace(/_/g, " ")} excluded on line ${index + 1}; the value is not returned.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const injection of INJECTION_PATTERNS) {
|
|
126
|
+
if (injection.pattern.test(line)) {
|
|
127
|
+
injectionSignals.push({ kind: injection.kind, line: index + 1, message: injection.message });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
sanitizedLines.push(options.redactSecrets === false ? line : redactLine(line, kindsOnLine));
|
|
131
|
+
}
|
|
132
|
+
if (secretFindings.length) {
|
|
133
|
+
errors.push(`Import contains ${secretFindings.length} possible secret${secretFindings.length === 1 ? "" : "s"}; remove them before transmission.`);
|
|
134
|
+
}
|
|
135
|
+
if (injectionSignals.length) {
|
|
136
|
+
warnings.push(`${injectionSignals.length} instruction-like line${injectionSignals.length === 1 ? " was" : "s were"} detected and will be treated only as data.`);
|
|
137
|
+
}
|
|
138
|
+
if (!options.fileName)
|
|
139
|
+
warnings.push("No filename was supplied, so path-based secret exclusions could not be applied.");
|
|
140
|
+
const sanitizedText = sanitizedLines.join("\n");
|
|
141
|
+
return {
|
|
142
|
+
ok: errors.length === 0,
|
|
143
|
+
valid: errors.length === 0,
|
|
144
|
+
sanitizedText,
|
|
145
|
+
safeText: sanitizedText,
|
|
146
|
+
byteLength: bytes,
|
|
147
|
+
estimatedTokens: tokens,
|
|
148
|
+
errors: [...new Set(errors)],
|
|
149
|
+
warnings: [...new Set(warnings)],
|
|
150
|
+
secretFindings,
|
|
151
|
+
injectionSignals,
|
|
152
|
+
source: options.source ?? options.fileName,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function parseIpv4(hostname) {
|
|
156
|
+
const parts = hostname.split(".");
|
|
157
|
+
if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part)))
|
|
158
|
+
return undefined;
|
|
159
|
+
const numbers = parts.map(Number);
|
|
160
|
+
return numbers.every((part) => part >= 0 && part <= 255) ? numbers : undefined;
|
|
161
|
+
}
|
|
162
|
+
function isBlockedIpv4(parts) {
|
|
163
|
+
const [a, b] = parts;
|
|
164
|
+
return (a === 0 ||
|
|
165
|
+
a === 10 ||
|
|
166
|
+
a === 127 ||
|
|
167
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
168
|
+
(a === 169 && b === 254) ||
|
|
169
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
170
|
+
(a === 192 && b === 0) ||
|
|
171
|
+
(a === 192 && b === 168) ||
|
|
172
|
+
(a === 198 && (b === 18 || b === 19 || b === 51)) ||
|
|
173
|
+
(a === 203 && b === 0) ||
|
|
174
|
+
a >= 224);
|
|
175
|
+
}
|
|
176
|
+
function parseIpv6(address) {
|
|
177
|
+
let value = address.toLowerCase();
|
|
178
|
+
const dotted = value.match(/(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
179
|
+
if (dotted) {
|
|
180
|
+
const ipv4 = parseIpv4(dotted);
|
|
181
|
+
if (!ipv4)
|
|
182
|
+
return undefined;
|
|
183
|
+
value = value.slice(0, -dotted.length) + `${((ipv4[0] << 8) | ipv4[1]).toString(16)}:${((ipv4[2] << 8) | ipv4[3]).toString(16)}`;
|
|
184
|
+
}
|
|
185
|
+
if ((value.match(/::/g)?.length ?? 0) > 1)
|
|
186
|
+
return undefined;
|
|
187
|
+
const [leftRaw, rightRaw = ""] = value.split("::");
|
|
188
|
+
const left = leftRaw ? leftRaw.split(":") : [];
|
|
189
|
+
const right = rightRaw ? rightRaw.split(":") : [];
|
|
190
|
+
const missing = 8 - left.length - right.length;
|
|
191
|
+
if ((value.includes("::") && missing < 1) || (!value.includes("::") && missing !== 0))
|
|
192
|
+
return undefined;
|
|
193
|
+
const groups = [...left, ...Array.from({ length: missing }, () => "0"), ...right];
|
|
194
|
+
if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/i.test(group)))
|
|
195
|
+
return undefined;
|
|
196
|
+
return groups.map((group) => Number.parseInt(group, 16));
|
|
197
|
+
}
|
|
198
|
+
function isBlockedAddress(address) {
|
|
199
|
+
const value = address.toLowerCase().replace(/^\[|\]$/g, "").split("%")[0];
|
|
200
|
+
const ipv4 = parseIpv4(value);
|
|
201
|
+
if (ipv4)
|
|
202
|
+
return isBlockedIpv4(ipv4);
|
|
203
|
+
if (!value.includes(":"))
|
|
204
|
+
return false;
|
|
205
|
+
const groups = parseIpv6(value);
|
|
206
|
+
if (!groups)
|
|
207
|
+
return true;
|
|
208
|
+
const [first, second, third, fourth, fifth, sixth, seventh, eighth] = groups;
|
|
209
|
+
if (groups.every((group) => group === 0) || groups.slice(0, 7).every((group) => group === 0) && eighth === 1)
|
|
210
|
+
return true;
|
|
211
|
+
if ((first & 0xfe00) === 0xfc00 || (first & 0xffc0) === 0xfe80 || (first & 0xff00) === 0xff00)
|
|
212
|
+
return true;
|
|
213
|
+
if (first === 0x2001 && second === 0x0db8)
|
|
214
|
+
return true;
|
|
215
|
+
if (first === 0x0064 && second === 0xff9b)
|
|
216
|
+
return true;
|
|
217
|
+
if (first === 0x0100 && second === 0 && third === 0 && fourth === 0)
|
|
218
|
+
return true;
|
|
219
|
+
if (first === 0x2002)
|
|
220
|
+
return true;
|
|
221
|
+
// IPv4-mapped and IPv4-compatible addresses can otherwise conceal loopback,
|
|
222
|
+
// link-local, metadata, or RFC1918 destinations in hexadecimal form.
|
|
223
|
+
if (first === 0 && second === 0 && third === 0 && fourth === 0 && fifth === 0 && (sixth === 0 || sixth === 0xffff)) {
|
|
224
|
+
return isBlockedIpv4([seventh >> 8, seventh & 0xff, eighth >> 8, eighth & 0xff]);
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const BLOCKED_HOST = /^(?:localhost|localhost\.localdomain|metadata|metadata\.google\.internal|instance-data|kubernetes\.default\.svc)$/i;
|
|
229
|
+
const BLOCKED_SUFFIX = /\.(?:localhost|local|internal|home|lan|corp|test|invalid|example|onion)$/i;
|
|
230
|
+
/**
|
|
231
|
+
* Validate each URL and redirect before server-side fetching. Callers should
|
|
232
|
+
* provide resolveHost in production and pin/recheck the chosen address at fetch
|
|
233
|
+
* time to close DNS-rebinding gaps.
|
|
234
|
+
*/
|
|
235
|
+
async function validatePublicUrl(input, options = {}) {
|
|
236
|
+
const errors = [];
|
|
237
|
+
const warnings = [];
|
|
238
|
+
const resolvedAddresses = [];
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = new URL(input);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
errors.push("URL is not valid.");
|
|
245
|
+
}
|
|
246
|
+
if (!parsed) {
|
|
247
|
+
return { ok: false, valid: false, errors, warnings, resolvedAddresses, requiresDnsVerification: false };
|
|
248
|
+
}
|
|
249
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
250
|
+
errors.push("Only HTTP and HTTPS URLs are allowed.");
|
|
251
|
+
if (parsed.protocol === "http:" && options.allowHttp === false)
|
|
252
|
+
errors.push("Plain HTTP URLs are disabled.");
|
|
253
|
+
else if (parsed.protocol === "http:")
|
|
254
|
+
warnings.push("Plain HTTP content is unauthenticated; prefer HTTPS.");
|
|
255
|
+
if (parsed.username || parsed.password)
|
|
256
|
+
errors.push("URLs containing credentials are not allowed.");
|
|
257
|
+
const allowedPorts = options.allowedPorts ?? [80, 443];
|
|
258
|
+
if (parsed.port && !allowedPorts.includes(Number(parsed.port)))
|
|
259
|
+
errors.push(`Port ${parsed.port} is not allowed.`);
|
|
260
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
261
|
+
if (!hostname || BLOCKED_HOST.test(hostname) || BLOCKED_SUFFIX.test(hostname))
|
|
262
|
+
errors.push("Private or local hostnames are not allowed.");
|
|
263
|
+
if (hostname === "169.254.169.254" || hostname === "100.100.100.200")
|
|
264
|
+
errors.push("Cloud metadata endpoints are not allowed.");
|
|
265
|
+
if (isBlockedAddress(hostname))
|
|
266
|
+
errors.push("Private, loopback, link-local, documentation, or reserved IP addresses are not allowed.");
|
|
267
|
+
if (!hostname.includes(".") && !hostname.includes(":"))
|
|
268
|
+
errors.push("Single-label hostnames are not allowed.");
|
|
269
|
+
const literalIp = Boolean(parseIpv4(hostname) || hostname.includes(":"));
|
|
270
|
+
if (!errors.length && options.resolveHost && !literalIp) {
|
|
271
|
+
try {
|
|
272
|
+
const addresses = await options.resolveHost(hostname);
|
|
273
|
+
resolvedAddresses.push(...new Set(addresses.map((address) => address.trim()).filter(Boolean)));
|
|
274
|
+
if (!resolvedAddresses.length)
|
|
275
|
+
errors.push("Hostname did not resolve to a public address.");
|
|
276
|
+
if (resolvedAddresses.some(isBlockedAddress))
|
|
277
|
+
errors.push("Hostname resolves to a private or reserved address.");
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
errors.push("Hostname resolution failed; URL fetch was denied.");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const requiresDnsVerification = !literalIp && !options.resolveHost;
|
|
284
|
+
if (requiresDnsVerification && !errors.length) {
|
|
285
|
+
warnings.push("Resolve and verify the hostname again at fetch time; syntax validation alone does not prevent DNS rebinding.");
|
|
286
|
+
}
|
|
287
|
+
parsed.hash = "";
|
|
288
|
+
return {
|
|
289
|
+
ok: errors.length === 0,
|
|
290
|
+
valid: errors.length === 0,
|
|
291
|
+
url: errors.length ? undefined : parsed.toString(),
|
|
292
|
+
hostname,
|
|
293
|
+
errors: [...new Set(errors)],
|
|
294
|
+
warnings: [...new Set(warnings)],
|
|
295
|
+
resolvedAddresses,
|
|
296
|
+
requiresDnsVerification,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./normalize"), exports);
|
|
19
|
+
__exportStar(require("./brief"), exports);
|
|
20
|
+
__exportStar(require("./candidates"), exports);
|
|
21
|
+
__exportStar(require("./candidate-sort"), exports);
|
|
22
|
+
__exportStar(require("./brief-constraints"), exports);
|
|
23
|
+
__exportStar(require("./namesake"), exports);
|
|
24
|
+
__exportStar(require("./editorial-review"), exports);
|
|
25
|
+
__exportStar(require("./scoring"), exports);
|
|
26
|
+
__exportStar(require("./domain"), exports);
|
|
27
|
+
__exportStar(require("./namespaces"), exports);
|
|
28
|
+
__exportStar(require("./import-safety"), exports);
|
|
29
|
+
__exportStar(require("./url-input"), exports);
|
|
30
|
+
__exportStar(require("./export"), exports);
|
|
31
|
+
__exportStar(require("./usage"), exports);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyNamesakeSources = classifyNamesakeSources;
|
|
4
|
+
const normalize_1 = require("./normalize");
|
|
5
|
+
const CATEGORY_PROFILES = [
|
|
6
|
+
[/(?:product naming|naming decision|brand name|name generator|domain.*name|trademark)/i, /(?:product naming|naming decision|brand name|name generator|domain|trademark|social handle|name validation)/i],
|
|
7
|
+
[/(?:workout|exercise|fitness|reps?|sets?|cadence|bodyweight)/i, /(?:workout|exercise|fitness|training|interval timer|reps?|sets?|bodyweight)/i],
|
|
8
|
+
[/(?:developer|software|api|cli|code|programming)/i, /(?:developer|software|api|cli|code|programming|documentation)/i],
|
|
9
|
+
[/(?:bookkeep|accounting|expense|invoice)/i, /(?:bookkeep|accounting|expense|invoice|finance)/i],
|
|
10
|
+
];
|
|
11
|
+
function semanticNameKey(value) {
|
|
12
|
+
return value.replace(/^naming/, "name");
|
|
13
|
+
}
|
|
14
|
+
function sourceHost(url) {
|
|
15
|
+
try {
|
|
16
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function classifyNamesakeSources(name, brief, sources) {
|
|
23
|
+
const nameKey = (0, normalize_1.comparisonKey)(name);
|
|
24
|
+
const briefText = [brief?.product, brief?.pain, brief?.outcome, brief?.mechanism].filter(Boolean).join(" ");
|
|
25
|
+
return sources.map((source) => {
|
|
26
|
+
const host = sourceHost(source.url);
|
|
27
|
+
const sourceText = [source.title, source.url, ...(source.highlights ?? []), source.excerpt ?? ""].join(" ");
|
|
28
|
+
const sourceWords = new Set((0, normalize_1.normalizeName)(sourceText).split(" ").map(normalize_1.comparisonKey).filter(Boolean));
|
|
29
|
+
const exactName = sourceWords.has(nameKey) || host.split(".")[0] === nameKey;
|
|
30
|
+
const nearName = !exactName && ([...sourceWords].some((word) => semanticNameKey(word) === semanticNameKey(nameKey)) ||
|
|
31
|
+
semanticNameKey(host.split(".")[0]) === semanticNameKey(nameKey));
|
|
32
|
+
const lexicalNear = !exactName && [...sourceWords].some((word) => word.length > nameKey.length && nameKey.length >= 5 && word.startsWith(nameKey));
|
|
33
|
+
const profile = CATEGORY_PROFILES.find(([briefPattern]) => briefPattern.test(briefText));
|
|
34
|
+
const sameCategory = profile ? profile[1].test(sourceText) : false;
|
|
35
|
+
const codeTokenNoise = !sameCategory && /\b(?:api documentation|programming reference|class|method|symbol|code token)\b/i.test(sourceText);
|
|
36
|
+
const trustedStore = host === "play.google.com" || host === "apps.apple.com" || host === "appstore.com";
|
|
37
|
+
const officialMatchingHost = host.split(".")[0] === nameKey;
|
|
38
|
+
const activeProductSignal = (exactName || nearName) && (trustedStore || officialMatchingHost || /\b(?:app|product|company|service|platform|validation)\b/i.test(source.title));
|
|
39
|
+
const blocker = (exactName || nearName) && sameCategory && activeProductSignal;
|
|
40
|
+
const semanticClass = exactName && blocker
|
|
41
|
+
? "exact_active_same_category"
|
|
42
|
+
: nearName && blocker
|
|
43
|
+
? "near_active_same_category"
|
|
44
|
+
: exactName && sameCategory
|
|
45
|
+
? "exact_same_category"
|
|
46
|
+
: exactName && !codeTokenNoise
|
|
47
|
+
? "exact_other_category"
|
|
48
|
+
: lexicalNear
|
|
49
|
+
? "lexical_near_match"
|
|
50
|
+
: "unrelated";
|
|
51
|
+
return { ...source, semanticClass, exactName, nearName, sameCategory, activeProductSignal, blocker };
|
|
52
|
+
});
|
|
53
|
+
}
|