@strand-js/anthropic 0.1.4 → 0.1.6

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
@@ -57,12 +57,38 @@ data: ${JSON.stringify(data)}
57
57
 
58
58
  `);
59
59
  }
60
+ function normalizeRequest(req) {
61
+ const rawHeaders = req.headers ?? {};
62
+ const normalized = {
63
+ ...req,
64
+ headers: {
65
+ get: (name) => {
66
+ const val = rawHeaders[name.toLowerCase()];
67
+ return Array.isArray(val) ? val[0] ?? null : val ?? null;
68
+ }
69
+ }
70
+ };
71
+ return normalized;
72
+ }
60
73
  function createStrandHandler(config) {
61
74
  const client = new import_sdk.default({ apiKey: config.apiKey });
62
75
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
63
76
  const maxSteps = config.maxSteps ?? 10;
77
+ const rateLimiter = config.rateLimit ? new import_core2.RateLimiter(config.rateLimit) : null;
64
78
  return async (req, res) => {
79
+ const normalizedReq = normalizeRequest(req);
65
80
  const body = req.body;
81
+ if (rateLimiter) {
82
+ const ip = req.ip ?? "unknown";
83
+ const limited = rateLimiter.check(ip);
84
+ if (limited) {
85
+ res.status(429).json({
86
+ error: "Too many requests",
87
+ retryAfter: limited.retryAfter
88
+ });
89
+ return;
90
+ }
91
+ }
66
92
  const validation = (0, import_core2.validateMessages)(body?.messages, {
67
93
  maxMessages: config.maxMessages,
68
94
  maxMessageLength: config.maxMessageLength
@@ -73,7 +99,7 @@ function createStrandHandler(config) {
73
99
  }
74
100
  if (config.authorize) {
75
101
  try {
76
- await config.authorize(req);
102
+ await config.authorize(normalizedReq);
77
103
  } catch (err) {
78
104
  const message = err instanceof Error ? err.message : "Unauthorized";
79
105
  res.status(401).json({ error: message });
@@ -86,7 +112,7 @@ function createStrandHandler(config) {
86
112
  emit(res, "strand:start", { sessionId: (0, import_core2.generateId)(), requestId: (0, import_core2.generateId)() });
87
113
  try {
88
114
  const messages = body.messages;
89
- const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
115
+ const system = typeof config.system === "function" ? await config.system(normalizedReq) : config.system ?? "";
90
116
  const conversation = messages.map((m) => ({
91
117
  role: m.role,
92
118
  content: m.content
@@ -152,7 +178,7 @@ function createStrandHandler(config) {
152
178
  completedTools.map(async (block) => {
153
179
  emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
154
180
  try {
155
- const result = await config.onToolCall?.(block.name, block.input, { request: req });
181
+ const result = await config.onToolCall?.(block.name, block.input, { request: normalizedReq });
156
182
  emit(res, "strand:tool-result", { toolCallId: block.id, result });
157
183
  return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result ?? null) };
158
184
  } catch (err) {
@@ -190,7 +216,18 @@ function createStrandRoute(config) {
190
216
  const client = new import_sdk2.default({ apiKey: config.apiKey });
191
217
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
192
218
  const maxSteps = config.maxSteps ?? 10;
219
+ const rateLimiter = config.rateLimit ? new import_core3.RateLimiter(config.rateLimit) : null;
193
220
  return async (req) => {
221
+ if (rateLimiter) {
222
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
223
+ const limited = rateLimiter.check(ip);
224
+ if (limited) {
225
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
226
+ status: 429,
227
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
228
+ });
229
+ }
230
+ }
194
231
  const body = await req.json();
195
232
  const validation = (0, import_core3.validateMessages)(body?.messages, {
196
233
  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";
@@ -20,12 +20,38 @@ data: ${JSON.stringify(data)}
20
20
 
21
21
  `);
22
22
  }
