@strand-js/anthropic 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -54,17 +54,74 @@ createStrandHandler({
54
54
  })
55
55
  ```
56
56
 
57
- ### With auth
57
+ ## Production security checklist
58
+
59
+ Before going live, configure these options:
58
60
 
59
61
  ```ts
60
62
  createStrandHandler({
61
63
  apiKey: process.env.ANTHROPIC_API_KEY,
62
64
  model: 'claude-sonnet-4-6',
65
+
66
+ // 1. REQUIRED: authenticate every request
67
+ // Without this, anyone can hit your endpoint and burn your API credits.
63
68
  authorize: async (request) => {
64
- const user = await verifyToken(request.headers.get('authorization'))
65
- if (!user) throw new Error('Unauthorized')
69
+ const token = request.headers.get('authorization')?.replace('Bearer ', '')
70
+ const user = await verifyToken(token)
71
+ if (!user) throw new Error('Unauthorized') // returns 401, no LLM call is made
72
+ },
73
+
74
+ // 2. RECOMMENDED: built-in rate limiting by IP
75
+ // Prevents a single user from spamming your endpoint.
76
+ rateLimit: {
77
+ windowMs: 60_000, // 1 minute window
78
+ maxRequests: 20, // max 20 requests per IP per minute
66
79
  },
80
+
81
+ // 3. RECOMMENDED: limit message size
82
+ // Prevents oversized payloads from reaching the LLM.
83
+ maxMessages: 50,
84
+ maxMessageLength: 10_000,
67
85
  })
