npm package: @originmap/no-scan
Description: Opt out of Origin Map scanning — meta tag, well-known file, multi-framework support, security middleware
Latest version: 2.0.0

<p align="center">
  <img src="https://originmap.net/favicon.png" width="48" height="48" alt="Origin Map" />
</p>

<h1 align="center">@originmap/no-scan</h1>

<p align="center">
  Excluye tu sitio del escaneo de <a href="https://originmap.net">Origin Map</a>.
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/@originmap/no-scan"><img src="https://img.shields.io/npm/v/@originmap/no-scan?color=%2358d5ba&label=npm" alt="npm version" /></a>
  <a href="https://www.npmjs.com/package/@originmap/no-scan"><img src="https://img.shields.io/npm/dm/@originmap/no-scan?color=%2396a0aa" alt="downloads" /></a>
  <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license" />
  <a href="https://doc.originmap.net/es/no-scan/primeros-pasos"><img src="https://img.shields.io/badge/docs-doc.originmap.net-58d5ba" alt="docs" /></a>
  <a href="https://discord.gg/originmap"><img src="https://img.shields.io/badge/Discord-originmap-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
</p>

<p align="center">
  <a href="https://doc.originmap.net/es/no-scan/primeros-pasos"><b>Documentación</b></a>
</p>

---

## Qué hace

Origin Map es un toolkit de ingeniería inversa para apps web. Si quieres que tu sitio **no sea escaneado**, instala este paquete.

Emite dos señales que Origin Map detecta antes de escanear:

| Señal | Dónde | Cómo |
|-------|-------|------|
| `<meta name="originmap" content="no-scan">` | HTML `<head>` | Inyectado client-side (React, Vue, Svelte, Angular, vanilla JS) |
| `/.well-known/originmap.jsonc` | Ruta del servidor | Servido via middleware Express o archivo estático |

**Cualquiera de las dos** es suficiente. Ambas juntas dan máxima cobertura.

Además incluye middleware de **seguridad**: bot guard, rate limiter y shield (todo en uno).

---

## Instalación

```bash
npm install @originmap/no-scan
```

```bash
yarn add @originmap/no-scan
```

```bash
pnpm add @originmap/no-scan
```

```bash
bun add @originmap/no-scan
```

---

## Inicio rápido

### React / Next.js / Remix

```tsx
import { OriginMapNoScan } from "@originmap/no-scan"

export default function App() {
  return (
    <>
      <OriginMapNoScan domain="tusitio.com" />
      {/* tu app */}
    </>
  )
}
```

> Funciona con React 16.8+ (hooks). El prop `domain` es opcional — usa `window.location.hostname` por default.

### Vue 3

```vue
<script setup>
import { useOriginMapNoScan } from "@originmap/no-scan/vue"

useOriginMapNoScan("tusitio.com")
</script>
```

O como componente:

```vue
<template>
  <OriginMapNoScanVue domain="tusitio.com" />
</template>

<script setup>
import { OriginMapNoScanVue } from "@originmap/no-scan/vue"
</script>
```

### Svelte

```svelte
<script>
  import { originMapNoScan } from "@originmap/no-scan/svelte"
</script>

<div use:originMapNoScan={"tusitio.com"}></div>
```

O en el script:

```svelte
<script>
  import { injectNoScan } from "@originmap/no-scan/svelte"
  import { onMount } from "svelte"

  onMount(() => injectNoScan("tusitio.com"))
</script>
```

### Angular

```typescript
import { Component } from "@angular/core"
import { OriginMapNoScanService } from "@originmap/no-scan/angular"

@Component({ selector: "app-root", template: "..." })
export class AppComponent {
  constructor(private noScan: OriginMapNoScanService) {
    noScan.activate("tusitio.com")
  }
}
```

Sin inyección de dependencias:

```typescript
import { activateNoScan } from "@originmap/no-scan/angular"

activateNoScan("tusitio.com")
```

### Vanilla JS

```html
<script type="module">
  import { injectMetaTag } from "@originmap/no-scan"
  injectMetaTag("tusitio.com")
</script>
```

### Express / Connect / Fastify

```typescript
import express from "express"
import { originMapNoScan } from "@originmap/no-scan/middleware"

const app = express()
app.use(originMapNoScan({ domain: "tusitio.com" }))
```

> El `domain` es opcional — se auto-detecta desde `req.hostname`.

### Shield (todo en uno)

```typescript
import express from "express"
import { originMapShield } from "@originmap/no-scan/middleware"

const app = express()
app.use(originMapShield({ domain: "tusitio.com" }))
```

Un solo middleware que aplica: **rate limit** → **bot guard** → **well-known**.

### Archivo estático (SSG / CDN)

```typescript
import { generateWellKnown } from "@originmap/no-scan"
import { writeFileSync, mkdirSync } from "fs"

const content = await generateWellKnown("tusitio.com")
mkdirSync("public/.well-known", { recursive: true })
writeFileSync("public/.well-known/originmap.jsonc", content)
```

### Manual (sin paquete)

**Meta tag HTML:**
```html
<meta name="originmap" content="no-scan">
```

**Archivo estático** en `/.well-known/originmap.jsonc`:
```json
{
  "originmap": "no-scan",
  "domain": "tusitio.com"
}
```

---

## API

### Cliente — `@originmap/no-scan`

