@arcjet/skills 1.12.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 +201 -0
- package/README.md +153 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +62 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/choose-protections.md +70 -0
- package/docs/cli.md +65 -0
- package/docs/guard.md +142 -0
- package/docs/mcp.md +74 -0
- package/docs/protect.md +186 -0
- package/package.json +67 -0
- package/skills/choose-protections/SKILL.md +44 -0
- package/skills/cli/SKILL.md +44 -0
- package/skills/guard/SKILL.md +85 -0
- package/skills/mcp/SKILL.md +44 -0
- package/skills/protect/SKILL.md +121 -0
package/docs/guard.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Guard non-HTTP JavaScript with Arcjet
|
|
2
|
+
|
|
3
|
+
Source documentation for the `guard` Agent Skill. JavaScript and TypeScript
|
|
4
|
+
only. For HTTP routes, see `docs/protect.md`.
|
|
5
|
+
|
|
6
|
+
## When to use Guard
|
|
7
|
+
|
|
8
|
+
Use `@arcjet/guard` when there is no HTTP request object: agent tool calls,
|
|
9
|
+
MCP server handlers, queue workers, and background jobs.
|
|
10
|
+
|
|
11
|
+
The word "server" on MCP is misleading. MCP tools are invoked over stdio or
|
|
12
|
+
SSE and do not receive an HTTP request.
|
|
13
|
+
|
|
14
|
+
## Client
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { launchArcjet } from "@arcjet/guard";
|
|
18
|
+
|
|
19
|
+
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Create the client once at module scope. Prefer passing the client explicitly.
|
|
23
|
+
`registerArcjet` enables free `guard()` / `capture()` / `flush()` when you
|
|
24
|
+
cannot thread a client. Free `guard()` fail-opens if nothing is registered —
|
|
25
|
+
check `hasFailedOpen()` and do not treat that ALLOW as a pass.
|
|
26
|
+
|
|
27
|
+
Declare rules at module scope so `.deniedResult(decision)` and
|
|
28
|
+
`.errorResult(decision)` have a stable reference.
|
|
29
|
+
|
|
30
|
+
## One `guard()` per operation
|
|
31
|
+
|
|
32
|
+
Call `guard()` where you already know the operation. Hardcode the `label`.
|
|
33
|
+
Do not interpolate names in a generic dispatcher.
|
|
34
|
+
|
|
35
|
+
Labels are slugs: lowercase letters, digits, dash, and dot only; must start
|
|
36
|
+
and end with a letter or digit; max 256 bytes. Use `tools.get-weather`, not
|
|
37
|
+
`tools.get_weather`. Metadata keys may contain underscores; labels and
|
|
38
|
+
rate-limit `bucket` names may not.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const decision = await arcjet.guard("tools.get-weather", {
|
|
42
|
+
rules: [/* ... */],
|
|
43
|
+
metadata: { user: { id: userId } },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (decision.conclusion === "DENY") {
|
|
47
|
+
const rateLimited = toolCallLimit.deniedResult(decision);
|
|
48
|
+
if (rateLimited) {
|
|
49
|
+
throw new Error(`rate limited – retry after unix ${rateLimited.resetAtUnixSeconds}`);
|
|
50
|
+
}
|
|
51
|
+
if (decision.reason === "PROMPT_INJECTION") {
|
|
52
|
+
throw new Error("input flagged as prompt injection");
|
|
53
|
+
}
|
|
54
|
+
return { error: decision.reason };
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`decision.reason` is a flat string on DENY (`"RATE_LIMIT"`,
|
|
59
|
+
`"PROMPT_INJECTION"`, `"SENSITIVE_INFO"`, `"MODERATE_CONTENT"`, …) and
|
|
60
|
+
`undefined` on ALLOW. Branch on which rule denied, not only on `DENY`.
|
|
61
|
+
|
|
62
|
+
`metadata` is nested JSON for the Console. It does not affect the decision.
|
|
63
|
+
Do not put secrets or PII in it.
|
|
64
|
+
|
|
65
|
+
Every rate-limit rule needs a `key` and a `bucket`. Use a user or session id
|
|
66
|
+
when you have one; otherwise a stable identifier you control (`"default"`,
|
|
67
|
+
deployment name). Do not pass an empty string.
|
|
68
|
+
|
|
69
|
+
A denial by one rule still spends the others' budget in the same `guard()`
|
|
70
|
+
call. Split rules across two calls if a PII false positive must not drain a
|
|
71
|
+
rate-limit bucket.
|
|
72
|
+
|
|
73
|
+
`guard()` never throws for runtime degradation. `hasFailedOpen()` is true
|
|
74
|
+
when ALLOW means a rule or the decision could not be processed — that is
|
|
75
|
+
the fail-closed gate. `warnings` are request-validation diagnostics; they
|
|
76
|
+
never change the conclusion. Prefer `hasFailedOpen()` over deprecated
|
|
77
|
+
`hasError()`.
|
|
78
|
+
|
|
79
|
+
Pass `correlationId` to join a guard decision with a request or agent
|
|
80
|
+
trace. It is a dedicated field, not metadata.
|
|
81
|
+
|
|
82
|
+
`localDetectSensitiveInfo()` defaults to WASM and matches four types:
|
|
83
|
+
card, email, phone, IP. Names, addresses, and government / financial
|
|
84
|
+
identifiers need `@arcjet/sensitive-info-rampart` (`backend: rampart()`).
|
|
85
|
+
Listing a Rampart-only entity without that backend never matches.
|
|
86
|
+
`moderateContent()` is Guard-only.
|
|
87
|
+
|
|
88
|
+
## Framework wrappers
|
|
89
|
+
|
|
90
|
+
Prefer the versioned Guard namespaces over hand-wrapping every tool:
|
|
91
|
+
|
|
92
|
+
| SDK | Import |
|
|
93
|
+
| --- | ------ |
|
|
94
|
+
| Vercel AI SDK v7 | `@arcjet/guard/vercel-ai/v7` |
|
|
95
|
+
| Vercel Eve | `@arcjet/guard/vercel-eve/v0` |
|
|
96
|
+
| Mastra | `@arcjet/guard/mastra/v1` |
|
|
97
|
+
| Claude Agent SDK | `@arcjet/guard/claude-agent-sdk/v0` |
|
|
98
|
+
| Claude Managed Agents | `@arcjet/guard/claude-managed-agents/v0` |
|
|
99
|
+
| LangGraph JS | `@arcjet/guard/langgraph/v1` |
|
|
100
|
+
| LangChain JS `createAgent` | `@arcjet/guard/langchain/v1` |
|
|
101
|
+
| OpenAI Agents | `@arcjet/guard/openai-agents/v0` |
|
|
102
|
+
| Genkit JS | `@arcjet/guard/genkit/v1` |
|
|
103
|
+
| Google ADK JS | `@arcjet/guard/google-adk/v2` |
|
|
104
|
+
| Strands Agents JS | `@arcjet/guard/strands-agents/v1` |
|
|
105
|
+
| TanStack AI | `@arcjet/guard/tanstack-ai/v0` |
|
|
106
|
+
|
|
107
|
+
Unversioned aliases do not resolve. Load the matching
|
|
108
|
+
`@arcjet/guard#integrate-arcjet-guard-*` skill for the integration you are
|
|
109
|
+
implementing. Those skills ship in the `@arcjet/guard` package.
|
|
110
|
+
|
|
111
|
+
Framework wrappers fail closed by default when Guard is unavailable. Core
|
|
112
|
+
`guard()` fails open — check `hasFailedOpen()`.
|
|
113
|
+
|
|
114
|
+
JS LangChain `createAgent` is `@arcjet/guard/langchain/v1`, not LangGraph.
|
|
115
|
+
JS Google ADK is `guardPlugin` on the Runner — there is no `guardTool`.
|
|
116
|
+
TanStack AI is `guardMiddleware` — do not use `guardTool` (an `execute`
|
|
117
|
+
throw is swallowed). JS Strands Agents is official `@strands-agents/sdk`,
|
|
118
|
+
not Python `strands`.
|
|
119
|
+
|
|
120
|
+
Every adapter uses one `ArcjetDenialResult` payload
|
|
121
|
+
(`{ arcjetDenied: true, reason, message, retryable }`). Delivery is
|
|
122
|
+
per-framework: return the object (AI SDK, Mastra, OpenAI Agents, Genkit
|
|
123
|
+
`toolResponse.output`, LangGraph, LangChain `guardTool`, Google ADK plugin
|
|
124
|
+
dict, Strands `guardTool`); wrap it (Claude Agent SDK `isError: true`,
|
|
125
|
+
LangChain `guardMiddleware` `ToolMessage`, Strands `guardHooks` cancel
|
|
126
|
+
string, TanStack `onBeforeToolCall` skip); or throw `ArcjetDeniedError`
|
|
127
|
+
(Eve, unless `onDeny: "result"`). A throw from a return-style adapter
|
|
128
|
+
drops the fields.
|
|
129
|
+
|
|
130
|
+
Human-in-the-loop APIs (`needsApproval`, `interrupt()`, `requireApproval`,
|
|
131
|
+
`requireConfirmation`, `canUseTool`) are not policy gates. After a human
|
|
132
|
+
yes, Guard still runs.
|
|
133
|
+
|
|
134
|
+
## Capture and flush
|
|
135
|
+
|
|
136
|
+
`capture()` records that something happened. It never denies. Call `flush()`
|
|
137
|
+
on shutdown. On serverless, pass a platform `waitUntil`.
|
|
138
|
+
|
|
139
|
+
## Verify
|
|
140
|
+
|
|
141
|
+
Invoke the protected function directly. Do not `curl` an HTTP route to test
|
|
142
|
+
Guard. Confirm with `npx @arcjet/cli@latest guards list --site-id <id>`.
|
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Arcjet MCP server
|
|
2
|
+
|
|
3
|
+
Connect an AI coding client to the Arcjet API over HTTP with OAuth. Use this
|
|
4
|
+
when the CLI is not available or the client has built-in MCP support.
|
|
5
|
+
|
|
6
|
+
**Endpoint:** `https://api.arcjet.com/mcp`
|
|
7
|
+
|
|
8
|
+
Verify that URL before connecting. Only connect from trusted clients. Enable
|
|
9
|
+
confirmation prompts for write operations.
|
|
10
|
+
|
|
11
|
+
## Client setup
|
|
12
|
+
|
|
13
|
+
### VS Code
|
|
14
|
+
|
|
15
|
+
`.vscode/mcp.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"servers": {
|
|
20
|
+
"arcjet": {
|
|
21
|
+
"type": "http",
|
|
22
|
+
"url": "https://api.arcjet.com/mcp"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Claude Code
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
claude mcp add arcjet --transport http https://api.arcjet.com/mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Cursor
|
|
35
|
+
|
|
36
|
+
`.cursor/mcp.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"arcjet": {
|
|
42
|
+
"type": "streamable-http",
|
|
43
|
+
"url": "https://api.arcjet.com/mcp"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Windsurf
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"arcjet": {
|
|
55
|
+
"serverUrl": "https://api.arcjet.com/mcp"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Claude Desktop / ChatGPT
|
|
62
|
+
|
|
63
|
+
Add a custom connector named `Arcjet` with URL `https://api.arcjet.com/mcp`.
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
OAuth. The first connection redirects to sign in with an Arcjet account.
|
|
68
|
+
|
|
69
|
+
## Common workflows
|
|
70
|
+
|
|
71
|
+
- Bootstrap: `list-teams` → `list-sites` → `get-site-key` → write `ARCJET_KEY`
|
|
72
|
+
- Investigate: `analyze-traffic` → `list-requests` → `investigate-ip`
|
|
73
|
+
- Remote rules: `create-rule` (DRY_RUN) → `get-dry-run-impact` → `promote-rule`
|
|
74
|
+
- Briefing: `get-security-briefing`
|
package/docs/protect.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Protect HTTP routes with Arcjet
|
|
2
|
+
|
|
3
|
+
Source documentation for the `protect` Agent Skill. JavaScript and TypeScript
|
|
4
|
+
only. For non-HTTP tool calls, jobs, and MCP handlers, see `docs/guard.md`.
|
|
5
|
+
|
|
6
|
+
## When to use request protection
|
|
7
|
+
|
|
8
|
+
Use a framework adapter when the code has an HTTP request object (Next.js
|
|
9
|
+
`Request`, Express `req`, Fastify request, SvelteKit `RequestEvent`).
|
|
10
|
+
|
|
11
|
+
| Runtime / framework | Package |
|
|
12
|
+
| ------------------- | ------- |
|
|
13
|
+
| Next.js | `@arcjet/next` |
|
|
14
|
+
| Node.js (`http`, Express) | `@arcjet/node` |
|
|
15
|
+
| Bun | `@arcjet/bun` |
|
|
16
|
+
| Deno | `@arcjet/deno` |
|
|
17
|
+
| Fastify | `@arcjet/fastify` |
|
|
18
|
+
| NestJS | `@arcjet/nest` |
|
|
19
|
+
| Nuxt | `@arcjet/nuxt` |
|
|
20
|
+
| React Router | `@arcjet/react-router` |
|
|
21
|
+
| Remix | `@arcjet/remix` |
|
|
22
|
+
| SvelteKit | `@arcjet/sveltekit` |
|
|
23
|
+
| Astro | `@arcjet/astro` |
|
|
24
|
+
|
|
25
|
+
Hono on Node uses `@arcjet/node`. Hono on Bun uses `@arcjet/bun`.
|
|
26
|
+
|
|
27
|
+
MCP servers, queue workers, and agent tool calls do **not** receive an HTTP
|
|
28
|
+
request. Use `@arcjet/guard` instead.
|
|
29
|
+
|
|
30
|
+
Server actions, tRPC, and other RPC-over-HTTP still have an HTTP request —
|
|
31
|
+
use request protection.
|
|
32
|
+
|
|
33
|
+
Runtime baseline: Node.js `>=22.21.0 <23 || >=24.5.0`, Bun ≥ 1.3.0, Deno
|
|
34
|
+
`stable` / `lts`. Bump the runtime before installing if the project is below
|
|
35
|
+
those floors.
|
|
36
|
+
|
|
37
|
+
## Client setup
|
|
38
|
+
|
|
39
|
+
Create **one** `arcjet()` client at module scope in a shared file. Use
|
|
40
|
+
`withRule()` for route-specific extras so clones share the decision cache.
|
|
41
|
+
Sibling `arcjet()` constructors do not share cache.
|
|
42
|
+
|
|
43
|
+
Always include `shield({ mode: "LIVE" })` as a base rule.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import arcjet, { shield, detectBot } from "@arcjet/next";
|
|
47
|
+
|
|
48
|
+
export const aj = arcjet({
|
|
49
|
+
key: process.env.ARCJET_KEY!,
|
|
50
|
+
rules: [
|
|
51
|
+
shield({ mode: "LIVE" }),
|
|
52
|
+
detectBot({ mode: "LIVE", allow: ["CATEGORY:SEARCH_ENGINE"] }),
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Rules that omit `mode` default to `DRY_RUN` in the JavaScript HTTP SDKs. Pass
|
|
58
|
+
`mode: "LIVE"` to enforce.
|
|
59
|
+
|
|
60
|
+
Never hardcode `ARCJET_KEY`. Write it to the project's env file. Prefer
|
|
61
|
+
`npx @arcjet/cli@latest` to create a site and fetch a key (see `docs/cli.md`).
|
|
62
|
+
|
|
63
|
+
Install the adapter with the project's package manager. Do not hand-edit
|
|
64
|
+
`package.json` and guess a version.
|
|
65
|
+
|
|
66
|
+
### Frameworks that are not a shared client file
|
|
67
|
+
|
|
68
|
+
- **Astro** — register the integration in `astro.config.mjs` and import
|
|
69
|
+
`arcjet:client`. There is no `lib/arcjet.ts` and no `withRule()`.
|
|
70
|
+
`@arcjet/astro` validates with Zod `.strict()`, so leftover
|
|
71
|
+
`detectPromptInjection({ threshold })` throws at startup.
|
|
72
|
+
- **Nuxt** — register the `@arcjet/nuxt` module. The key lives in
|
|
73
|
+
`nuxt.config.ts`. Import from `#arcjet`. Each `arcjet()` call is its own
|
|
74
|
+
cache — share one server module.
|
|
75
|
+
- **NestJS** — `ArcjetModule.forRoot()` plus `@InjectArcjet()`.
|
|
76
|
+
- **Bun / Deno** — wrap the fetch handler with `aj.handler()` so Arcjet can
|
|
77
|
+
see the socket for client IP. Still call `protect()` inside. Deno imports
|
|
78
|
+
use the `npm:` prefix.
|
|
79
|
+
- **Hono on Node** — type the app with `HttpBindings` from
|
|
80
|
+
`@hono/node-server` and pass `c.env.incoming` to `protect()`. Hono on Bun
|
|
81
|
+
passes `c.req.raw`.
|
|
82
|
+
|
|
83
|
+
### What to pass to `protect()`
|
|
84
|
+
|
|
85
|
+
| Framework | Argument |
|
|
86
|
+
| --------- | -------- |
|
|
87
|
+
| Express / Node.js | `req` |
|
|
88
|
+
| Next.js App Router | `req` |
|
|
89
|
+
| Next.js Server Components / actions | `await request()` from `@arcjet/next` |
|
|
90
|
+
| Fastify | `request` (Fastify request, not raw Node) |
|
|
91
|
+
| NestJS | `req` |
|
|
92
|
+
| SvelteKit / Nuxt | `event` |
|
|
93
|
+
| Remix / React Router | `args` |
|
|
94
|
+
| Astro / Bun / Deno | `request` |
|
|
95
|
+
| Hono on Node | `c.env.incoming` |
|
|
96
|
+
| Hono on Bun | `c.req.raw` |
|
|
97
|
+
|
|
98
|
+
## Call `protect()` in the handler
|
|
99
|
+
|
|
100
|
+
Call `protect()` inside each route handler, once per request. Do not call it
|
|
101
|
+
from Express middleware or Next.js middleware.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
export async function GET(request: Request) {
|
|
105
|
+
const decision = await aj.protect(request);
|
|
106
|
+
|
|
107
|
+
if (decision.isDenied()) {
|
|
108
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Response.json({ ok: true });
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Map denial reasons to HTTP responses only when the status or body should
|
|
116
|
+
differ:
|
|
117
|
+
|
|
118
|
+
- `decision.reason.isRateLimit()` → 429
|
|
119
|
+
- `decision.reason.isEmail()` / `isSensitiveInfo()` / `isPromptInjection()` → 400
|
|
120
|
+
- everything else (bot, shield, filter) → 403
|
|
121
|
+
|
|
122
|
+
`decision.isErrored()` means the SDK failed open. Log it and allow.
|
|
123
|
+
|
|
124
|
+
`detectBot` requires exactly one of `allow` or `deny`. Neither or both throws.
|
|
125
|
+
|
|
126
|
+
`detectPromptInjection` takes `mode` only. Do not pass `threshold`. Do not
|
|
127
|
+
read `score`; branch on `isPromptInjection()` / `injectionDetected`.
|
|
128
|
+
|
|
129
|
+
Rules that need extra input at `protect()` time: `tokenBucket` needs
|
|
130
|
+
`{ requested }`, `validateEmail` / `protectSignup` need `{ email }`,
|
|
131
|
+
`sensitiveInfo` needs `{ sensitiveInfoValue }`, `detectPromptInjection`
|
|
132
|
+
needs `{ detectPromptInjectionMessage }`.
|
|
133
|
+
|
|
134
|
+
Pass `correlationId` when the decision must join another request, guard
|
|
135
|
+
call, or agent trace. It does not affect fingerprinting.
|
|
136
|
+
|
|
137
|
+
## Characteristics and client IP
|
|
138
|
+
|
|
139
|
+
Put a `userId` characteristic on the rule that needs it, then pass a trusted,
|
|
140
|
+
authenticated user ID at protection time. Never rate limit by a client-controlled
|
|
141
|
+
header unless a trusted proxy strips and rewrites it.
|
|
142
|
+
|
|
143
|
+
Treat client-IP provenance as security configuration. When no usable public
|
|
144
|
+
address is available, adapters may fall back to forwarding headers and log one
|
|
145
|
+
`client_ip_provenance="unverified-header"` warning for the lifetime of each
|
|
146
|
+
SDK client. Configure every trusted proxy (`proxies`, or a helper such as
|
|
147
|
+
`cloudflare()`). The application must be reachable only through infrastructure
|
|
148
|
+
that overwrites or safely appends those headers. Malformed proxy entries are
|
|
149
|
+
rejected. `0.0.0.0/0` and `::/0` warn because they trust every peer.
|
|
150
|
+
|
|
151
|
+
If the application already has an independently trusted client IP, pass it as
|
|
152
|
+
`ipSrc` to both `protect()` and the diagnostics API. A non-empty value wins
|
|
153
|
+
over automatic detection. An empty string is treated as omitted. Malformed
|
|
154
|
+
values are rejected. Syntax validation does not prove provenance.
|
|
155
|
+
|
|
156
|
+
Never silence an `unverified-header` warning by copying `X-Forwarded-For` (or
|
|
157
|
+
another client-controlled header) into `ipSrc`. That relabels attacker input
|
|
158
|
+
as trusted. Inspect representative requests with
|
|
159
|
+
`client.clientIpDetails(request)` in `@arcjet/node`, or `findIpDetails()` /
|
|
160
|
+
`resolveClientIp()` from `@arcjet/ip`. Check `ip`, `provenance`, `verified`,
|
|
161
|
+
and `header`. These diagnostics do not consume the once-per-client warning.
|
|
162
|
+
|
|
163
|
+
`protect()` accepts nested-JSON `metadata`. It does not affect fingerprinting.
|
|
164
|
+
Do not put secrets or PII in it. When present, request decisions also expose
|
|
165
|
+
optional IP threat intelligence (`decision.ip.threat`).
|
|
166
|
+
|
|
167
|
+
When present, `decision.ip.threat` is optional IP threat intelligence. Check
|
|
168
|
+
before reading.
|
|
169
|
+
|
|
170
|
+
## Common mistakes
|
|
171
|
+
|
|
172
|
+
- Using `@arcjet/guard` on an HTTP route, or a request adapter on a tool call
|
|
173
|
+
- Calling `protect()` in middleware instead of the route handler
|
|
174
|
+
- Calling `protect()` twice for the same request (double-counts rate limits)
|
|
175
|
+
- Leaving rules in `DRY_RUN` and expecting them to block
|
|
176
|
+
- Passing both `allow` and `deny` to `detectBot`
|
|
177
|
+
- Passing `threshold` to `detectPromptInjection`
|
|
178
|
+
- Copying `X-Forwarded-For` into `ipSrc` to hide an unverified-header warning
|
|
179
|
+
- Hardcoding `ARCJET_KEY`
|
|
180
|
+
|
|
181
|
+
## Verify
|
|
182
|
+
|
|
183
|
+
1. Type-check or build the project.
|
|
184
|
+
2. Hit the protected route (`curl`). To trip a rate limit, loop the request.
|
|
185
|
+
3. Confirm the decision with `npx @arcjet/cli@latest requests list --site-id <id>`
|
|
186
|
+
or the [Console](https://console.arcjet.com).
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcjet/skills",
|
|
3
|
+
"version": "1.12.0",
|
|
4
|
+
"description": "Versioned Agent Skills for the Arcjet JavaScript SDK, shipped with TanStack Intent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-skills",
|
|
7
|
+
"ai",
|
|
8
|
+
"arcjet",
|
|
9
|
+
"security",
|
|
10
|
+
"tanstack-intent"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://arcjet.com",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/arcjet/arcjet-js/issues",
|
|
15
|
+
"email": "support@arcjet.com"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Arcjet",
|
|
20
|
+
"email": "support@arcjet.com",
|
|
21
|
+
"url": "https://arcjet.com"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/arcjet/arcjet-js.git",
|
|
26
|
+
"directory": "arcjet-skills"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"docs",
|
|
31
|
+
"skills"
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"lint": "oxlint --tsconfig=tsconfig.json",
|
|
50
|
+
"format": "oxfmt",
|
|
51
|
+
"format:check": "oxfmt --check",
|
|
52
|
+
"validate-skills": "node ../.github/scripts/intent-cli.mjs validate",
|
|
53
|
+
"stale-skills": "node ../.github/scripts/intent-cli.mjs stale --json",
|
|
54
|
+
"test-api": "node --test -- test/*.test.ts",
|
|
55
|
+
"test-coverage": "node --experimental-test-coverage --test -- test/*.test.ts",
|
|
56
|
+
"test": "npm run build && npm run test-coverage && npm run validate-skills && node ../.github/scripts/check-skill-source-stale.mjs"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@tanstack/intent": "0.3.8",
|
|
60
|
+
"@types/node": "22.20.1",
|
|
61
|
+
"tsdown": "0.22.14",
|
|
62
|
+
"typescript": "7.0.2"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.21.0 <23 || >=24.5.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: choose-protections
|
|
3
|
+
description: Choose which Arcjet rules address a security problem. Use when deciding between detectBot, shield, rate limits, detectPromptInjection, sensitiveInfo, validateEmail, filter, or Guard-only content moderation.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
compatibility: JavaScript and TypeScript Arcjet SDK.
|
|
6
|
+
metadata:
|
|
7
|
+
author: arcjet
|
|
8
|
+
type: core
|
|
9
|
+
library: "@arcjet/skills"
|
|
10
|
+
library_version: "1.12.0" # x-release-please-version
|
|
11
|
+
sources:
|
|
12
|
+
- docs/choose-protections.md
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Choose Arcjet protections
|
|
16
|
+
|
|
17
|
+
Load this skill when you need to pick rules. Then load `@arcjet/skills#protect`
|
|
18
|
+
or `@arcjet/skills#guard` to implement them.
|
|
19
|
+
|
|
20
|
+
| Problem | Rule | Where |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Bots, scrapers, automated clients | `detectBot` (`allow` **or** `deny`, not both) | Request only |
|
|
23
|
+
| SQLi / XSS / common probes | `shield` (`mode: "LIVE"`) | Request only |
|
|
24
|
+
| Cost / abuse quotas | `tokenBucket`, `fixedWindow`, `slidingWindow` | Request and Guard |
|
|
25
|
+
| Jailbreaks, instruction overrides | `detectPromptInjection` (binary; no `threshold`) | Request and Guard |
|
|
26
|
+
| Card / email / phone / IP in text | `sensitiveInfo` / `localDetectSensitiveInfo` | Request and Guard |
|
|
27
|
+
| Names, addresses, gov IDs | same rules + `@arcjet/sensitive-info-rampart` | Server runtime |
|
|
28
|
+
| Unsafe / abusive text | `moderateContent` | Guard only |
|
|
29
|
+
| Signup spam, disposable email | `validateEmail` + `protectSignup` | Request only |
|
|
30
|
+
| VPN / Tor / country / IP lists | `filter` | Request only |
|
|
31
|
+
| Dangerous tool calls | Guard per-tool `label` + rate limits | Guard only |
|
|
32
|
+
|
|
33
|
+
Token bucket: variable-cost AI (`requested` per call). Fixed window: hard cap.
|
|
34
|
+
Sliding window: no boundary burst. Request rate limits default to IP; set
|
|
35
|
+
`characteristics: ["userId"]` for a user key. Guard rate limits always need
|
|
36
|
+
`key` and `bucket`.
|
|
37
|
+
|
|
38
|
+
Default sensitive-info backend matches four structured types. Rampart-only
|
|
39
|
+
entities never match without `backend: rampart()`.
|
|
40
|
+
|
|
41
|
+
`capture()` records that an action happened. It is not a protection rule.
|
|
42
|
+
|
|
43
|
+
Remote bot and filter rules can be managed with the CLI or MCP without a
|
|
44
|
+
redeploy — load `@arcjet/skills#cli` or `@arcjet/skills#mcp`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli
|
|
3
|
+
description: "Connect a project to Arcjet with the CLI: sign in, pick a team and site, and write ARCJET_KEY. Use when bootstrapping Arcjet, listing requests or guards, or managing remote rules from a terminal."
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
compatibility: Any project that can run npx or the arcjet binary.
|
|
6
|
+
metadata:
|
|
7
|
+
author: arcjet
|
|
8
|
+
type: core
|
|
9
|
+
library: "@arcjet/skills"
|
|
10
|
+
library_version: "1.12.0" # x-release-please-version
|
|
11
|
+
sources:
|
|
12
|
+
- docs/cli.md
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Arcjet CLI
|
|
16
|
+
|
|
17
|
+
Preferred way to get a real `ARCJET_KEY` before writing Arcjet code. Do not
|
|
18
|
+
leave the key as a TODO.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx -y @arcjet/cli@latest auth status
|
|
22
|
+
# if not signed in:
|
|
23
|
+
npx -y @arcjet/cli@latest auth login
|
|
24
|
+
|
|
25
|
+
npx -y @arcjet/cli@latest teams list --output json --fields id,name
|
|
26
|
+
npx -y @arcjet/cli@latest sites list --team-id <team_id> --output json --fields id,name
|
|
27
|
+
npx -y @arcjet/cli@latest sites get-key --site-id <site_id> --output json --fields key
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Write `key` to the project's env file as `ARCJET_KEY=ajkey_...`. Match
|
|
31
|
+
existing env-file conventions and `.gitignore`. Never hardcode the key.
|
|
32
|
+
|
|
33
|
+
Frequent use: `npm install -g @arcjet/cli`, `brew install arcjet/tap/arcjet`,
|
|
34
|
+
or `curl -sSfL https://arcjet.com/cli/install.sh | bash`.
|
|
35
|
+
|
|
36
|
+
`--output json` and `--fields` keep agent context small. Confirm with the
|
|
37
|
+
user before write or delete operations (`--confirm`).
|
|
38
|
+
|
|
39
|
+
Investigate: `requests list` / `guards list` / `requests explain` /
|
|
40
|
+
`guards explain` / `briefing`. Remote rules: `rules create` (DRY_RUN) →
|
|
41
|
+
`analyze dry-run-impact` → `rules promote`.
|
|
42
|
+
|
|
43
|
+
If the CLI cannot run, load `@arcjet/skills#mcp` or send the user to
|
|
44
|
+
https://console.arcjet.com.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guard
|
|
3
|
+
description: "Add Arcjet Guard to non-HTTP JavaScript: agent tool calls, MCP handlers, queue workers, and background jobs. Use when there is no HTTP request object, or when the user asks to guard tools, rate-limit agent actions, or block prompt injection on tool arguments."
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
compatibility: JavaScript and TypeScript apps using @arcjet/guard on Node.js >= 22.
|
|
6
|
+
metadata:
|
|
7
|
+
author: arcjet
|
|
8
|
+
type: core
|
|
9
|
+
library: "@arcjet/skills"
|
|
10
|
+
library_version: "1.12.0" # x-release-please-version
|
|
11
|
+
sources:
|
|
12
|
+
- docs/guard.md
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Guard non-HTTP JavaScript
|
|
16
|
+
|
|
17
|
+
Use `@arcjet/guard` when there is no HTTP request. MCP tools, queue workers,
|
|
18
|
+
and agent tool calls are Guard. HTTP routes are `@arcjet/skills#protect`.
|
|
19
|
+
|
|
20
|
+
For a specific vendor SDK, load the matching skill from `@arcjet/guard`
|
|
21
|
+
(`@arcjet/guard#integrate-arcjet-guard-agents`, `-eve`, `-mastra`,
|
|
22
|
+
`-langgraph`, `-langchain`, `-openai-agents`, `-genkit`, `-google-adk`,
|
|
23
|
+
`-strands-agents`, `-tanstack-ai`, `-claude-agent-sdk`,
|
|
24
|
+
`-claude-managed-agents`).
|
|
25
|
+
|
|
26
|
+
## Client
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { launchArcjet } from "@arcjet/guard";
|
|
30
|
+
|
|
31
|
+
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
One client at module scope. Get the key with `@arcjet/skills#cli` first.
|
|
35
|
+
Declare rules at module scope so `.deniedResult(decision)` works.
|
|
36
|
+
|
|
37
|
+
## One `guard()` per operation
|
|
38
|
+
|
|
39
|
+
Hardcode the `label`. Do not interpolate in a generic dispatcher.
|
|
40
|
+
|
|
41
|
+
Labels: lowercase letters, digits, `-`, `.` only; start and end with a letter
|
|
42
|
+
or digit. `tools.get-weather`, not `tools.get_weather`.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const decision = await arcjet.guard("tools.get-weather", {
|
|
46
|
+
rules: [/* ... */],
|
|
47
|
+
metadata: { user: { id: userId } },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (decision.conclusion === "DENY") {
|
|
51
|
+
const rateLimited = toolCallLimit.deniedResult(decision);
|
|
52
|
+
if (rateLimited) {
|
|
53
|
+
return { error: `rate limited, retry after ${rateLimited.resetAtUnixSeconds}` };
|
|
54
|
+
}
|
|
55
|
+
return { error: decision.reason };
|
|
56
|
+
}
|
|
57
|
+
if (decision.hasFailedOpen()) {
|
|
58
|
+
// ALLOW only because a rule could not run — deny here if the site is sensitive
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Core `guard()` fails open — check `hasFailedOpen()`. `warnings` never change
|
|
63
|
+
the conclusion. Rate-limit rules need `key` and `bucket`. Nested `metadata`
|
|
64
|
+
is Console-only; no secrets or PII. `decision.reason` is a flat string on
|
|
65
|
+
DENY and `undefined` on ALLOW.
|
|
66
|
+
|
|
67
|
+
`localDetectSensitiveInfo()` default backend matches card / email / phone /
|
|
68
|
+
IP only. Names and government IDs need `backend: rampart()`.
|
|
69
|
+
`moderateContent()` is Guard-only.
|
|
70
|
+
|
|
71
|
+
`capture()` is visibility, never a deny. `flush()` on shutdown.
|
|
72
|
+
|
|
73
|
+
## Versioned wrappers
|
|
74
|
+
|
|
75
|
+
Import `@arcjet/guard/<vendor>/v<major>` — unversioned paths do not resolve.
|
|
76
|
+
Wrappers fail closed by default. HITL (`needsApproval`, `interrupt()`,
|
|
77
|
+
`requireConfirmation`, `canUseTool`) is not a policy gate; Guard still runs
|
|
78
|
+
after a human yes.
|
|
79
|
+
|
|
80
|
+
Google ADK is `guardPlugin` (no `guardTool`). TanStack AI is
|
|
81
|
+
`guardMiddleware` (do not wrap `execute`). LangChain `createAgent` is
|
|
82
|
+
`/langchain/v1`, not LangGraph.
|
|
83
|
+
|
|
84
|
+
Load `@arcjet/skills#choose-protections` to pick rules. Exact signatures live
|
|
85
|
+
in the installed `@arcjet/guard` types.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp
|
|
3
|
+
description: Connect an AI coding client to the Arcjet MCP server at https://api.arcjet.com/mcp. Use when the client has built-in MCP support or the CLI is not available.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
compatibility: AI clients that support remote MCP over HTTP / OAuth.
|
|
6
|
+
metadata:
|
|
7
|
+
author: arcjet
|
|
8
|
+
type: core
|
|
9
|
+
library: "@arcjet/skills"
|
|
10
|
+
library_version: "1.12.0" # x-release-please-version
|
|
11
|
+
sources:
|
|
12
|
+
- docs/mcp.md
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Arcjet MCP server
|
|
16
|
+
|
|
17
|
+
Endpoint: `https://api.arcjet.com/mcp` only. OAuth on first connect. Enable
|
|
18
|
+
write confirmations. Prefer `@arcjet/skills#cli` when a terminal is available.
|
|
19
|
+
|
|
20
|
+
**Cursor** — `.cursor/mcp.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"arcjet": {
|
|
26
|
+
"type": "streamable-http",
|
|
27
|
+
"url": "https://api.arcjet.com/mcp"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**VS Code** — `.vscode/mcp.json` with `"type": "http"` and the same URL.
|
|
34
|
+
|
|
35
|
+
**Claude Code:**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add arcjet --transport http https://api.arcjet.com/mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Bootstrap: `list-teams` → `list-sites` → `get-site-key` → write `ARCJET_KEY`.
|
|
42
|
+
Investigate: `analyze-traffic`, `list-requests`, `list-guards`,
|
|
43
|
+
`investigate-ip`. Remote rules: `create-rule` (DRY_RUN) →
|
|
44
|
+
`get-dry-run-impact` → `promote-rule`.
|