68
86
  ```
69
87
 
88
+ **Also configure on your server (outside Strand):**
89
+ - **CORS** — restrict which origins can call your endpoint
90
+ - **HTTPS** — never run in production over HTTP
91
+
92
+ ## What `authorize` is
93
+
94
+ `authorize` is a function you provide that runs before any LLM call. Strand calls it with the incoming request. If it throws, the request is rejected with a 401 and no API credits are used. If it resolves, the request continues.
95
+
96
+ ```ts
97
+ // JWT example
98
+ authorize: async (request) => {
99
+ const token = request.headers.get('authorization')?.replace('Bearer ', '')
100
+ const user = await verifyJWT(token)
101
+ if (!user) throw new Error('Unauthorized')
102
+ }
103
+
104
+ // Simple shared secret example
105
+ authorize: async (request) => {
106
+ if (request.headers.get('x-api-key') !== process.env.MY_SECRET)
107
+ throw new Error('Unauthorized')
108
+ }
109
+ ```
110
+
111
+ ## Config reference
112
+
113
+ | Option | Type | Description |
114
+ |---|---|---|
115
+ | `apiKey` | `string` | Your Anthropic API key |
116
+ | `model` | `string` | Model ID, e.g. `'claude-sonnet-4-6'` |
117
+ | `system` | `string \| (req) => string` | System prompt (static or dynamic) |
118
+ | `tools` | `ToolDefinition[]` | Tools available to the model |
119
+ | `onToolCall` | `async (name, args, ctx) => result` | Server-side tool execution |
120
+ | `authorize` | `async (req) => void` | Throw to reject with 401 |
121
+ | `rateLimit` | `{ windowMs, maxRequests }` | Built-in IP rate limiting |
122
+ | `maxMessages` | `number` | Max messages per request (default: 100) |
123
+ | `maxMessageLength` | `number` | Max chars per message (default: 50,000) |
124
+ | `maxSteps` | `number` | Max tool call rounds (default: 10) |
125
+ | `onFinish` | `(session) => void` | Called after response completes |
126
+
70
127
  [Full documentation →](https://github.com/strand-js/strand)
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ToolDefinition, Session } from '@strand-js/core';
1
+ import { ToolDefinition, RateLimitConfig, Session } from '@strand-js/core';
2
2
 
3
3
  interface StrandHandlerConfig {
4
4
  apiKey: string;
@@ -9,6 +9,7 @@ interface StrandHandlerConfig {
9
9
  request: Request;
10
10
  }) => Promise<unknown>;
11
11
  authorize?: (request: Request) => Promise<unknown> | unknown;
12
+ rateLimit?: RateLimitConfig;
12
13
  maxMessages?: number;
13
14
  maxMessageLength?: number;
14
15
  maxSteps?: number;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ToolDefinition, Session } from '@strand-js/core';
1
+ import { ToolDefinition, RateLimitConfig, Session } from '@strand-js/core';
2
2
 
3
3
  interface StrandHandlerConfig {
4
4
  apiKey: string;
@@ -9,6 +9,7 @@ interface StrandHandlerConfig {
9
9
  request: Request;
10
10
  }) => Promise<unknown>;
11
11
  authorize?: (request: Request) => Promise<unknown> | unknown;
12
+ rateLimit?: RateLimitConfig;
12
13
  maxMessages?: number;
13
14
  maxMessageLength?: number;
14
15
  maxSteps?: number;
package/dist/index.js CHANGED
@@ -61,8 +61,20 @@ function createStrandHandler(config) {
61
61
  const client = new import_sdk.default({ apiKey: config.apiKey });
62
62
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
63
63
  const maxSteps = config.maxSteps ?? 10;
64
+ const rateLimiter = config.rateLimit ? new import_core2.RateLimiter(config.rateLimit) : null;
64
65
  return async (req, res) => {
65
66
  const body = req.body;
67
+ if (rateLimiter) {
68
+ const ip = req.ip ?? "unknown";
69
+ const limited = rateLimiter.check(ip);
70
+ if (limited) {
71
+ res.status(429).json({
72
+ error: "Too many requests",
73
+ retryAfter: limited.retryAfter
74
+ });
75
+ return;
76
+ }
77
+ }
66
78
  const validation = (0, import_core2.validateMessages)(body?.messages, {
67
79
  maxMessages: config.maxMessages,
68
80
  maxMessageLength: config.maxMessageLength
@@ -190,7 +202,18 @@ function createStrandRoute(config) {
190
202
  const client = new import_sdk2.default({ apiKey: config.apiKey });
191
203
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
192
204
  const maxSteps = config.maxSteps ?? 10;
205
+ const rateLimiter = config.rateLimit ? new import_core3.RateLimiter(config.rateLimit) : null;
193
206
  return async (req) => {
207
+ if (rateLimiter) {
208
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
209
+ const limited = rateLimiter.check(ip);
210
+ if (limited) {
211
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
212
+ status: 429,
213
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
214
+ });
215
+ }
216
+ }
194
217
  const body = await req.json();
195
218
  const validation = (0, import_core3.validateMessages)(body?.messages, {
196
219
  maxMessages: config.maxMessages,
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/handler.ts
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
- import { generateId, validateMessages } from "@strand-js/core";
3
+ import { generateId, validateMessages, RateLimiter } from "@strand-js/core";
4
4
 
5
5
  // src/format.ts
6
6
  import { toolToJsonSchema } from "@strand-js/core";
@@ -24,8 +24,20 @@ function createStrandHandler(config) {
24
24
  const client = new Anthropic({ apiKey: config.apiKey });
25
25
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
26
26
  const maxSteps = config.maxSteps ?? 10;
27
+ const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : null;
27
28
  return async (req, res) => {
28
29
  const body = req.body;
30
+ if (rateLimiter) {
31
+ const ip = req.ip ?? "unknown";
32
+ const limited = rateLimiter.check(ip);
33
+ if (limited) {
34
+ res.status(429).json({
35
+ error: "Too many requests",
36
+ retryAfter: limited.retryAfter
37
+ });
38
+ return;
39
+ }
40
+ }
29
41
  const validation = validateMessages(body?.messages, {
30
42
  maxMessages: config.maxMessages,
31
43
  maxMessageLength: config.maxMessageLength
@@ -142,7 +154,7 @@ function createStrandHandler(config) {
142
154
 
143
155
  // src/route.ts
144
156
  import Anthropic2 from "@anthropic-ai/sdk";
145
- import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
157
+ import { generateId as generateId2, validateMessages as validateMessages2, RateLimiter as RateLimiter2 } from "@strand-js/core";
146
158
  function sseChunk(eventType, data) {
147
159
  return new TextEncoder().encode(`event: ${eventType}
148
160
  data: ${JSON.stringify(data)}
@@ -153,7 +165,18 @@ function createStrandRoute(config) {
153
165
  const client = new Anthropic2({ apiKey: config.apiKey });
154
166
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
155
167
  const maxSteps = config.maxSteps ?? 10;
168
+ const rateLimiter = config.rateLimit ? new RateLimiter2(config.rateLimit) : null;
156
169
  return async (req) => {
170
+ if (rateLimiter) {
171
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
172
+ const limited = rateLimiter.check(ip);
173
+ if (limited) {
174
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
175
+ status: 429,
176
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
177
+ });
178
+ }
179
+ }
157
180
  const body = await req.json();
158
181
  const validation = validateMessages2(body?.messages, {
159
182
  maxMessages: config.maxMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strand-js/anthropic",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "MIT",
5
5
  "description": "Anthropic provider adapter for Strand",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "@anthropic-ai/sdk": "^0.40.0",
21
- "@strand-js/core": "0.1.4"
21
+ "@strand-js/core": "0.1.5"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/express": "^5.0.0",