| Export | Tipo | Descripción |
|--------|------|-------------|
| `OriginMapNoScan` | Componente React | Inyecta meta tag via `useEffect`. Props: `{ domain?: string }` |
| `injectMetaTag(domain?)` | `Promise<void>` | Vanilla JS — agrega meta tag al `<head>` |
| `generateWellKnown(domain)` | `Promise<string>` | Genera el contenido JSONC del well-known file |
| `generateToken(domain)` | `Promise<string>` | Genera token de verificación HMAC-SHA256 |

### Vue — `@originmap/no-scan/vue`

| Export | Tipo | Descripción |
|--------|------|-------------|
| `useOriginMapNoScan(domain?)` | Composable | Vue 3 composable — inyecta en `onMounted` |
| `OriginMapNoScanVue` | Componente | Componente Vue 3 con prop `domain` |
| `injectMetaTag(domain?)` | `Promise<void>` | Re-export de la función base |

### Svelte — `@originmap/no-scan/svelte`

| Export | Tipo | Descripción |
|--------|------|-------------|
| `originMapNoScan(node, domain?)` | Action | Svelte action |
| `injectNoScan(domain?)` | `Promise<void>` | Alias de `injectMetaTag` |
| `injectMetaTag(domain?)` | `Promise<void>` | Re-export de la función base |

### Angular — `@originmap/no-scan/angular`

| Export | Tipo | Descripción |
|--------|------|-------------|
| `OriginMapNoScanService` | Servicio | Angular service con `activate(domain?)` — SSR-safe |
| `activateNoScan(domain?)` | `void` | Función standalone sin DI |
| `injectMetaTag(domain?)` | `Promise<void>` | Re-export de la función base |

### Middleware — `@originmap/no-scan/middleware`

| Export | Tipo | Descripción |
|--------|------|-------------|
| `originMapNoScan(opts?)` | Middleware | Sirve `/.well-known/originmap.jsonc` |
| `botGuard(opts?)` | Middleware | Bloquea bots/scrapers por User-Agent |
| `rateLimit(opts?)` | Middleware | Rate limit por IP (50 req/s default) |
| `originMapShield(opts?)` | Middleware | Todo en uno: rate limit + bot guard + well-known |

---

## Seguridad

### Bot Guard

Bloquea user-agents de bots, scrapers y herramientas de automatización. Los bots legítimos de buscadores pasan automáticamente.

```typescript
import { botGuard } from "@originmap/no-scan/middleware"

app.use(botGuard({
  blockEmptyUA: true,          // bloquea requests sin User-Agent (default: true)
  customPatterns: [/my-bot/i], // patrones adicionales a bloquear
  allowList: [/mi-crawler/i],  // bots custom a permitir
  onBlocked: (req, res) => {   // respuesta personalizada (default: 403)
    res.statusCode = 403
    res.end("Bloqueado")
  },
}))
```

**Bloqueados:** curl, wget, httpie, python-requests, aiohttp, httpx, scrapy, phantomjs, puppeteer, playwright, selenium, nikto, sqlmap, nmap, dirbuster, gobuster, wpscan, burpsuite, bot, crawler, spider, scraper.

**Permitidos:** googlebot, bingbot, duckduckbot, facebookexternalhit, twitterbot, linkedinbot, whatsapp, telegrambot, discordbot, applebot, gptbot, slackbot, pinterestbot, yandexbot.

### Rate Limiter

Rate limit por IP con fixed-window counter. Devuelve `429 Too Many Requests` con header `Retry-After`.

```typescript
import { rateLimit } from "@originmap/no-scan/middleware"

app.use(rateLimit({
  maxRequests: 50,          // requests por ventana (default: 50)
  windowMs: 1000,           // ventana en ms (default: 1s)
  cleanupIntervalMs: 60000, // limpieza de entries viejas (default: 60s)
  keyGenerator: (req) => getIP(req), // key personalizada
}))
```

Headers en cada respuesta:
- `X-RateLimit-Limit` — límite máximo
- `X-RateLimit-Remaining` — requests restantes
- `X-RateLimit-Reset` — timestamp de reset (epoch seconds)

### Shield

Middleware combinado que aplica todo en orden:

1. **Rate limit** — configurable o desactivable con `false`
2. **Bot guard** — configurable o desactivable con `false`
3. **Well-known** — sirve `/.well-known/originmap.jsonc`

```typescript
import { originMapShield } from "@originmap/no-scan/middleware"

// Configuración personalizada
app.use(originMapShield({
  domain: "tusitio.com",
  rateLimit: { maxRequests: 100, windowMs: 2000 },
  botGuard: { blockEmptyUA: false },
}))

// Desactivar una capa
app.use(originMapShield({
  domain: "tusitio.com",
  rateLimit: false, // sin rate limit
}))
```

---

## Cómo funciona la detección

Cuando un usuario apunta Origin Map a tu sitio, el scanner ejecuta estos checks **antes** de analizar código:

1. Busca `<meta name="originmap" content="no-scan">` en el DOM
2. Hace `GET /.well-known/originmap.jsonc` a tu dominio (timeout 3s)

Si cualquier check devuelve una señal válida de opt-out, el escaneo se **aborta** y el usuario ve un aviso de "Sitio Protegido".

El **token HMAC-SHA256** incluido en ambas señales verifica que el opt-out fue configurado intencionalmente por el dueño del sitio.

---

## Licencia

MIT — [Rurylox LLC](https://originmap.net)