@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.
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkNamespaces = checkNamespaces;
4
+ const normalize_1 = require("./normalize");
5
+ const AUTOMATED = ["npm", "pypi", "apple_app_store", "github"];
6
+ const MANUAL = ["google_play", "chrome_web_store", "x", "instagram", "linkedin", "youtube"];
7
+ const defaultCache = new Map();
8
+ const defaultInflight = new Map();
9
+ class NamespaceTimeoutError extends Error {
10
+ constructor() {
11
+ super("Namespace request timed out.");
12
+ this.name = "NamespaceTimeoutError";
13
+ }
14
+ }
15
+ async function timedFetch(fetcher, url, timeoutMs, init = {}) {
16
+ const controller = new AbortController();
17
+ let timer;
18
+ try {
19
+ const timeout = new Promise((_, reject) => {
20
+ timer = setTimeout(() => {
21
+ controller.abort();
22
+ reject(new NamespaceTimeoutError());
23
+ }, timeoutMs);
24
+ });
25
+ return await Promise.race([fetcher(url, { ...init, signal: controller.signal }), timeout]);
26
+ }
27
+ finally {
28
+ if (timer)
29
+ clearTimeout(timer);
30
+ }
31
+ }
32
+ async function cancelResponseBody(response) {
33
+ if (!response.body)
34
+ return;
35
+ try {
36
+ await response.body.cancel();
37
+ }
38
+ catch {
39
+ // Status-only endpoints do not need their bodies; tolerate runtimes that
40
+ // expose a body stream which is already locked.
41
+ }
42
+ }
43
+ async function readBoundedJson(response, maxBytes) {
44
+ const declared = Number(response.headers.get("content-length") ?? "0");
45
+ if (Number.isFinite(declared) && declared > maxBytes) {
46
+ await cancelResponseBody(response);
47
+ throw new Error("Namespace JSON response exceeded its byte limit.");
48
+ }
49
+ if (!response.body)
50
+ throw new Error("Namespace JSON response body was empty.");
51
+ const reader = response.body.getReader();
52
+ const chunks = [];
53
+ let total = 0;
54
+ try {
55
+ while (true) {
56
+ const { done, value } = await reader.read();
57
+ if (done)
58
+ break;
59
+ if (!value)
60
+ continue;
61
+ total += value.byteLength;
62
+ if (total > maxBytes) {
63
+ await reader.cancel("Namespace JSON response exceeded its byte limit.").catch(() => undefined);
64
+ throw new Error("Namespace JSON response exceeded its byte limit.");
65
+ }
66
+ chunks.push(value);
67
+ }
68
+ }
69
+ finally {
70
+ reader.releaseLock();
71
+ }
72
+ const bytes = new Uint8Array(total);
73
+ let offset = 0;
74
+ for (const chunk of chunks) {
75
+ bytes.set(chunk, offset);
76
+ offset += chunk.byteLength;
77
+ }
78
+ return JSON.parse(new TextDecoder().decode(bytes));
79
+ }
80
+ function source(kind, name, label, status, provider, sourceUrl, checkedAt, detail, evidenceStatus, confidence, responseCode) {
81
+ return {
82
+ namespace: kind,
83
+ label,
84
+ status,
85
+ url: manualUrl(kind, name),
86
+ responseCode,
87
+ detail,
88
+ evidence: {
89
+ id: `namespace:${kind}:${(0, normalize_1.comparisonKey)(name)}`,
90
+ label: `${provider} exact-name check`,
91
+ provider,
92
+ sourceUrl,
93
+ checkedAt,
94
+ status: evidenceStatus,
95
+ confidence,
96
+ value: { namespace: kind, status, responseCode },
97
+ detail,
98
+ },
99
+ };
100
+ }
101
+ function manualUrl(kind, rawName) {
102
+ const slug = (0, normalize_1.toDomainLabel)(rawName);
103
+ const encoded = encodeURIComponent(rawName.trim());
104
+ switch (kind) {
105
+ case "npm":
106
+ return `https://www.npmjs.com/package/${encodeURIComponent(slug)}`;
107
+ case "pypi":
108
+ return `https://pypi.org/project/${encodeURIComponent(slug)}/`;
109
+ case "github":
110
+ return `https://github.com/${encodeURIComponent(slug.slice(0, 39))}`;
111
+ case "apple_app_store":
112
+ return `https://apps.apple.com/us/search?term=${encoded}`;
113
+ case "google_play":
114
+ return `https://play.google.com/store/search?q=${encoded}&c=apps`;
115
+ case "chrome_web_store":
116
+ return `https://chromewebstore.google.com/search/${encoded}`;
117
+ case "x":
118
+ return `https://x.com/${encodeURIComponent(slug.slice(0, 15))}`;
119
+ case "instagram":
120
+ return `https://www.instagram.com/${encodeURIComponent(slug.slice(0, 30))}/`;
121
+ case "linkedin":
122
+ return `https://www.linkedin.com/search/results/companies/?keywords=${encoded}`;
123
+ case "youtube":
124
+ return `https://www.youtube.com/results?search_query=${encoded}`;
125
+ }
126
+ }
127
+ function exactEndpoint(kind, rawName, country) {
128
+ const slug = (0, normalize_1.toDomainLabel)(rawName);
129
+ switch (kind) {
130
+ case "npm":
131
+ return `https://registry.npmjs.org/${encodeURIComponent(slug)}`;
132
+ case "pypi":
133
+ return `https://pypi.org/pypi/${encodeURIComponent(slug)}/json`;
134
+ case "github":
135
+ return `https://api.github.com/users/${encodeURIComponent(slug.slice(0, 39))}`;
136
+ case "apple_app_store":
137
+ return `https://itunes.apple.com/search?term=${encodeURIComponent(rawName.trim())}&entity=software&limit=50&country=${encodeURIComponent(country)}`;
138
+ default:
139
+ return manualUrl(kind, rawName);
140
+ }
141
+ }
142
+ function platformLabel(kind) {
143
+ return {
144
+ npm: "npm package",
145
+ pypi: "PyPI project",
146
+ github: "GitHub account",
147
+ apple_app_store: "Apple App Store",
148
+ google_play: "Google Play",
149
+ chrome_web_store: "Chrome Web Store",
150
+ x: "X handle",
151
+ instagram: "Instagram handle",
152
+ linkedin: "LinkedIn company",
153
+ youtube: "YouTube handle",
154
+ }[kind];
155
+ }
156
+ function providerLabel(kind) {
157
+ return {
158
+ npm: "npm Registry",
159
+ pypi: "PyPI JSON API",
160
+ github: "GitHub REST API",
161
+ apple_app_store: "Apple iTunes Search API",
162
+ google_play: "Google Play manual search",
163
+ chrome_web_store: "Chrome Web Store manual search",
164
+ x: "X manual verification",
165
+ instagram: "Instagram manual verification",
166
+ linkedin: "LinkedIn manual verification",
167
+ youtube: "YouTube manual verification",
168
+ }[kind];
169
+ }
170
+ function manualResult(kind, name, now) {
171
+ const checkedAt = now().toISOString();
172
+ const social = ["x", "instagram", "linkedin", "youtube"].includes(kind);
173
+ const detail = social
174
+ ? "No supported public API confirms handle availability. Automated or blocked page responses are not interpreted; verify manually."
175
+ : "The platform does not provide a suitable public catalog API for this exact check; use the manual search link.";
176
+ return source(kind, name, platformLabel(kind), social ? "unknown" : "manual", providerLabel(kind), manualUrl(kind, name), checkedAt, detail, "manual", "low");
177
+ }
178
+ async function automatedResult(kind, name, options) {
179
+ const fetcher = options.fetch ?? globalThis.fetch?.bind(globalThis);
180
+ if (!fetcher)
181
+ throw new Error("A Fetch-compatible function is required for namespace checks.");
182
+ const now = options.now ?? (() => new Date());
183
+ const timeoutMs = Math.max(50, options.timeoutMs ?? 4_000);
184
+ const country = (options.country ?? "us").toLowerCase();
185
+ const endpoint = exactEndpoint(kind, name, country);
186
+ const provider = providerLabel(kind);
187
+ const checkedAt = now().toISOString();
188
+ const headers = { accept: "application/json" };
189
+ if (kind === "github") {
190
+ headers["x-github-api-version"] = "2022-11-28";
191
+ headers["user-agent"] = "naming-signal/0.1";
192
+ if (options.githubToken)
193
+ headers.authorization = `Bearer ${options.githubToken}`;
194
+ }
195
+ const started = Date.now();
196
+ try {
197
+ const response = await timedFetch(fetcher, endpoint, timeoutMs, { headers });
198
+ const latencyMs = Date.now() - started;
199
+ options.usageLedger?.record({
200
+ kind: "namespace",
201
+ provider,
202
+ task: `namespace.${kind}`,
203
+ calls: 0,
204
+ requests: 1,
205
+ failures: response.status === 429 || response.status >= 500 ? 1 : 0,
206
+ latencyMs,
207
+ });
208
+ if (kind === "apple_app_store" && response.ok) {
209
+ let payload;
210
+ try {
211
+ payload = await readBoundedJson(response, 1_000_000);
212
+ }
213
+ catch {
214
+ return source(kind, name, platformLabel(kind), "unknown", provider, endpoint, checkedAt, "Apple returned an unreadable response; status remains unknown.", "unknown", "low", response.status);
215
+ }
216
+ const target = (0, normalize_1.comparisonKey)(name);
217
+ const exact = (payload.results ?? []).filter((item) => typeof item.trackName === "string" && (0, normalize_1.comparisonKey)(item.trackName) === target);
218
+ if (exact.length) {
219
+ return source(kind, name, platformLabel(kind), "conflict", provider, endpoint, checkedAt, `${exact.length} exact normalized title match${exact.length === 1 ? "" : "es"} found in the ${country.toUpperCase()} storefront.`, "confirmed", "high", response.status);
220
+ }
221
+ return source(kind, name, platformLabel(kind), "likely_available", provider, endpoint, checkedAt, `No exact normalized title match appeared in the first 50 ${country.toUpperCase()} storefront results; other storefronts and title rules remain unchecked.`, "inferred", "low", response.status);
222
+ }
223
+ await cancelResponseBody(response);
224
+ if (response.ok) {
225
+ return source(kind, name, platformLabel(kind), "taken", provider, endpoint, checkedAt, `Exact ${platformLabel(kind).toLowerCase()} lookup returned HTTP ${response.status}.`, "confirmed", "high", response.status);
226
+ }
227
+ if (response.status === 404) {
228
+ return source(kind, name, platformLabel(kind), "likely_available", provider, endpoint, checkedAt, `Exact lookup returned 404. The namespace appears unused, but reserved names and registration rules can still block it.`, "inferred", kind === "github" ? "medium" : "high", response.status);
229
+ }
230
+ return source(kind, name, platformLabel(kind), "unknown", provider, endpoint, checkedAt, response.status === 429
231
+ ? "The provider rate-limited this check; status remains unknown."
232
+ : `Provider returned ambiguous HTTP ${response.status}; status remains unknown.`, "unknown", "low", response.status);
233
+ }
234
+ catch (error) {
235
+ options.usageLedger?.record({
236
+ kind: "namespace",
237
+ provider,
238
+ task: `namespace.${kind}`,
239
+ calls: 0,
240
+ requests: 1,
241
+ failures: 1,
242
+ latencyMs: Date.now() - started,
243
+ });
244
+ return source(kind, name, platformLabel(kind), "unknown", provider, endpoint, checkedAt, error instanceof NamespaceTimeoutError || (error instanceof Error && error.name === "AbortError")
245
+ ? "Provider request timed out; status remains unknown."
246
+ : "Provider request failed; status remains unknown.", "unknown", "low");
247
+ }
248
+ }
249
+ async function checkOne(kind, name, options) {
250
+ const now = options.now ?? (() => new Date());
251
+ if (!AUTOMATED.includes(kind))
252
+ return manualResult(kind, name, now);
253
+ const cacheKey = `namespace:v1:${kind}:${(0, normalize_1.comparisonKey)(name)}:${options.country ?? "us"}`;
254
+ const local = !options.fetch ? defaultCache.get(cacheKey) : undefined;
255
+ if (local && local.expiresAt > now().getTime()) {
256
+ options.usageLedger?.record({ kind: "namespace", provider: "core-cache", task: `namespace.${kind}.cache`, calls: 0, requests: 0, cacheHits: 1 });
257
+ return { ...local.value, cached: true, evidence: { ...local.value.evidence } };
258
+ }
259
+ const external = await options.cache?.get(cacheKey);
260
+ if (external) {
261
+ options.usageLedger?.record({ kind: "namespace", provider: "core-cache", task: `namespace.${kind}.cache`, calls: 0, requests: 0, cacheHits: 1 });
262
+ return { ...external, cached: true, evidence: { ...external.evidence } };
263
+ }
264
+ const existing = !options.fetch ? defaultInflight.get(cacheKey) : undefined;
265
+ if (existing) {
266
+ const result = await existing;
267
+ return { ...result, evidence: { ...result.evidence } };
268
+ }
269
+ const operation = automatedResult(kind, name, options);
270
+ if (!options.fetch)
271
+ defaultInflight.set(cacheKey, operation);
272
+ try {
273
+ const result = await operation;
274
+ const ttl = result.status === "unknown" ? options.unknownCacheTtlMs ?? 15_000 : options.cacheTtlMs ?? 5 * 60_000;
275
+ if (ttl > 0) {
276
+ if (!options.fetch)
277
+ defaultCache.set(cacheKey, { value: result, expiresAt: now().getTime() + ttl });
278
+ await options.cache?.set(cacheKey, result, ttl);
279
+ }
280
+ return { ...result, evidence: { ...result.evidence } };
281
+ }
282
+ finally {
283
+ if (!options.fetch)
284
+ defaultInflight.delete(cacheKey);
285
+ }
286
+ }
287
+ /** Check exact public namespaces. Unsupported/social platforms remain manual or unknown, never "free". */
288
+ async function checkNamespaces(name, options = {}) {
289
+ if (typeof name !== "string" || !(0, normalize_1.toDomainLabel)(name))
290
+ throw new TypeError("A non-empty name is required.");
291
+ let kinds = options.include ? [...new Set(options.include)] : [...AUTOMATED];
292
+ if (options.includeManual)
293
+ kinds = [...new Set([...kinds, ...MANUAL])];
294
+ return Promise.all(kinds.map((kind) => checkOne(kind, name, options)));
295
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeName = normalizeName;
4
+ exports.toDomainLabel = toDomainLabel;
5
+ exports.normalizeDomain = normalizeDomain;
6
+ exports.comparisonKey = comparisonKey;
7
+ const TRADEMARK_MARKS = /[\u00ae\u2122\u2120]/gu;
8
+ const DASHES = /[\u2010-\u2015\u2212]/gu;
9
+ const COMBINING_MARKS = /\p{M}+/gu;
10
+ const NON_NAME_CHARACTERS = /[^a-z0-9]+/g;
11
+ /**
12
+ * Produce a comparison-safe form while keeping meaningful word boundaries.
13
+ * This is intentionally not a display-name formatter or a domain lookup.
14
+ */
15
+ function normalizeName(input) {
16
+ if (typeof input !== "string")
17
+ return "";
18
+ return input
19
+ .replace(TRADEMARK_MARKS, "")
20
+ .normalize("NFKD")
21
+ .replace(COMBINING_MARKS, "")
22
+ .replace(DASHES, "-")
23
+ .replace(/&/g, " and ")
24
+ .toLowerCase()
25
+ .replace(NON_NAME_CHARACTERS, " ")
26
+ .trim()
27
+ .replace(/\s+/g, " ");
28
+ }
29
+ function toDomainLabel(input) {
30
+ return normalizeName(input)
31
+ .replace(/\band\b/g, "and")
32
+ .replace(/\s+/g, "")
33
+ .replace(/[^a-z0-9-]/g, "")
34
+ .replace(/-+/g, "-")
35
+ .replace(/^-+|-+$/g, "")
36
+ .slice(0, 63);
37
+ }
38
+ /** Normalize and validate a fully-qualified domain, or build one from a name and TLD. */
39
+ function normalizeDomain(input, tld) {
40
+ if (typeof input !== "string" || !input.trim()) {
41
+ throw new TypeError("A domain or name is required.");
42
+ }
43
+ let hostname;
44
+ if (tld !== undefined) {
45
+ const label = toDomainLabel(input);
46
+ const normalizedTld = String(tld).trim().toLowerCase().replace(/^\.+/, "").replace(/\.+$/, "");
47
+ if (!label || !normalizedTld)
48
+ throw new TypeError("The name and TLD must contain valid DNS characters.");
49
+ hostname = `${label}.${normalizedTld}`;
50
+ }
51
+ else {
52
+ const raw = input.trim();
53
+ try {
54
+ const parsed = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`);
55
+ hostname = parsed.hostname;
56
+ }
57
+ catch {
58
+ throw new TypeError(`Invalid fully-qualified domain: ${input}`);
59
+ }
60
+ }
61
+ hostname = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
62
+ if (hostname.length > 253 || !hostname.includes(".")) {
63
+ throw new TypeError(`Invalid fully-qualified domain: ${input}`);
64
+ }
65
+ const labels = hostname.split(".");
66
+ if (labels.some((label) => label.length < 1 ||
67
+ label.length > 63 ||
68
+ !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label))) {
69
+ throw new TypeError(`Invalid fully-qualified domain: ${input}`);
70
+ }
71
+ return hostname;
72
+ }
73
+ function comparisonKey(input) {
74
+ return normalizeName(input).replace(/\s+/g, "");
75
+ }