@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 +60 -3
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +23 -0
- package/dist/index.mjs +25 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -54,17 +54,74 @@ createStrandHandler({
|
|
|
54
54
|
})
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
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
|
|
65
|
-
|
|
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.
|
|
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.
|
|
21
|
+
"@strand-js/core": "0.1.5"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/express": "^5.0.0",
|