23
+ function normalizeRequest(req) {
24
+ const rawHeaders = req.headers ?? {};
25
+ const normalized = {
26
+ ...req,
27
+ headers: {
28
+ get: (name) => {
29
+ const val = rawHeaders[name.toLowerCase()];
30
+ return Array.isArray(val) ? val[0] ?? null : val ?? null;
31
+ }
32
+ }
33
+ };
34
+ return normalized;
35
+ }
23
36
  function createStrandHandler(config) {
24
37
  const client = new Anthropic({ apiKey: config.apiKey });
25
38
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
26
39
  const maxSteps = config.maxSteps ?? 10;
40
+ const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : null;
27
41
  return async (req, res) => {
42
+ const normalizedReq = normalizeRequest(req);
28
43
  const body = req.body;
44
+ if (rateLimiter) {
45
+ const ip = req.ip ?? "unknown";
46
+ const limited = rateLimiter.check(ip);
47
+ if (limited) {
48
+ res.status(429).json({
49
+ error: "Too many requests",
50
+ retryAfter: limited.retryAfter
51
+ });
52
+ return;
53
+ }
54
+ }
29
55
  const validation = validateMessages(body?.messages, {
30
56
  maxMessages: config.maxMessages,
31
57
  maxMessageLength: config.maxMessageLength
@@ -36,7 +62,7 @@ function createStrandHandler(config) {
36
62
  }
37
63
  if (config.authorize) {
38
64
  try {
39
- await config.authorize(req);
65
+ await config.authorize(normalizedReq);
40
66
  } catch (err) {
41
67
  const message = err instanceof Error ? err.message : "Unauthorized";
42
68
  res.status(401).json({ error: message });
@@ -49,7 +75,7 @@ function createStrandHandler(config) {
49
75
  emit(res, "strand:start", { sessionId: generateId(), requestId: generateId() });
50
76
  try {
51
77
  const messages = body.messages;
52
- const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
78
+ const system = typeof config.system === "function" ? await config.system(normalizedReq) : config.system ?? "";
53
79
  const conversation = messages.map((m) => ({
54
80
  role: m.role,
55
81
  content: m.content
@@ -115,7 +141,7 @@ function createStrandHandler(config) {
115
141
  completedTools.map(async (block) => {
116
142
  emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
117
143
  try {
118
- const result = await config.onToolCall?.(block.name, block.input, { request: req });
144
+ const result = await config.onToolCall?.(block.name, block.input, { request: normalizedReq });
119
145
  emit(res, "strand:tool-result", { toolCallId: block.id, result });
120
146
  return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result ?? null) };
121
147
  } catch (err) {
@@ -142,7 +168,7 @@ function createStrandHandler(config) {
142
168
 
143
169
  // src/route.ts
144
170
  import Anthropic2 from "@anthropic-ai/sdk";
145
- import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
171
+ import { generateId as generateId2, validateMessages as validateMessages2, RateLimiter as RateLimiter2 } from "@strand-js/core";
146
172
  function sseChunk(eventType, data) {
147
173
  return new TextEncoder().encode(`event: ${eventType}
148
174
  data: ${JSON.stringify(data)}
@@ -153,7 +179,18 @@ function createStrandRoute(config) {
153
179
  const client = new Anthropic2({ apiKey: config.apiKey });
154
180
  const anthropicTools = (config.tools ?? []).map(toolToAnthropicTool);
155
181
  const maxSteps = config.maxSteps ?? 10;
182
+ const rateLimiter = config.rateLimit ? new RateLimiter2(config.rateLimit) : null;
156
183
  return async (req) => {
184
+ if (rateLimiter) {
185
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
186
+ const limited = rateLimiter.check(ip);
187
+ if (limited) {
188
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
189
+ status: 429,
190
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
191
+ });
192
+ }
193
+ }
157
194
  const body = await req.json();
158
195
  const validation = validateMessages2(body?.messages, {
159
196
  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.6",
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.6"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/express": "^5.0.0",